PickerColumn.mjs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { createVNode as _createVNode } from "vue";
  2. import { ref, watch, reactive, defineComponent } from "vue";
  3. import { deepClone } from "../utils/deep-clone.mjs";
  4. import { clamp, isObject, unknownProp, numericProp, makeArrayProp, makeNumberProp, preventDefault, createNamespace, makeRequiredProp } from "../utils/index.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_LIMIT_TIME = 300;
  10. const MOMENTUM_LIMIT_DISTANCE = 15;
  11. const [name, bem] = createNamespace("picker-column");
  12. function getElementTranslateY(element) {
  13. const {
  14. transform
  15. } = window.getComputedStyle(element);
  16. const translateY = transform.slice(7, transform.length - 1).split(", ")[5];
  17. return Number(translateY);
  18. }
  19. const PICKER_KEY = Symbol(name);
  20. const isOptionDisabled = (option) => isObject(option) && option.disabled;
  21. var stdin_default = defineComponent({
  22. name,
  23. props: {
  24. textKey: makeRequiredProp(String),
  25. readonly: Boolean,
  26. allowHtml: Boolean,
  27. className: unknownProp,
  28. itemHeight: makeRequiredProp(Number),
  29. defaultIndex: makeNumberProp(0),
  30. swipeDuration: makeRequiredProp(numericProp),
  31. initialOptions: makeArrayProp(),
  32. visibleItemCount: makeRequiredProp(numericProp)
  33. },
  34. emits: ["change"],
  35. setup(props, {
  36. emit,
  37. slots
  38. }) {
  39. let moving;
  40. let startOffset;
  41. let touchStartTime;
  42. let momentumOffset;
  43. let transitionEndTrigger;
  44. const root = ref();
  45. const wrapper = ref();
  46. const state = reactive({
  47. index: props.defaultIndex,
  48. offset: 0,
  49. duration: 0,
  50. options: deepClone(props.initialOptions)
  51. });
  52. const touch = useTouch();
  53. const count = () => state.options.length;
  54. const baseOffset = () => props.itemHeight * (+props.visibleItemCount - 1) / 2;
  55. const adjustIndex = (index) => {
  56. index = clamp(index, 0, count());
  57. for (let i = index; i < count(); i++) {
  58. if (!isOptionDisabled(state.options[i]))
  59. return i;
  60. }
  61. for (let i = index - 1; i >= 0; i--) {
  62. if (!isOptionDisabled(state.options[i]))
  63. return i;
  64. }
  65. };
  66. const setIndex = (index, emitChange) => {
  67. index = adjustIndex(index) || 0;
  68. const offset = -index * props.itemHeight;
  69. const trigger = () => {
  70. if (index !== state.index) {
  71. state.index = index;
  72. if (emitChange) {
  73. emit("change", index);
  74. }
  75. }
  76. };
  77. if (moving && offset !== state.offset) {
  78. transitionEndTrigger = trigger;
  79. } else {
  80. trigger();
  81. }
  82. state.offset = offset;
  83. };
  84. const setOptions = (options) => {
  85. if (JSON.stringify(options) !== JSON.stringify(state.options)) {
  86. state.options = deepClone(options);
  87. setIndex(props.defaultIndex);
  88. }
  89. };
  90. const onClickItem = (index) => {
  91. if (moving || props.readonly) {
  92. return;
  93. }
  94. transitionEndTrigger = null;
  95. state.duration = DEFAULT_DURATION;
  96. setIndex(index, true);
  97. };
  98. const getOptionText = (option) => {
  99. if (isObject(option) && props.textKey in option) {
  100. return option[props.textKey];
  101. }
  102. return option;
  103. };
  104. const getIndexByOffset = (offset) => clamp(Math.round(-offset / props.itemHeight), 0, count() - 1);
  105. const momentum = (distance, duration) => {
  106. const speed = Math.abs(distance / duration);
  107. distance = state.offset + speed / 3e-3 * (distance < 0 ? -1 : 1);
  108. const index = getIndexByOffset(distance);
  109. state.duration = +props.swipeDuration;
  110. setIndex(index, true);
  111. };
  112. const stopMomentum = () => {
  113. moving = false;
  114. state.duration = 0;
  115. if (transitionEndTrigger) {
  116. transitionEndTrigger();
  117. transitionEndTrigger = null;
  118. }
  119. };
  120. const onTouchStart = (event) => {
  121. if (props.readonly) {
  122. return;
  123. }
  124. touch.start(event);
  125. if (moving) {
  126. const translateY = getElementTranslateY(wrapper.value);
  127. state.offset = Math.min(0, translateY - baseOffset());
  128. startOffset = state.offset;
  129. } else {
  130. startOffset = state.offset;
  131. }
  132. state.duration = 0;
  133. touchStartTime = Date.now();
  134. momentumOffset = startOffset;
  135. transitionEndTrigger = null;
  136. };
  137. const onTouchMove = (event) => {
  138. if (props.readonly) {
  139. return;
  140. }
  141. touch.move(event);
  142. if (touch.isVertical()) {
  143. moving = true;
  144. preventDefault(event, true);
  145. }
  146. state.offset = clamp(startOffset + touch.deltaY.value, -(count() * props.itemHeight), props.itemHeight);
  147. const now = Date.now();
  148. if (now - touchStartTime > MOMENTUM_LIMIT_TIME) {
  149. touchStartTime = now;
  150. momentumOffset = state.offset;
  151. }
  152. };
  153. const onTouchEnd = () => {
  154. if (props.readonly) {
  155. return;
  156. }
  157. const distance = state.offset - momentumOffset;
  158. const duration = Date.now() - touchStartTime;
  159. const allowMomentum = duration < MOMENTUM_LIMIT_TIME && Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
  160. if (allowMomentum) {
  161. momentum(distance, duration);
  162. return;
  163. }
  164. const index = getIndexByOffset(state.offset);
  165. state.duration = DEFAULT_DURATION;
  166. setIndex(index, true);
  167. setTimeout(() => {
  168. moving = false;
  169. }, 0);
  170. };
  171. const renderOptions = () => {
  172. const optionStyle = {
  173. height: `${props.itemHeight}px`
  174. };
  175. return state.options.map((option, index) => {
  176. const text = getOptionText(option);
  177. const disabled = isOptionDisabled(option);
  178. const data = {
  179. role: "button",
  180. style: optionStyle,
  181. tabindex: disabled ? -1 : 0,
  182. class: bem("item", {
  183. disabled,
  184. selected: index === state.index
  185. }),
  186. onClick: () => onClickItem(index)
  187. };
  188. const childData = {
  189. class: "van-ellipsis",
  190. [props.allowHtml ? "innerHTML" : "textContent"]: text
  191. };
  192. return _createVNode("li", data, [slots.option ? slots.option(option) : _createVNode("div", childData, null)]);
  193. });
  194. };
  195. const setValue = (value) => {
  196. const {
  197. options
  198. } = state;
  199. for (let i = 0; i < options.length; i++) {
  200. if (getOptionText(options[i]) === value) {
  201. return setIndex(i);
  202. }
  203. }
  204. };
  205. const getValue = () => state.options[state.index];
  206. const hasOptions = () => state.options.length;
  207. setIndex(state.index);
  208. useParent(PICKER_KEY);
  209. useExpose({
  210. state,
  211. setIndex,
  212. getValue,
  213. setValue,
  214. setOptions,
  215. hasOptions,
  216. stopMomentum
  217. });
  218. watch(() => props.initialOptions, setOptions);
  219. watch(() => props.defaultIndex, (value) => setIndex(value));
  220. useEventListener("touchmove", onTouchMove, {
  221. target: root
  222. });
  223. return () => _createVNode("div", {
  224. "ref": root,
  225. "class": [bem(), props.className],
  226. "onTouchstartPassive": onTouchStart,
  227. "onTouchend": onTouchEnd,
  228. "onTouchcancel": onTouchEnd
  229. }, [_createVNode("ul", {
  230. "ref": wrapper,
  231. "style": {
  232. transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
  233. transitionDuration: `${state.duration}ms`,
  234. transitionProperty: state.duration ? "all" : "none"
  235. },
  236. "class": bem("wrapper"),
  237. "onTransitionend": stopMomentum
  238. }, [renderOptions()])]);
  239. }
  240. });
  241. export {
  242. PICKER_KEY,
  243. stdin_default as default
  244. };