wux-weapp
Version: 
一套组件化、可复用、易扩展的微信小程序 UI 组件库
384 lines (363 loc) • 12.8 kB
JavaScript
import baseComponent from '../helpers/baseComponent'
import classNames from '../helpers/libs/classNames'
import shallowEqual from '../helpers/libs/shallowEqual'
import styleToCssString from '../helpers/libs/styleToCssString'
import fieldNamesBehavior from '../helpers/mixins/fieldNamesBehavior'
import { getTouchPoints, getPointsNumber } from '../helpers/shared/gestures'
import { vibrateShort } from '../helpers/hooks/useNativeAPI'
import {  props } from './props'
import {
    getRealCol,
    getRealValue,
    getIndexFromValue,
    getLabelFromIndex,
} from './utils'
function getStyles(value) {
    return Array.isArray(value) ? value.map((n) => styleToCssString(n)) : styleToCssString(value)
}
baseComponent({
    behaviors: [fieldNamesBehavior],
    properties: props,
    data: {
        inputValue: null,
        selectedIndex: null,
        selectedValue: null,
        cols: [],
        extIndicatorStyle: '',
        extItemStyle: '',
        extMaskStyle: '',
        contentStyle: '',
        itemCount: 7, // 默认显示的子元素个数
        styles: {},
    },
    computed: {
        classes: ['prefixCls, labelAlign', function(prefixCls, labelAlign) {
            const wrap = classNames(prefixCls, {
                [`${prefixCls}--${labelAlign}`]: labelAlign,
            })
            const mask = `${prefixCls}__mask`
            const indicator = `${prefixCls}__indicator`
            const content = `${prefixCls}__content`
            const item = `${prefixCls}__item`
            const image = `${prefixCls}__image`
            return {
                wrap,
                mask,
                indicator,
                content,
                item,
                image,
            }
        }],
    },
    observers: {
        itemHeight(newVal) {
            this.updatedStyles(newVal)
        },
        itemStyle(newVal) {
            this.setData({
                extItemStyle: getStyles(newVal),
            })
        },
        indicatorStyle(newVal) {
            this.setData({
                extIndicatorStyle: getStyles(newVal),
            })
        },
        maskStyle(newVal) {
            this.setData({
                extMaskStyle: getStyles(newVal),
            })
        },
        ['value, options'](value, options) {
            const { controlled } = this.data
            const fieldNames = this.getFieldNames()
            const cols = getRealCol(options, fieldNames)
            if (!shallowEqual(this.data.cols, cols)) {
                this.setData({ cols })
            }
            if (controlled) {
                this.setValue(value, true)
            }
        },
        inputValue(newVal) {
            const {
                selectedIndex,
                selectedValue,
            } = this.getValue(newVal)
            this.setData({
                selectedIndex,
                selectedValue,
            })
        },
    },
    methods: {
        updatedStyles(itemHeight) {
            let num = this.data.itemCount
            if (num % 2 === 0) {
                num--
            }
            num--
            num /= 2
            const wrap = `height: ${itemHeight * this.data.itemCount}px;`
            const item = `line-height: ${itemHeight}px; height: ${itemHeight}px;`
            const image = `width: ${itemHeight * .6}px; height: ${itemHeight * .6}px;`
            const content = `padding: ${itemHeight * num}px 0;`
            const indicator = `top: ${itemHeight * num}px; height: ${itemHeight}px;`
            const mask = `background-size: 100% ${itemHeight * num}px;`
            const styles = {
                wrap,
                item,
                image,
                content,
                indicator,
                mask,
            }
            this.setData({ styles })
        },
        updated(inputValue, isForce) {
            if (this.data.inputValue !== inputValue || isForce) {
                this.setData({
                    inputValue,
                })
            }
            // 设置选择器位置
            if (isForce) {
                this.select(inputValue, this.data.itemHeight, (y) => this.scrollTo(y, 0, false))
            }
        },
        setValue(value, isForce) {
            const { value: inputValue } = this.getValue(value)
            this.updated(inputValue, isForce)
        },
        getValue(value = this.data.inputValue, cols = this.data.cols) {
            const fieldNames = this.getFieldNames()
            const inputValue = getRealValue(value, cols, fieldNames) || null
            const selectedValue = inputValue
            const selectedIndex = getIndexFromValue(value, cols, fieldNames)
            const displayValue = getLabelFromIndex(selectedIndex, cols, fieldNames.label)
            return {
                value: inputValue,
                displayValue,
                selectedIndex,
                selectedValue,
                cols,
            }
        },
        /**
         * 设置选择器的位置信息
         */
        scrollTo(y, time = .3, runCallbacks = true) {
            if (this.scrollY !== y) {
                if (this.runCallbacks) {
                    clearTimeout(this.runCallbacks)
                    this.runCallbacks = null
                }
                this.scrollY = y
                this.setTransform(-y, time, () => {
                    runCallbacks && (this.runCallbacks = setTimeout(() => {
                        this.setTransform(-y, 0, this.scrollingComplete)
                    }, +time * 1000))
                })
            }
        },
        /**
         * 滚动结束时的回调函数
         */
        onFinish() {
            this.isMoving = false
            let targetY = this.scrollY
            const { cols, itemHeight } = this.data
            const height = (cols.length - 1) * itemHeight
            let time = .3
            // const velocity = this.Velocity.getVelocity(targetY) * 4
            // if (velocity) {
            //     targetY = velocity * 40 + targetY
            //     time = Math.abs(velocity) * .1
            //     time = parseFloat(time.toFixed(2))
            // }
            if (targetY % itemHeight !== 0) {
                targetY = Math.round(targetY / itemHeight) * itemHeight
            }
            if (targetY < 0) {
                targetY = 0
            } else if (targetY > height) {
                targetY = height
            }
            // check disabled & reset
            const child = this.getChildMeta(targetY, itemHeight)
            const disabledName = this.getFieldName('disabled')
            if (child && !child[disabledName]) {
                this.scrollTo(targetY, time < .3 ? .3 : time)
            } else {
                this.select(this.data.inputValue, itemHeight, (y) => this.scrollTo(y, 0, false))
            }
            this.onScrollChange()
        },
        /**
         * 手指触摸动作开始
         */
        onTouchStart(e) {
            if (getPointsNumber(e) > 1) return
            this.isMoving = true
            this.startY = getTouchPoints(e).y
            this.lastY = this.scrollY
            this.triggerEvent('beforeChange', this.getValue())
        },
        /**
         * 手指触摸后移动
         */
        onTouchMove(e) {
            if (!this.isMoving || getPointsNumber(e) > 1) return
            this.scrollY = this.lastY - getTouchPoints(e).y + this.startY
            this.setTransform(-this.scrollY, false, this.onScrollChange)
            // this.Velocity.record(this.scrollY)
        },
        /**
         * 手指触摸动作结束
         */
        onTouchEnd(e) {
            if (getPointsNumber(e) > 1) return
            this.onFinish()
        },
        /**
         * 手指触摸后马上离开
         */
        onItemClick(e) {
            const { index, disabled } = e.currentTarget.dataset
            if (!disabled) {
                this.scrollTo(index * this.data.itemHeight)
            }
        },
        /**
         * 设置滚动样式
         */
        setTransform(y, time, cb) {
            const contentStyle = {
                transform: `translate3d(0,${y}px,0)`,
                transition: time ? `cubic-bezier(0, 0, 0.2, 1.15) ${time}s` : 'none',
            }
            this.setData({ contentStyle: styleToCssString(contentStyle) }, cb)
        },
        /**
         * 设置选择器
         */
        select(value, itemHeight, scrollTo) {
            const fieldNames = this.getFieldNames()
            const { cols: children } = this.data
            const index = getIndexFromValue(value, children, fieldNames)
            this.selectByIndex(index, itemHeight, scrollTo)
        },
        /**
         * 通过元素的索引值设置选择器
         */
        selectByIndex(index, itemHeight, zscrollTo) {
            if (index < 0 || index >= this.data.cols.length || !itemHeight) return
            zscrollTo.call(this, index * itemHeight)
        },
        /**
         * 计算子元素的索引值
         */
        computeChildIndex(top, itemHeight, childrenLength) {
            const index = Math.round(top / itemHeight)
            return Math.min(index, childrenLength - 1)
        },
        /**
         * 获取子元素的属性
         */
        getChildMeta(top, itemHeight) {
            const { cols: children } = this.data
            const index = this.computeChildIndex(top, itemHeight, children.length)
            const child = children[index]
            return child
        },
        /**
         * 滚动完成的回调函数
         */
        scrollingComplete() {
            const top = this.scrollY
            if (top >= 0) {
                const valueName = this.getFieldName('value')
                const { itemHeight } = this.data
                const child = this.getChildMeta(top, itemHeight)
                if (child) {
                    const inputValue = child[valueName]
                    if (this.data.inputValue !== inputValue) {
                        this.fireValueChange(inputValue)
                    }
                }
            }
        },
        /**
         * 滚动数据选择变化后的回调函数
         */
        onScrollChange() {
            const top = this.scrollY
            if (top >= 0) {
                const valueName = this.getFieldName('value')
                const { cols: children, itemHeight } = this.data
                const index = this.computeChildIndex(top, itemHeight, children.length)
                if (this.scrollValue !== index) {
                    this.scrollValue = index
                    const child = children[index]
                    if (child) {
                        const values = this.getValue(child[valueName])
                        this.triggerEvent('scrollChange', values)
                    }
                    // 振动反馈
                    vibrateShort()
                }
            }
        },
        /**
         * 数据选择变化后的回调函数
         */
        fireValueChange(value) {
            if (!this.data.controlled) {
                this.updated(value)
            }
            this.triggerEvent('valueChange', this.getValue(value))
            // 振动反馈
            vibrateShort()
        },
    },
    created() {
        this.scrollValue = undefined
        this.scrollY = -1
        this.lastY = 0
        this.startY = 0
        this.isMoving = false
        // this.Velocity = ((minInterval = 30, maxInterval = 100) => {
        //     let _time = 0
        //     let _y = 0
        //     let _velocity = 0
        //     const recorder = {
        //         record: (y) => {
        //             const now = +new Date()
        //             _velocity = (y - _y) / (now - _time)
        //             if (now - _time >= minInterval) {
        //                 _velocity = now - _time <= maxInterval ? _velocity : 0
        //                 _y = y
        //                 _time = now
        //             }
        //         },
        //         getVelocity: (y) => {
        //             if (y !== _y) {
        //                 recorder.record(y)
        //             }
        //             return _velocity
        //         },
        //     }
        //     return recorder
        // })()
    },
    attached() {
        const { defaultValue, value, controlled, options, itemHeight } = this.data
        const inputValue = controlled ? value : defaultValue
        const fieldNames = this.getFieldNames()
        const cols = getRealCol(options, fieldNames)
        
        this.updatedStyles(itemHeight)
        this.setData({ cols })
        this.setValue(inputValue, true)
    },
})