import { deepClone } from '../utils/deep-clone'; import { createNamespace, isObj } from '../utils'; import { range } from '../utils/format/number'; import { preventDefault } from '../utils/dom/event'; import { TouchMixin } from '../mixins/touch'; const DEFAULT_DURATION = 200; // 惯性滑动思路: // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move // 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动,持续 `MOMENTUM_DURATION` const MOMENTUM_DURATION = 1000; const MOMENTUM_LIMIT_TIME = 300; const MOMENTUM_LIMIT_DISTANCE = 15; const [createComponent, bem] = createNamespace('picker-column'); function getElementTranslateY(element) { const { transform } = window.getComputedStyle(element); const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; return Number(translateY); } function isOptionDisabled(option) { return isObj(option) && option.disabled; } export default createComponent({ mixins: [TouchMixin], props: { valueKey: String, className: String, itemHeight: Number, defaultIndex: Number, initialOptions: Array, visibleItemCount: Number }, data() { return { offset: 0, duration: 0, options: deepClone(this.initialOptions), currentIndex: this.defaultIndex }; }, created() { if (this.$parent.children) { this.$parent.children.push(this); } this.setIndex(this.currentIndex); }, destroyed() { const { children } = this.$parent; if (children) { children.splice(children.indexOf(this), 1); } }, watch: { defaultIndex() { this.setIndex(this.defaultIndex); } }, computed: { count() { return this.options.length; } }, methods: { onTouchStart(event) { this.touchStart(event); if (this.moving) { const translateY = getElementTranslateY(this.$refs.wrapper); this.startOffset = Math.min(0, translateY); } else { this.startOffset = this.offset; } this.duration = 0; this.moving = false; this.transitionEndTrigger = null; this.touchStartTime = Date.now(); this.momentumOffset = this.startOffset; }, onTouchMove(event) { preventDefault(event); this.moving = true; this.touchMove(event); this.offset = range( this.startOffset + this.deltaY, -(this.count * this.itemHeight), this.itemHeight ); const now = Date.now(); if (now - this.touchStartTime > MOMENTUM_LIMIT_TIME) { this.touchStartTime = now; this.momentumOffset = this.offset; } }, onTouchEnd() { const distance = this.offset - this.momentumOffset; const duration = Date.now() - this.touchStartTime; const allowMomentum = duration < MOMENTUM_LIMIT_TIME && Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE; if (allowMomentum) { this.momentum(distance, duration); return; } if (this.offset !== this.startOffset) { this.duration = DEFAULT_DURATION; const index = this.getIndexByOffset(this.offset); this.setIndex(index, true); } }, onTransitionEnd() { this.stopMomentum(); }, onClickItem(index) { this.duration = DEFAULT_DURATION; this.setIndex(index, true); }, adjustIndex(index) { index = range(index, 0, this.count); for (let i = index; i < this.count; i++) { if (!isOptionDisabled(this.options[i])) return i; } for (let i = index - 1; i >= 0; i--) { if (!isOptionDisabled(this.options[i])) return i; } }, getOptionText(option) { return isObj(option) && this.valueKey in option ? option[this.valueKey] : option; }, setIndex(index, userAction) { index = this.adjustIndex(index) || 0; this.offset = -index * this.itemHeight; const trigger = () => { if (index !== this.currentIndex) { this.currentIndex = index; if (userAction) { this.$emit('change', index); } } }; // 若有触发过 `touchmove` 事件,那应该 // 在 `transitionend` 后再触发 `change` 事件 if (this.moving) { this.transitionEndTrigger = trigger; } else { trigger(); } }, setValue(value) { const { options } = this; for (let i = 0; i < options.length; i++) { if (this.getOptionText(options[i]) === value) { return this.setIndex(i); } } }, getValue() { return this.options[this.currentIndex]; }, getIndexByOffset(offset) { return range( Math.round(-offset / this.itemHeight), 0, this.count - 1 ); }, momentum(distance, duration) { const speed = Math.abs(distance / duration); distance = this.offset + speed / 0.002 * (distance < 0 ? -1 : 1); const index = this.getIndexByOffset(distance); this.duration = MOMENTUM_DURATION; this.setIndex(index, true); }, stopMomentum() { this.moving = false; this.duration = 0; if (this.transitionEndTrigger) { this.transitionEndTrigger(); this.transitionEndTrigger = null; } } }, render(h) { const { itemHeight, visibleItemCount } = this; const baseOffset = (itemHeight * (visibleItemCount - 1)) / 2; const wrapperStyle = { transform: `translate3d(0, ${this.offset + baseOffset}px, 0)`, transitionDuration: `${this.duration}ms`, transitionProperty: this.duration ? 'transform' : 'none', lineHeight: `${itemHeight}px` }; const optionStyle = { height: `${itemHeight}px` }; return (