import "./picker.scss" function debounce(handle, delay) { let timeout = null return function () { if (timeout) { clearTimeout(timeout) timeout = null } const self = this const args = arguments timeout = setTimeout(() => handle.apply(self, args), delay) } } function getClientCenterY(elem) { const { top, bottom } = elem.getBoundingClientRect() return (top + bottom) / 2 } function normalizeOptions(options) { return options.map((option) => { switch (typeof option) { case 'string': { return { value: option, name: option } } case 'number': case 'boolean': { return { value: option, name: `${option}` } } } return option }) } function isTouchEvent(event) { return event.changedTouches || event.touches } function getEventXY(event) { if (isTouchEvent(event)) { return event.changedTouches[0] || event.touches[0] } return event } export default { props: { value: null, options: { type: Array, default: () => [], }, dragSensitivity: { type: Number, default: 1.7, }, touchSensitivity: { type: Number, default: 1.7, }, scrollSensitivity: { type: Number, default: 1, }, empty: { type: String, default: 'No Items', }, placeholder: { type: String, default: null, }, }, data() { const normalizedOptions = normalizeOptions(this.options) let innerIndex = normalizedOptions.findIndex(option => option.value == this.value) if (innerIndex === -1 && !this.placeholder && this.options.length > 0) { innerIndex = 0 } const innerValue = normalizedOptions[innerIndex] && normalizedOptions[innerIndex].value || null return { normalizedOptions, innerIndex, innerValue, top: null, pivots: [], pivotMin: 0, pivotMax: 0, transitioning: false, transitionTO: null, start: null, isMouseDown: false, isDragging: false, scrollOffsetTop: 0, scrollMin: 0, scrollMax: 0, } }, mounted() { this.calculatePivots() this.top = this.findScrollByIndex(this.innerIndex) if (this.innerValue !== this.value) { this.$emit('input', this.innerValue) } this.$el.addEventListener('touchstart', this.onStart) this.$el.addEventListener('touchmove', this.onMove) this.$el.addEventListener('touchend', this.onEnd) this.$el.addEventListener('touchcancel', this.onCancel) this.$el.addEventListener('mousewheel', this.onScroll) this.$el.addEventListener('DOMMouseScroll', this.onScroll) this.$el.addEventListener('wheel', this.onScroll) this.$el.addEventListener('mousedown', this.onStart) this.$el.addEventListener('mousemove', this.onMove) this.$el.addEventListener('mouseup', this.onEnd) this.$el.addEventListener('mouseleave', this.onCancel) }, destroyed() { this.$el.removeEventListener('touchstart', this.onStart) this.$el.removeEventListener('touchmove', this.onMove) this.$el.removeEventListener('touchend', this.onEnd) this.$el.removeEventListener('touchcancel', this.onCancel) this.$el.removeEventListener('mousewheel', this.onScroll) this.$el.removeEventListener('DOMMouseScroll', this.onScroll) this.$el.removeEventListener('wheel', this.onScroll) this.$el.removeEventListener('mousedown', this.onStart) this.$el.removeEventListener('mousemove', this.onMove) this.$el.removeEventListener('mouseup', this.onEnd) this.$el.removeEventListener('mouseleave', this.onCancel) }, watch: { value(value) { if ((value === null || value === undefined) && this.placeholder) { this.correction(-1) return } const nextInnerIndex = this.normalizedOptions.findIndex((option) => option.value == value) if (nextInnerIndex === -1) { this.$emit('input', this.innerValue) return } if (this.innerIndex !== nextInnerIndex) { this.correction(nextInnerIndex) } }, options(options) { const normalizedOptions = this.normalizedOptions = normalizeOptions(options) let internalIndex = normalizedOptions.findIndex(option => option.value == this.value) if (internalIndex === -1 && !this.placeholder && this.options.length > 0) { internalIndex = 0 } const innerValue = normalizedOptions[internalIndex] && normalizedOptions[internalIndex].value || null this.$nextTick(() => { this.calculatePivots() this.top = this.findScrollByIndex(internalIndex) this.innerIndex = internalIndex if (this.innerValue !== innerValue) { this.$emit('input', this.innerValue = innerValue) } }) } }, methods: { resize() { this.$nextTick(() => { this.calculatePivots() this.top = this.findScrollByIndex(this.innerIndex) }) }, calculatePivots() { const rotatorTop = this.$refs.list.getBoundingClientRect().top this.pivots = (this.$refs.items || []).map((item) => getClientCenterY(item) - rotatorTop).sort((a, b) => a - b) this.pivotMin = Math.min(...this.pivots) this.pivotMax = Math.max(...this.pivots) this.scrollOffsetTop = this.$refs.selection.offsetTop + this.$refs.selection.offsetHeight / 2 this.scrollMin = this.scrollOffsetTop - this.pivotMin this.scrollMax = this.scrollOffsetTop - this.pivotMax }, sanitizeInternalIndex(index) { return Math.min(Math.max(index, this.placeholder ? -1 : 0), this.normalizedOptions.length - 1) }, findIndexFromScroll(scroll) { let prevDiff = null let pivotIndex = 0 this.pivots.forEach((pivot, i) => { const diff = pivot + scroll - this.scrollOffsetTop if (prevDiff === null || Math.abs(prevDiff) > Math.abs(diff)) { pivotIndex = i prevDiff = diff } }) if (this.placeholder || this.options.length === 0) { return pivotIndex - 1 } return pivotIndex }, findScrollByIndex(index) { let pivotIndex = index if (this.placeholder || this.options.length === 0) { pivotIndex++ } if (index > -1 && pivotIndex in this.pivots) { return this.scrollOffsetTop - this.pivots[pivotIndex] } if (index >= this.pivots.length) { return this.scrollOffsetTop - this.pivotMax } return this.scrollOffsetTop - this.pivotMin }, onScroll(e) { if (this.top >= this.scrollMin && e.deltaY < 0) return if (this.top <= this.scrollMax && e.deltaY > 0) return if (this.pivots.length === 1) return e.preventDefault() const nextDirInnerIndex = this.sanitizeInternalIndex(this.innerIndex + (e.deltaY > 0 ? 1 : -1)) const deltaMax = e.deltaY > 0 ? this.findScrollByIndex(nextDirInnerIndex - 1) - this.findScrollByIndex(nextDirInnerIndex) : this.findScrollByIndex(nextDirInnerIndex) - this.findScrollByIndex(nextDirInnerIndex + 1) const deltaY = Math.max(Math.min(e.deltaY, deltaMax), deltaMax * -1) this.top = Math.min(Math.max(this.top - deltaY * this.scrollSensitivity, this.scrollMax), this.scrollMin) const nextInnerIndex = this.sanitizeInternalIndex(this.findIndexFromScroll(this.top)) const nextInnerValue = this.normalizedOptions[nextInnerIndex] && this.normalizedOptions[nextInnerIndex].value || null this.innerIndex = nextInnerIndex if (this.innerValue !== nextInnerValue) { this.$emit('input', this.innerValue = nextInnerValue) } this.onAfterWheel() }, onAfterWheel: debounce(function () { this.correction(this.findIndexFromScroll(this.top)) }, 200), onStart(event) { if (event.cancelable) { event.preventDefault() } const { clientY } = getEventXY(event) this.start = [this.top, clientY] if (!isTouchEvent(event)) { this.isMouseDown = true } this.isDragging = false }, onMove(e) { if (e.cancelable) { e.preventDefault() } if (!this.start) { return } const { clientY } = getEventXY(e) const diff = clientY - this.start[1] if (Math.abs(diff) > 1.5) { this.isDragging = true } this.top = this.start[0] + diff * (isTouchEvent(e) ? this.touchSensitivity : this.dragSensitivity) }, onEnd(e) { if (e.cancelable) { e.preventDefault() } if (this.isDragging) { this.correction(this.findIndexFromScroll(this.top)) } else { this.handleClick(e) } this.start = null this.isDragging = false this.isMouseDown = false }, onCancel(e) { if (e.cancelable) { e.preventDefault() } this.correction(this.findIndexFromScroll(this.top)) this.start = null this.isMouseDown = false this.isDragging = false }, handleClick(e) { const touchInfo = getEventXY(e) const x = touchInfo.clientX const y = touchInfo.clientY const topRect = this.$refs.top.getBoundingClientRect() const bottomRect = this.$refs.bottom.getBoundingClientRect() if (topRect.left <= x && x <= topRect.right && topRect.top <= y && y <= topRect.bottom) { this.correction(this.innerIndex - 1) } else if (bottomRect.left <= x && x <= bottomRect.right && bottomRect.top <= y && y <= bottomRect.bottom) { this.correction(this.innerIndex + 1) } }, correction(index) { const nextInnerIndex = this.sanitizeInternalIndex(index) const nextInnerValue = this.normalizedOptions[nextInnerIndex] && this.normalizedOptions[nextInnerIndex].value || null this.top = this.findScrollByIndex(nextInnerIndex) this.transitioning = true if (this.transitionTO) { clearTimeout(this.transitionTO) this.transitionTO = null } this.transitionTO = setTimeout(() => { this.transitioning = false this.transitionTO = null this.innerIndex = nextInnerIndex if (this.innerValue !== nextInnerValue) { this.innerValue = nextInnerValue this.$emit('input', this.innerValue) } }, 100) }, }, render(h) { let items = [] if (this.placeholder) { items.push(h("div", { class: { "vue-scroll-picker-item": true, "-placeholder": true, "-selected": this.innerIndex == -1, }, ref: "items", refInFor: true, domProps: { innerHTML: this.placeholder, }, })) } else if (this.normalizedOptions.length === 0 && this.placeholder === null) { items.push(h("div", { class: ["vue-scroll-picker-item", "-empty", "-selected"], ref: "items", refInFor: true, domProps: { innerHTML: this.empty, }, })) } items = items.concat(this.normalizedOptions.map((option, index) => { return h("div", { class: { "vue-scroll-picker-item": true, "-selected": this.innerIndex == index, }, key: option.value, ref: "items", refInFor: true, domProps: { innerHTML: option.name, }, }) })) return h("div", {class: ["vue-scroll-picker"]}, [ h("div", {class: ["vue-scroll-picker-list"]}, [ h("div", { ref: 'list', class: { "vue-scroll-picker-list-rotator": true, "-transition": this.transitioning, }, style: this.top !== null ? { top: `${this.top}px` } : {}, }, items) ]), h("div", {class: ["vue-scroll-picker-layer"]}, [ h("div", {class: ["top"], ref: "top"}), h("div", {class: ["middle"], ref: "selection"}), h("div", {class: ["bottom"], ref: "bottom"}), ]), ]) } }