PickerColumn.mjs 6.4 KB

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