PickerColumn.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { ref, computed, watchEffect, defineComponent, createVNode as _createVNode } from "vue";
  2. import { clamp, numericProp, makeArrayProp, preventDefault, createNamespace, makeRequiredProp } from "../utils/index.mjs";
  3. import { getElementTranslateY, findIndexOfEnabledOption } from "./utils.mjs";
  4. import { useEventListener, useParent } from "@vant/use";
  5. import { useTouch } from "../composables/use-touch.mjs";
  6. import { useExpose } from "../composables/use-expose.mjs";
  7. const DEFAULT_DURATION = 200;
  8. const MOMENTUM_TIME = 300;
  9. const MOMENTUM_DISTANCE = 15;
  10. const [name, bem] = createNamespace("picker-column");
  11. const PICKER_KEY = Symbol(name);
  12. var stdin_default = defineComponent({
  13. name,
  14. props: {
  15. value: numericProp,
  16. fields: makeRequiredProp(Object),
  17. options: makeArrayProp(),
  18. readonly: Boolean,
  19. allowHtml: Boolean,
  20. optionHeight: makeRequiredProp(Number),
  21. swipeDuration: makeRequiredProp(numericProp),
  22. visibleOptionNum: makeRequiredProp(numericProp)
  23. },
  24. emits: ["change", "clickOption", "scrollInto"],
  25. setup(props, {
  26. emit,
  27. slots
  28. }) {
  29. let moving;
  30. let startOffset;
  31. let touchStartTime;
  32. let momentumOffset;
  33. let transitionEndTrigger;
  34. const root = ref();
  35. const wrapper = ref();
  36. const currentOffset = ref(0);
  37. const currentDuration = ref(0);
  38. const touch = useTouch();
  39. const count = () => props.options.length;
  40. const baseOffset = () => props.optionHeight * (+props.visibleOptionNum - 1) / 2;
  41. const updateValueByIndex = (index) => {
  42. let enabledIndex = findIndexOfEnabledOption(props.options, index);
  43. const offset = -enabledIndex * props.optionHeight;
  44. const trigger = () => {
  45. if (enabledIndex > count() - 1) {
  46. enabledIndex = findIndexOfEnabledOption(props.options, index);
  47. }
  48. const value = props.options[enabledIndex][props.fields.value];
  49. if (value !== props.value) {
  50. emit("change", value);
  51. }
  52. };
  53. if (moving && offset !== currentOffset.value) {
  54. transitionEndTrigger = trigger;
  55. } else {
  56. trigger();
  57. }
  58. currentOffset.value = offset;
  59. };
  60. const isReadonly = () => props.readonly || !props.options.length;
  61. const onClickOption = (index) => {
  62. if (moving || isReadonly()) {
  63. return;
  64. }
  65. transitionEndTrigger = null;
  66. currentDuration.value = DEFAULT_DURATION;
  67. updateValueByIndex(index);
  68. emit("clickOption", props.options[index]);
  69. };
  70. const getIndexByOffset = (offset) => clamp(Math.round(-offset / props.optionHeight), 0, count() - 1);
  71. const currentIndex = computed(() => getIndexByOffset(currentOffset.value));
  72. const momentum = (distance, duration) => {
  73. const speed = Math.abs(distance / duration);
  74. distance = currentOffset.value + speed / 3e-3 * (distance < 0 ? -1 : 1);
  75. const index = getIndexByOffset(distance);
  76. currentDuration.value = +props.swipeDuration;
  77. updateValueByIndex(index);
  78. };
  79. const stopMomentum = () => {
  80. moving = false;
  81. currentDuration.value = 0;
  82. if (transitionEndTrigger) {
  83. transitionEndTrigger();
  84. transitionEndTrigger = null;
  85. }
  86. };
  87. const onTouchStart = (event) => {
  88. if (isReadonly()) {
  89. return;
  90. }
  91. touch.start(event);
  92. if (moving) {
  93. const translateY = getElementTranslateY(wrapper.value);
  94. currentOffset.value = Math.min(0, translateY - baseOffset());
  95. }
  96. currentDuration.value = 0;
  97. startOffset = currentOffset.value;
  98. touchStartTime = Date.now();
  99. momentumOffset = startOffset;
  100. transitionEndTrigger = null;
  101. };
  102. const onTouchMove = (event) => {
  103. if (isReadonly()) {
  104. return;
  105. }
  106. touch.move(event);
  107. if (touch.isVertical()) {
  108. moving = true;
  109. preventDefault(event, true);
  110. }
  111. const newOffset = clamp(startOffset + touch.deltaY.value, -(count() * props.optionHeight), props.optionHeight);
  112. const newIndex = getIndexByOffset(newOffset);
  113. if (newIndex !== currentIndex.value) {
  114. emit("scrollInto", props.options[newIndex]);
  115. }
  116. currentOffset.value = newOffset;
  117. const now = Date.now();
  118. if (now - touchStartTime > MOMENTUM_TIME) {
  119. touchStartTime = now;
  120. momentumOffset = newOffset;
  121. }
  122. };
  123. const onTouchEnd = () => {
  124. if (isReadonly()) {
  125. return;
  126. }
  127. const distance = currentOffset.value - momentumOffset;
  128. const duration = Date.now() - touchStartTime;
  129. const startMomentum = duration < MOMENTUM_TIME && Math.abs(distance) > MOMENTUM_DISTANCE;
  130. if (startMomentum) {
  131. momentum(distance, duration);
  132. return;
  133. }
  134. const index = getIndexByOffset(currentOffset.value);
  135. currentDuration.value = DEFAULT_DURATION;
  136. updateValueByIndex(index);
  137. setTimeout(() => {
  138. moving = false;
  139. }, 0);
  140. };
  141. const renderOptions = () => {
  142. const optionStyle = {
  143. height: `${props.optionHeight}px`
  144. };
  145. return props.options.map((option, index) => {
  146. const text = option[props.fields.text];
  147. const {
  148. disabled
  149. } = option;
  150. const value = option[props.fields.value];
  151. const data = {
  152. role: "button",
  153. style: optionStyle,
  154. tabindex: disabled ? -1 : 0,
  155. class: [bem("item", {
  156. disabled,
  157. selected: value === props.value
  158. }), option.className],
  159. onClick: () => onClickOption(index)
  160. };
  161. const childData = {
  162. class: "van-ellipsis",
  163. [props.allowHtml ? "innerHTML" : "textContent"]: text
  164. };
  165. return _createVNode("li", data, [slots.option ? slots.option(option, index) : _createVNode("div", childData, null)]);
  166. });
  167. };
  168. useParent(PICKER_KEY);
  169. useExpose({
  170. stopMomentum
  171. });
  172. watchEffect(() => {
  173. const index = moving ? Math.floor(-currentOffset.value / props.optionHeight) : props.options.findIndex((option) => option[props.fields.value] === props.value);
  174. const enabledIndex = findIndexOfEnabledOption(props.options, index);
  175. const offset = -enabledIndex * props.optionHeight;
  176. if (moving && enabledIndex < index) stopMomentum();
  177. currentOffset.value = offset;
  178. });
  179. useEventListener("touchmove", onTouchMove, {
  180. target: root
  181. });
  182. return () => _createVNode("div", {
  183. "ref": root,
  184. "class": bem(),
  185. "onTouchstartPassive": onTouchStart,
  186. "onTouchend": onTouchEnd,
  187. "onTouchcancel": onTouchEnd
  188. }, [_createVNode("ul", {
  189. "ref": wrapper,
  190. "style": {
  191. transform: `translate3d(0, ${currentOffset.value + baseOffset()}px, 0)`,
  192. transitionDuration: `${currentDuration.value}ms`,
  193. transitionProperty: currentDuration.value ? "all" : "none"
  194. },
  195. "class": bem("wrapper"),
  196. "onTransitionend": stopMomentum
  197. }, [renderOptions()])]);
  198. }
  199. });
  200. export {
  201. PICKER_KEY,
  202. stdin_default as default
  203. };