204 lines
6.9 KiB
JavaScript
204 lines
6.9 KiB
JavaScript
import { ref, computed, watchEffect, defineComponent, createVNode as _createVNode } from "vue";
|
|
import { clamp, numericProp, makeArrayProp, preventDefault, createNamespace, makeRequiredProp } from "../utils/index.mjs";
|
|
import { getElementTranslateY, findIndexOfEnabledOption } from "./utils.mjs";
|
|
import { useEventListener, useParent } from "@vant/use";
|
|
import { useTouch } from "../composables/use-touch.mjs";
|
|
import { useExpose } from "../composables/use-expose.mjs";
|
|
const DEFAULT_DURATION = 200;
|
|
const MOMENTUM_TIME = 300;
|
|
const MOMENTUM_DISTANCE = 15;
|
|
const [name, bem] = createNamespace("picker-column");
|
|
const PICKER_KEY = Symbol(name);
|
|
var stdin_default = defineComponent({
|
|
name,
|
|
props: {
|
|
value: numericProp,
|
|
fields: makeRequiredProp(Object),
|
|
options: makeArrayProp(),
|
|
readonly: Boolean,
|
|
allowHtml: Boolean,
|
|
optionHeight: makeRequiredProp(Number),
|
|
swipeDuration: makeRequiredProp(numericProp),
|
|
visibleOptionNum: makeRequiredProp(numericProp)
|
|
},
|
|
emits: ["change", "clickOption", "scrollInto"],
|
|
setup(props, {
|
|
emit,
|
|
slots
|
|
}) {
|
|
let moving;
|
|
let startOffset;
|
|
let touchStartTime;
|
|
let momentumOffset;
|
|
let transitionEndTrigger;
|
|
const root = ref();
|
|
const wrapper = ref();
|
|
const currentOffset = ref(0);
|
|
const currentDuration = ref(0);
|
|
const touch = useTouch();
|
|
const count = () => props.options.length;
|
|
const baseOffset = () => props.optionHeight * (+props.visibleOptionNum - 1) / 2;
|
|
const updateValueByIndex = (index) => {
|
|
let enabledIndex = findIndexOfEnabledOption(props.options, index);
|
|
const offset = -enabledIndex * props.optionHeight;
|
|
const trigger = () => {
|
|
if (enabledIndex > count() - 1) {
|
|
enabledIndex = findIndexOfEnabledOption(props.options, index);
|
|
}
|
|
const value = props.options[enabledIndex][props.fields.value];
|
|
if (value !== props.value) {
|
|
emit("change", value);
|
|
}
|
|
};
|
|
if (moving && offset !== currentOffset.value) {
|
|
transitionEndTrigger = trigger;
|
|
} else {
|
|
trigger();
|
|
}
|
|
currentOffset.value = offset;
|
|
};
|
|
const isReadonly = () => props.readonly || !props.options.length;
|
|
const onClickOption = (index) => {
|
|
if (moving || isReadonly()) {
|
|
return;
|
|
}
|
|
transitionEndTrigger = null;
|
|
currentDuration.value = DEFAULT_DURATION;
|
|
updateValueByIndex(index);
|
|
emit("clickOption", props.options[index]);
|
|
};
|
|
const getIndexByOffset = (offset) => clamp(Math.round(-offset / props.optionHeight), 0, count() - 1);
|
|
const currentIndex = computed(() => getIndexByOffset(currentOffset.value));
|
|
const momentum = (distance, duration) => {
|
|
const speed = Math.abs(distance / duration);
|
|
distance = currentOffset.value + speed / 3e-3 * (distance < 0 ? -1 : 1);
|
|
const index = getIndexByOffset(distance);
|
|
currentDuration.value = +props.swipeDuration;
|
|
updateValueByIndex(index);
|
|
};
|
|
const stopMomentum = () => {
|
|
moving = false;
|
|
currentDuration.value = 0;
|
|
if (transitionEndTrigger) {
|
|
transitionEndTrigger();
|
|
transitionEndTrigger = null;
|
|
}
|
|
};
|
|
const onTouchStart = (event) => {
|
|
if (isReadonly()) {
|
|
return;
|
|
}
|
|
touch.start(event);
|
|
if (moving) {
|
|
const translateY = getElementTranslateY(wrapper.value);
|
|
currentOffset.value = Math.min(0, translateY - baseOffset());
|
|
}
|
|
currentDuration.value = 0;
|
|
startOffset = currentOffset.value;
|
|
touchStartTime = Date.now();
|
|
momentumOffset = startOffset;
|
|
transitionEndTrigger = null;
|
|
};
|
|
const onTouchMove = (event) => {
|
|
if (isReadonly()) {
|
|
return;
|
|
}
|
|
touch.move(event);
|
|
if (touch.isVertical()) {
|
|
moving = true;
|
|
preventDefault(event, true);
|
|
}
|
|
const newOffset = clamp(startOffset + touch.deltaY.value, -(count() * props.optionHeight), props.optionHeight);
|
|
const newIndex = getIndexByOffset(newOffset);
|
|
if (newIndex !== currentIndex.value) {
|
|
emit("scrollInto", props.options[newIndex]);
|
|
}
|
|
currentOffset.value = newOffset;
|
|
const now = Date.now();
|
|
if (now - touchStartTime > MOMENTUM_TIME) {
|
|
touchStartTime = now;
|
|
momentumOffset = newOffset;
|
|
}
|
|
};
|
|
const onTouchEnd = () => {
|
|
if (isReadonly()) {
|
|
return;
|
|
}
|
|
const distance = currentOffset.value - momentumOffset;
|
|
const duration = Date.now() - touchStartTime;
|
|
const startMomentum = duration < MOMENTUM_TIME && Math.abs(distance) > MOMENTUM_DISTANCE;
|
|
if (startMomentum) {
|
|
momentum(distance, duration);
|
|
return;
|
|
}
|
|
const index = getIndexByOffset(currentOffset.value);
|
|
currentDuration.value = DEFAULT_DURATION;
|
|
updateValueByIndex(index);
|
|
setTimeout(() => {
|
|
moving = false;
|
|
}, 0);
|
|
};
|
|
const renderOptions = () => {
|
|
const optionStyle = {
|
|
height: `${props.optionHeight}px`
|
|
};
|
|
return props.options.map((option, index) => {
|
|
const text = option[props.fields.text];
|
|
const {
|
|
disabled
|
|
} = option;
|
|
const value = option[props.fields.value];
|
|
const data = {
|
|
role: "button",
|
|
style: optionStyle,
|
|
tabindex: disabled ? -1 : 0,
|
|
class: [bem("item", {
|
|
disabled,
|
|
selected: value === props.value
|
|
}), option.className],
|
|
onClick: () => onClickOption(index)
|
|
};
|
|
const childData = {
|
|
class: "van-ellipsis",
|
|
[props.allowHtml ? "innerHTML" : "textContent"]: text
|
|
};
|
|
return _createVNode("li", data, [slots.option ? slots.option(option, index) : _createVNode("div", childData, null)]);
|
|
});
|
|
};
|
|
useParent(PICKER_KEY);
|
|
useExpose({
|
|
stopMomentum
|
|
});
|
|
watchEffect(() => {
|
|
const index = moving ? Math.floor(-currentOffset.value / props.optionHeight) : props.options.findIndex((option) => option[props.fields.value] === props.value);
|
|
const enabledIndex = findIndexOfEnabledOption(props.options, index);
|
|
const offset = -enabledIndex * props.optionHeight;
|
|
if (moving && enabledIndex < index) stopMomentum();
|
|
currentOffset.value = offset;
|
|
});
|
|
useEventListener("touchmove", onTouchMove, {
|
|
target: root
|
|
});
|
|
return () => _createVNode("div", {
|
|
"ref": root,
|
|
"class": bem(),
|
|
"onTouchstartPassive": onTouchStart,
|
|
"onTouchend": onTouchEnd,
|
|
"onTouchcancel": onTouchEnd
|
|
}, [_createVNode("ul", {
|
|
"ref": wrapper,
|
|
"style": {
|
|
transform: `translate3d(0, ${currentOffset.value + baseOffset()}px, 0)`,
|
|
transitionDuration: `${currentDuration.value}ms`,
|
|
transitionProperty: currentDuration.value ? "all" : "none"
|
|
},
|
|
"class": bem("wrapper"),
|
|
"onTransitionend": stopMomentum
|
|
}, [renderOptions()])]);
|
|
}
|
|
});
|
|
export {
|
|
PICKER_KEY,
|
|
stdin_default as default
|
|
};
|