|
|
@@ -1,259 +1,231 @@
|
|
|
<template>
|
|
|
- <view
|
|
|
- class="nut-picker__content"
|
|
|
- :style="{ height: height + 'px' }"
|
|
|
- @touchstart="onTouchStart"
|
|
|
- @touchmove="onTouchMove"
|
|
|
- @touchend="onTouchEnd"
|
|
|
- @touchcancel="onTouchEnd"
|
|
|
- @transitionend="stopMomentum"
|
|
|
- >
|
|
|
- <view class="nut-picker__wrapper" ref="wrapper" :style="wrapperStyle">
|
|
|
+ <view class="nut-picker__list" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
|
|
|
+ <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle">
|
|
|
<view
|
|
|
- class="nut-picker__item"
|
|
|
- :key="index"
|
|
|
- v-for="(item, index) in options"
|
|
|
- >{{ dataType === 'cascade' ? item.text : item }}</view
|
|
|
+ class="nut-picker-roller-item"
|
|
|
+ :class="{ 'nut-picker-roller-item-hidden': isHidden(index + 1) }"
|
|
|
+ v-for="(item, index) in listData.values"
|
|
|
+ :style="setRollerStyle(index + 1)"
|
|
|
+ :key="item.label ? item.label : index"
|
|
|
>
|
|
|
+ {{ dataType === 'cascade' ? item.text : item }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="nut-picker-content">
|
|
|
+ <view class="nut-picker-list-panel" ref="list" :style="touchListStyle">
|
|
|
+ <view
|
|
|
+ class="nut-picker-item nut-picker-item-ref"
|
|
|
+ v-for="(item, index) in listData.values"
|
|
|
+ :key="item.label ? item.label : index"
|
|
|
+ >{{ dataType === 'cascade' ? item.text : item }}
|
|
|
+ </view>
|
|
|
+ <view class="nut-picker-placeholder" v-if="listData && listData.length === 1"></view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
<script lang="ts">
|
|
|
import { reactive, ref, watch, computed, toRefs, onMounted } from 'vue';
|
|
|
import { createComponent } from '../../utils/create';
|
|
|
-import { useTouch } from '../../utils/useTouch';
|
|
|
import { commonProps } from './commonProps';
|
|
|
-import {
|
|
|
- PickerObjOpt,
|
|
|
- PickerOption,
|
|
|
- PickerObjectColumn,
|
|
|
- PickerObjectColumns
|
|
|
-} from './types';
|
|
|
-const MOMENTUM_LIMIT_DISTANCE = 15;
|
|
|
-const MOMENTUM_LIMIT_TIME = 300;
|
|
|
-const DEFAULT_DURATION = 200;
|
|
|
+import { TouchParams } from './types';
|
|
|
const { create } = createComponent('picker-column');
|
|
|
-function range(num: number, min: number, max: number): number {
|
|
|
- return Math.min(Math.max(num, min), max);
|
|
|
-}
|
|
|
-function stopPropagation(event: Event) {
|
|
|
- event.stopPropagation();
|
|
|
-}
|
|
|
-function preventDefault(event: Event, isStopPropagation?: boolean) {
|
|
|
- if (typeof event.cancelable !== 'boolean' || event.cancelable) {
|
|
|
- event.preventDefault();
|
|
|
- }
|
|
|
-
|
|
|
- if (isStopPropagation) {
|
|
|
- stopPropagation(event);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function getElementTranslateY(element: Element) {
|
|
|
- const style = window.getComputedStyle(element);
|
|
|
- const transform = style.transform || style.webkitTransform;
|
|
|
- const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
|
|
|
- return Number(translateY);
|
|
|
-}
|
|
|
-export function isObject(val: unknown): val is Record<any, any> {
|
|
|
- return val !== null && typeof val === 'object';
|
|
|
-}
|
|
|
-
|
|
|
-function isOptionDisabled(option: PickerObjectColumn) {
|
|
|
- return isObject(option) && option.disabled;
|
|
|
-}
|
|
|
|
|
|
export default create({
|
|
|
props: {
|
|
|
dataType: String,
|
|
|
+ itemShow: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
...commonProps
|
|
|
},
|
|
|
|
|
|
emits: ['click', 'change'],
|
|
|
setup(props, { emit }) {
|
|
|
const wrapper = ref();
|
|
|
-
|
|
|
const state = reactive({
|
|
|
- index: props.defaultIndex,
|
|
|
- offset: 0,
|
|
|
- duration: 0,
|
|
|
- options: props.listData as PickerObjectColumn[],
|
|
|
- moving: false,
|
|
|
- startOffset: 0,
|
|
|
- touchStartTime: 0,
|
|
|
- momentumOffset: 0,
|
|
|
- transitionEndTrigger: null as null | Function
|
|
|
+ touchParams: {
|
|
|
+ startY: 0,
|
|
|
+ endY: 0,
|
|
|
+ startTime: 0,
|
|
|
+ endTime: 0,
|
|
|
+ lastY: 0
|
|
|
+ },
|
|
|
+ currIndex: 1,
|
|
|
+ transformY: 0,
|
|
|
+ scrollDistance: 0,
|
|
|
+ lineSpacing: 36,
|
|
|
+ rotation: 20,
|
|
|
+ timer: null
|
|
|
});
|
|
|
|
|
|
- const touch = useTouch();
|
|
|
-
|
|
|
- const wrapperStyle = computed(() => ({
|
|
|
- transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
|
|
|
- transitionDuration: `${state.duration}ms`,
|
|
|
- transitionProperty: state.duration ? 'all' : 'none'
|
|
|
- }));
|
|
|
-
|
|
|
- const handleClick = (event: Event) => {
|
|
|
- emit('click', event);
|
|
|
- };
|
|
|
-
|
|
|
- const getIndexByOffset = (offset: number) => {
|
|
|
- return range(
|
|
|
- Math.round(-offset / +props.itemHeight),
|
|
|
- 0,
|
|
|
- state.options.length - 1
|
|
|
- );
|
|
|
- };
|
|
|
+ const roller = ref(null);
|
|
|
+ const list = ref(null);
|
|
|
+ const listItem = ref(null);
|
|
|
+
|
|
|
+ const touchDeg = ref(0);
|
|
|
+ const touchTime = ref(0);
|
|
|
+ const touchTranslateY = ref(0);
|
|
|
+ const touchListStyle = computed(() => {
|
|
|
+ return {
|
|
|
+ transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
|
|
|
+ transform: `translate3d(0, ${state.scrollDistance}px, 0)`
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
- const baseOffset = () => {
|
|
|
- return (+props.itemHeight * (+props.visibleItemCount - 1)) / 2;
|
|
|
- };
|
|
|
+ const touchRollerStyle = computed(() => {
|
|
|
+ return {
|
|
|
+ transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
|
|
|
+ transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
- const stopMomentum = () => {
|
|
|
- state.moving = false;
|
|
|
- state.duration = 0;
|
|
|
- if (state.transitionEndTrigger) {
|
|
|
- state.transitionEndTrigger();
|
|
|
- state.transitionEndTrigger = null;
|
|
|
- }
|
|
|
+ const onTouchStart = (event: TouchEvent) => {
|
|
|
+ event.preventDefault();
|
|
|
+ let changedTouches = event.changedTouches[0];
|
|
|
+ state.touchParams.startY = changedTouches.pageY;
|
|
|
+ state.touchParams.startTime = event.timeStamp || Date.now();
|
|
|
+ state.transformY = state.scrollDistance;
|
|
|
};
|
|
|
|
|
|
- const adjustIndex = (index: number) => {
|
|
|
- index = range(index, 0, state.options.length);
|
|
|
+ const onTouchMove = (event: TouchEvent) => {
|
|
|
+ event.preventDefault();
|
|
|
+ let changedTouches = event.changedTouches[0];
|
|
|
+ (state.touchParams as TouchParams).lastY = changedTouches.pageY;
|
|
|
+ (state.touchParams as TouchParams).lastTime = event.timeStamp || Date.now();
|
|
|
+ let move = state.touchParams.lastY - state.touchParams.startY;
|
|
|
|
|
|
- for (let i = index; i < state.options.length; i++) {
|
|
|
- if (!isOptionDisabled(state.options[i])) return i;
|
|
|
- }
|
|
|
- for (let i = index - 1; i >= 0; i--) {
|
|
|
- if (!isOptionDisabled(state.options[i])) return i;
|
|
|
- }
|
|
|
+ setMove(move);
|
|
|
};
|
|
|
|
|
|
- const setIndex = (index: number, emitChange = false) => {
|
|
|
- index = adjustIndex(index) || 0;
|
|
|
-
|
|
|
- const offset = -index * +props.itemHeight;
|
|
|
- const trigger = () => {
|
|
|
- if (index !== state.index) {
|
|
|
- state.index = index;
|
|
|
+ const onTouchEnd = (event: TouchEvent) => {
|
|
|
+ event.preventDefault();
|
|
|
|
|
|
- if (emitChange) {
|
|
|
- emit('change', index);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
+ let changedTouches = event.changedTouches[0];
|
|
|
+ state.touchParams.lastY = changedTouches.pageY;
|
|
|
+ state.touchParams.lastTime = event.timestamp || Date.now();
|
|
|
+ let move = state.touchParams.lastY - state.touchParams.startY;
|
|
|
|
|
|
- if (state.moving && offset !== state.offset) {
|
|
|
- state.transitionEndTrigger = trigger;
|
|
|
+ let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
|
|
|
+ if (moveTime <= 300) {
|
|
|
+ move = move * 2;
|
|
|
+ moveTime = moveTime + 1000;
|
|
|
+ setMove(move, 'end', moveTime);
|
|
|
} else {
|
|
|
- trigger();
|
|
|
+ setMove(move, 'end');
|
|
|
}
|
|
|
-
|
|
|
- state.offset = offset;
|
|
|
};
|
|
|
|
|
|
- const momentum = (distance: number, duration: number) => {
|
|
|
- const speed = Math.abs(distance / duration);
|
|
|
-
|
|
|
- distance = state.offset + (speed / 0.03) * (distance < 0 ? -1 : 1);
|
|
|
-
|
|
|
- const index = getIndexByOffset(distance);
|
|
|
-
|
|
|
- setIndex(index, true);
|
|
|
+ const setRollerStyle = (index: number) => {
|
|
|
+ return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
|
|
|
};
|
|
|
|
|
|
- const onTouchStart = (event: Event) => {
|
|
|
- if (props.readonly) {
|
|
|
- return;
|
|
|
+ const isHidden = (index: number) => {
|
|
|
+ if (index >= state.currIndex + 8 || index <= state.currIndex - 8) {
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
}
|
|
|
- touch.start(event);
|
|
|
+ };
|
|
|
|
|
|
- if (state.moving) {
|
|
|
- const translateY = getElementTranslateY(wrapper.value);
|
|
|
- state.offset = Math.min(0, translateY - baseOffset());
|
|
|
- state.startOffset = state.offset;
|
|
|
+ const setTransform = (translateY = 0, type: string | null, time = 1000, deg: string | number) => {
|
|
|
+ if (type === 'end') {
|
|
|
+ touchTime.value = time;
|
|
|
} else {
|
|
|
- state.startOffset = state.offset;
|
|
|
+ touchTime.value = 0;
|
|
|
}
|
|
|
-
|
|
|
- state.duration = 0;
|
|
|
- state.touchStartTime = Date.now();
|
|
|
- state.momentumOffset = state.startOffset;
|
|
|
- state.transitionEndTrigger = null;
|
|
|
+ touchDeg.value = deg as number;
|
|
|
+ touchTranslateY.value = translateY;
|
|
|
+ state.scrollDistance = translateY;
|
|
|
};
|
|
|
- const onTouchMove = (event: Event) => {
|
|
|
- if (props.readonly) {
|
|
|
- return;
|
|
|
- }
|
|
|
- state.moving = true;
|
|
|
- touch.move(event);
|
|
|
|
|
|
- if (touch.isVertical()) {
|
|
|
- state.moving = true;
|
|
|
- preventDefault(event, true);
|
|
|
- }
|
|
|
+ const setMove = (move: number, type?: string, time?: number) => {
|
|
|
+ let updateMove = move + state.transformY;
|
|
|
+ if (type === 'end') {
|
|
|
+ // 限定滚动距离
|
|
|
+ if (updateMove > 0) {
|
|
|
+ updateMove = 0;
|
|
|
+ }
|
|
|
+ if (updateMove < -(props.listData.values.length - 1) * state.lineSpacing) {
|
|
|
+ updateMove = -(props.listData.values.length - 1) * state.lineSpacing;
|
|
|
+ }
|
|
|
|
|
|
- const moveOffset = state.startOffset + touch.deltaY.value;
|
|
|
- if (moveOffset > props.itemHeight) {
|
|
|
- state.offset = props.itemHeight as number;
|
|
|
- } else {
|
|
|
- state.offset = state.startOffset + touch.deltaY.value;
|
|
|
- }
|
|
|
+ // 设置滚动距离为lineSpacing的倍数值
|
|
|
+ let endMove = Math.round(updateMove / state.lineSpacing) * state.lineSpacing;
|
|
|
+ let deg = `${(Math.abs(Math.round(endMove / state.lineSpacing)) + 1) * state.rotation}deg`;
|
|
|
+ setTransform(endMove, type, time, deg);
|
|
|
|
|
|
- const now = Date.now();
|
|
|
+ let t = time ? time / 2 : 0;
|
|
|
+ (state.timer as any) = setTimeout(() => {
|
|
|
+ setChooseValue();
|
|
|
+ }, t);
|
|
|
|
|
|
- if (now - state.touchStartTime > MOMENTUM_LIMIT_TIME) {
|
|
|
- state.touchStartTime = now;
|
|
|
- state.momentumOffset = state.offset;
|
|
|
+ state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
|
|
|
+ } else {
|
|
|
+ let deg = '0deg';
|
|
|
+ if (updateMove < 0) {
|
|
|
+ deg = `${(Math.abs(updateMove / state.lineSpacing) + 1) * state.rotation}deg`;
|
|
|
+ } else {
|
|
|
+ deg = `${(-updateMove / state.lineSpacing + 1) * state.rotation}deg`;
|
|
|
+ }
|
|
|
+
|
|
|
+ setTransform(updateMove, null, undefined, deg);
|
|
|
+ state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
|
|
|
}
|
|
|
};
|
|
|
- const onTouchEnd = () => {
|
|
|
- const index = getIndexByOffset(state.offset);
|
|
|
- state.duration = DEFAULT_DURATION;
|
|
|
- setIndex(index, true);
|
|
|
- const distance = state.offset - state.momentumOffset;
|
|
|
- const duration = Date.now() - state.touchStartTime;
|
|
|
-
|
|
|
- const allowMomentum =
|
|
|
- duration < MOMENTUM_LIMIT_TIME &&
|
|
|
- Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
|
|
|
|
|
|
- if (allowMomentum) {
|
|
|
- momentum(distance, duration);
|
|
|
- return;
|
|
|
- }
|
|
|
+ const setChooseValue = () => {
|
|
|
+ emit('change', state.currIndex - 1);
|
|
|
};
|
|
|
|
|
|
- onMounted(() => {
|
|
|
- setIndex(+props.defaultIndex);
|
|
|
- });
|
|
|
+ const modifyStatus = (type: boolean) => {
|
|
|
+ let index = props.defaultIndex;
|
|
|
+
|
|
|
+ state.currIndex = index === -1 ? 1 : (index as number) + 1;
|
|
|
+ let move = index === -1 ? 0 : (index as number) * state.lineSpacing;
|
|
|
+ type && setChooseValue();
|
|
|
+ setMove(-move);
|
|
|
+ };
|
|
|
|
|
|
watch(
|
|
|
() => props.listData,
|
|
|
(val) => {
|
|
|
- if (val) {
|
|
|
- state.options = val as PickerObjectColumn[];
|
|
|
- }
|
|
|
+ state.transformY = 0;
|
|
|
+ modifyStatus(false);
|
|
|
+ },
|
|
|
+ {
|
|
|
+ deep: true
|
|
|
}
|
|
|
);
|
|
|
|
|
|
watch(
|
|
|
() => props.defaultIndex,
|
|
|
(val) => {
|
|
|
- setIndex(+val);
|
|
|
+ state.transformY = 0;
|
|
|
+ modifyStatus(false);
|
|
|
}
|
|
|
);
|
|
|
|
|
|
+ onMounted(() => {
|
|
|
+ modifyStatus(true);
|
|
|
+ });
|
|
|
+
|
|
|
return {
|
|
|
...toRefs(state),
|
|
|
+ ...toRefs(props),
|
|
|
wrapper,
|
|
|
+ setRollerStyle,
|
|
|
+ isHidden,
|
|
|
+ roller,
|
|
|
+ list,
|
|
|
+ listItem,
|
|
|
onTouchStart,
|
|
|
onTouchMove,
|
|
|
onTouchEnd,
|
|
|
- wrapperStyle,
|
|
|
- stopMomentum,
|
|
|
- columns: state.options,
|
|
|
- height: Number(props.visibleItemCount) * +props.itemHeight
|
|
|
+ touchRollerStyle,
|
|
|
+ touchListStyle
|
|
|
};
|
|
|
}
|
|
|
});
|