From 5bae32c726b9002fb89f7668a3cb95e528c010bf Mon Sep 17 00:00:00 2001 From: cookfront <cookfront@gmail.com> Date: Mon, 20 Feb 2017 11:49:37 +0800 Subject: [PATCH] picker component --- docs/examples/picker.md | 22 ++++ package.json | 1 - packages/picker/src/draggable.js | 51 ++++++++++ packages/picker/src/picker-column.vue | 141 ++++++++++++++++++++++++-- packages/picker/src/picker.vue | 6 +- packages/zanui-css/src/index.pcss | 1 + packages/zanui-css/src/picker.pcss | 73 +++++++++++++ src/utils/transition.js | 100 ++++++++++++++++++ 8 files changed, 385 insertions(+), 10 deletions(-) create mode 100644 packages/picker/src/draggable.js create mode 100644 packages/zanui-css/src/picker.pcss create mode 100644 src/utils/transition.js diff --git a/docs/examples/picker.md b/docs/examples/picker.md index d9c84223b..f4d8959f3 100644 --- a/docs/examples/picker.md +++ b/docs/examples/picker.md @@ -1,7 +1,29 @@ +<script> +export default { + data() { + return { + pickerColumns: [ + { + values: ['杭州', '宁波', '温州', '嘉兴', '湖州', '绍兴', '金华', '衢州', '舟山', '台州', '丽水'] + } + ] + }; + } +}; +</script> + ## Picker组件 模仿iOS中的`UIPickerView`。 +### 基础用法 + +:::demo 基础用法 +```html +<z-picker :columns="pickerColumns"></z-picker> +``` +::: + ### API | 参数 | 说明 | 类型 | 默认值 | 可选值 | diff --git a/package.json b/package.json index 2090aa0d9..f10b62000 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "rimraf": "^2.5.4", "run-sequence": "^1.2.2", "saladcss-bem": "^0.0.1", - "sass-loader": "^3.2.3", "style-loader": "^0.13.1", "theaterjs": "^3.0.0", "transliteration": "^1.1.11", diff --git a/packages/picker/src/draggable.js b/packages/picker/src/draggable.js new file mode 100644 index 000000000..4f524c986 --- /dev/null +++ b/packages/picker/src/draggable.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; + +let isDragging = false; + +const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window; + +export default function(element, options) { + const moveFn = function(event) { + if (options.drag) { + options.drag(supportTouch ? event.changedTouches[0] || event.touches[0] : event); + } + }; + + const endFn = function(event) { + if (!supportTouch) { + document.removeEventListener('mousemove', moveFn); + document.removeEventListener('mouseup', endFn); + } + document.onselectstart = null; + document.ondragstart = null; + + isDragging = false; + + if (options.end) { + options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event); + } + }; + + element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) { + if (isDragging) return; + document.onselectstart = function() { return false; }; + document.ondragstart = function() { return false; }; + + if (!supportTouch) { + document.addEventListener('mousemove', moveFn); + document.addEventListener('mouseup', endFn); + } + isDragging = true; + + if (options.start) { + event.preventDefault(); + options.start(supportTouch ? event.changedTouches[0] || event.touches[0] : event); + } + }); + + if (supportTouch) { + element.addEventListener('touchmove', moveFn); + element.addEventListener('touchend', endFn); + element.addEventListener('touchcancel', endFn); + } +}; diff --git a/packages/picker/src/picker-column.vue b/packages/picker/src/picker-column.vue index b07316837..f0ab04053 100644 --- a/packages/picker/src/picker-column.vue +++ b/packages/picker/src/picker-column.vue @@ -1,14 +1,21 @@ <template> <div class="z-picker-column"> - <div class="z-picker-column-wrapper"> - <div class="z-picker-item"> - + <div class="z-picker-column-wrapper" :class="{ dragging: isDragging }" ref="wrapper" :style="{ height: visibleContentHeight + 'px' }"> + <div + v-for="item in currentValues" + class="z-picker-column__item" + :class="{ 'z-picker-column__item--selected': item === currentValue }" + :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }"> + {{item}} </div> </div> </div> </template> <script> +import translateUtil from 'src/utils/transition'; +import draggable from './draggable'; + const DEFAULT_ITEM_HEIGHT = 44; export default { @@ -40,7 +47,7 @@ export default { return { currentValue: this.value, currentValues: this.values, - dragging: false + isDragging: false }; }, @@ -50,11 +57,16 @@ export default { }, currentValues(val) { - + if (this.valueIndex === -1) { + this.currentValue = (val || [])[0]; + } }, currentValue(val) { + this.doOnValueChange(); + this.$emit('change', this); + this.$emit('input', val); } }, @@ -64,11 +76,128 @@ export default { */ visibleContentHeight() { return this.itemHeight * this.visibileColumnCount; + }, + + valueIndex() { + return this.currentValues.indexOf(this.currentValue); + }, + + dragRange() { + var values = this.currentValues; + var visibileColumnCount = this.visibileColumnCount; + var itemHeight = this.itemHeight; + + return [ -itemHeight * (values.length - Math.ceil(visibileColumnCount / 2)), itemHeight * Math.floor(visibileColumnCount / 2) ]; } }, - methods: { + mounted() { + this.ready = true; + this.$emit('input', this.currentValue); + this.initEvents(); + this.doOnValueChange(); + }, + + methods: { + value2Translate(value) { + let values = this.currentValues; + let valueIndex = values.indexOf(value); + let offset = Math.floor(this.visibileColumnCount / 2); + let itemHeight = this.itemHeight; + + if (valueIndex !== -1) { + return (valueIndex - offset) * -itemHeight; + } + }, + + translate2Value(translate) { + let itemHeight = this.itemHeight; + translate = Math.round(translate / itemHeight) * itemHeight; + + let index = -(translate - Math.floor(this.visibileColumnCount / 2) * itemHeight) / itemHeight; + + return this.currentValues[index]; + }, + + initEvents() { + var el = this.$refs.wrapper; + var dragState = {}; + + var velocityTranslate, prevTranslate, pickerItems; + + draggable(el, { + start: (event) => { + dragState = { + range: this.dragRange, + start: new Date(), + startLeft: event.pageX, + startTop: event.pageY, + startTranslateTop: translateUtil.getElementTranslate(el).top + }; + pickerItems = el.querySelectorAll('.z-picker-item'); + }, + + drag: (event) => { + this.isDragging = true; + + dragState.left = event.pageX; + dragState.top = event.pageY; + + let deltaY = dragState.top - dragState.startTop; + let translate = dragState.startTranslateTop + deltaY; + + translateUtil.translateElement(el, null, translate); + + velocityTranslate = translate - prevTranslate || translate; + + prevTranslate = translate; + }, + + end: () => { + if (this.isDragging) { + this.isDragging = false; + + var momentumRatio = 7; + var currentTranslate = translateUtil.getElementTranslate(el).top; + var duration = new Date() - dragState.start; + + var momentumTranslate; + if (duration < 300) { + momentumTranslate = currentTranslate + velocityTranslate * momentumRatio; + } + + var dragRange = dragState.range; + + this.$nextTick(() => { + var translate; + var itemHeight = this.itemHeight; + + if (momentumTranslate) { + translate = Math.round(momentumTranslate / itemHeight) * itemHeight; + } else { + translate = Math.round(currentTranslate / itemHeight) * itemHeight; + } + + translate = Math.max(Math.min(translate, dragRange[1]), dragRange[0]); + + translateUtil.translateElement(el, null, translate); + + this.currentValue = this.translate2Value(translate); + }); + } + + dragState = {}; + } + }); + }, + + doOnValueChange() { + let value = this.currentValue; + let wrapper = this.$refs.wrapper; + + translateUtil.translateElement(wrapper, null, this.value2Translate(value)); + } } }; </script> diff --git a/packages/picker/src/picker.vue b/packages/picker/src/picker.vue index 513e33c8b..90e2d6d7a 100644 --- a/packages/picker/src/picker.vue +++ b/packages/picker/src/picker.vue @@ -1,10 +1,10 @@ <template> <div class="z-picker"> - <div class="z-picker-toolbar"> + <div class="z-picker__toolbar"> <slot> </slot> </div> - <div class="z-picker-columns"> + <div class="z-picker__columns" :class="['z-picker__columns--' + columns.length]"> <picker-column v-for="(item, index) in columns" v-model="values[index]" @@ -69,7 +69,7 @@ export default { let values = []; columns.forEach(column => { - values.push(column.value || column[column.defaultIndex || 0]); + values.push(column.value || column.values[column.defaultIndex || 0]); }); return values; diff --git a/packages/zanui-css/src/index.pcss b/packages/zanui-css/src/index.pcss index 8a849223c..afe503ef7 100644 --- a/packages/zanui-css/src/index.pcss +++ b/packages/zanui-css/src/index.pcss @@ -8,4 +8,5 @@ @import './field.pcss'; @import './icon.pcss'; @import './popup.pcss'; +@import './picker.pcss'; @import './switch.pcss'; diff --git a/packages/zanui-css/src/picker.pcss b/packages/zanui-css/src/picker.pcss new file mode 100644 index 000000000..59947341d --- /dev/null +++ b/packages/zanui-css/src/picker.pcss @@ -0,0 +1,73 @@ +@component-namespace z { + @b picker { + overflow: hidden; + + @e toolbar { + height: 40px; + } + + @e columns { + position: relative; + overflow: hidden; + + @m 1 { + .z-picker-column { + width: 100%; + } + } + + @m 2 { + .z-picker-column { + width: 50%; + } + } + + @m 2 { + .z-picker-column { + width: 33.333%; + } + } + } + } + + @b picker-column { + font-size: 18px; + overflow: hidden; + position: relative; + max-height: 100%; + text-align: center; + + @e item { + height: 44px; + line-height: 44px; + padding: 0 10px; + white-space: nowrap; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + color: #707274; + left: 0; + top: 0; + width: 100%; + box-sizing: border-box; + transition-duration: .3s; + backface-visibility: hidden; + + @m selected { + color: #000; + transform: translate3d(0, 0, 0) rotateX(0); + } + } + } + + .picker-column-wrapper { + transition-duration: 0.3s; + transition-timing-function: ease-out; + backface-visibility: hidden; + } + + .picker-column-wrapper.dragging, + .picker-column-wrapper.dragging .picker-item { + transition-duration: 0s; + } +} diff --git a/src/utils/transition.js b/src/utils/transition.js new file mode 100644 index 000000000..215dc0d4f --- /dev/null +++ b/src/utils/transition.js @@ -0,0 +1,100 @@ +import Vue from 'vue'; + +var exportObj = {}; + +if (!Vue.prototype.$isServer) { + var docStyle = document.documentElement.style; + var engine; + var translate3d = false; + + if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') { + engine = 'presto'; + } else if ('MozAppearance' in docStyle) { + engine = 'gecko'; + } else if ('WebkitAppearance' in docStyle) { + engine = 'webkit'; + } else if (typeof navigator.cpuClass === 'string') { + engine = 'trident'; + } + + var cssPrefix = {trident: '-ms-', gecko: '-moz-', webkit: '-webkit-', presto: '-o-'}[engine]; + + var vendorPrefix = {trident: 'ms', gecko: 'Moz', webkit: 'Webkit', presto: 'O'}[engine]; + + var helperElem = document.createElement('div'); + var perspectiveProperty = vendorPrefix + 'Perspective'; + var transformProperty = vendorPrefix + 'Transform'; + var transformStyleName = cssPrefix + 'transform'; + var transitionProperty = vendorPrefix + 'Transition'; + var transitionStyleName = cssPrefix + 'transition'; + var transitionEndProperty = vendorPrefix.toLowerCase() + 'TransitionEnd'; + + if (helperElem.style[perspectiveProperty] !== undefined) { + translate3d = true; + } + + var getTranslate = function(element) { + var result = {left: 0, top: 0}; + if (element === null || element.style === null) return result; + + var transform = element.style[transformProperty]; + var matches = /translate\(\s*(-?\d+(\.?\d+?)?)px,\s*(-?\d+(\.\d+)?)px\)\s*translateZ\(0px\)/ig.exec(transform); + if (matches) { + result.left = +matches[1]; + result.top = +matches[3]; + } + + return result; + }; + + var translateElement = function(element, x, y) { + if (x === null && y === null) return; + + if (element === null || element === undefined || element.style === null) return; + + if (!element.style[transformProperty] && x === 0 && y === 0) return; + + if (x === null || y === null) { + var translate = getTranslate(element); + if (x === null) { + x = translate.left; + } + if (y === null) { + y = translate.top; + } + } + + cancelTranslateElement(element); + + if (translate3d) { + element.style[transformProperty] += ' translate(' + (x ? (x + 'px') : '0px') + ',' + (y ? (y + 'px') : '0px') + ') translateZ(0px)'; + } else { + element.style[transformProperty] += ' translate(' + (x ? (x + 'px') : '0px') + ',' + (y ? (y + 'px') : '0px') + ')'; + } + }; + + var cancelTranslateElement = function(element) { + if (element === null || element.style === null) return; + + var transformValue = element.style[transformProperty]; + + if (transformValue) { + transformValue = transformValue.replace(/translate\(\s*(-?\d+(\.?\d+?)?)px,\s*(-?\d+(\.\d+)?)px\)\s*translateZ\(0px\)/g, ''); + element.style[transformProperty] = transformValue; + } + }; + + exportObj = { + transformProperty: transformProperty, + transformStyleName: transformStyleName, + transitionProperty: transitionProperty, + transitionStyleName: transitionStyleName, + transitionEndProperty: transitionEndProperty, + getElementTranslate: getTranslate, + translateElement: translateElement, + cancelTranslateElement: cancelTranslateElement + }; +}; + +export default exportObj; +