Swipe.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import { ref, watch, reactive, computed, onMounted, onActivated, onDeactivated, onBeforeUnmount, defineComponent, nextTick, createVNode as _createVNode } from "vue";
  2. import { clamp, isHidden, truthProp, numericProp, windowWidth, windowHeight, preventDefault, createNamespace, makeNumericProp } from "../utils/index.mjs";
  3. import { doubleRaf, useChildren, useEventListener, usePageVisibility } from "@vant/use";
  4. import { useTouch } from "../composables/use-touch.mjs";
  5. import { useExpose } from "../composables/use-expose.mjs";
  6. import { onPopupReopen } from "../composables/on-popup-reopen.mjs";
  7. const [name, bem] = createNamespace("swipe");
  8. const swipeProps = {
  9. loop: truthProp,
  10. width: numericProp,
  11. height: numericProp,
  12. vertical: Boolean,
  13. autoplay: makeNumericProp(0),
  14. duration: makeNumericProp(500),
  15. touchable: truthProp,
  16. lazyRender: Boolean,
  17. initialSwipe: makeNumericProp(0),
  18. indicatorColor: String,
  19. showIndicators: truthProp,
  20. stopPropagation: truthProp
  21. };
  22. const SWIPE_KEY = Symbol(name);
  23. var stdin_default = defineComponent({
  24. name,
  25. props: swipeProps,
  26. emits: ["change", "dragStart", "dragEnd"],
  27. setup(props, {
  28. emit,
  29. slots
  30. }) {
  31. const root = ref();
  32. const track = ref();
  33. const state = reactive({
  34. rect: null,
  35. width: 0,
  36. height: 0,
  37. offset: 0,
  38. active: 0,
  39. swiping: false
  40. });
  41. let dragging = false;
  42. const touch = useTouch();
  43. const {
  44. children,
  45. linkChildren
  46. } = useChildren(SWIPE_KEY);
  47. const count = computed(() => children.length);
  48. const size = computed(() => state[props.vertical ? "height" : "width"]);
  49. const delta = computed(() => props.vertical ? touch.deltaY.value : touch.deltaX.value);
  50. const minOffset = computed(() => {
  51. if (state.rect) {
  52. const base = props.vertical ? state.rect.height : state.rect.width;
  53. return base - size.value * count.value;
  54. }
  55. return 0;
  56. });
  57. const maxCount = computed(() => size.value ? Math.ceil(Math.abs(minOffset.value) / size.value) : count.value);
  58. const trackSize = computed(() => count.value * size.value);
  59. const activeIndicator = computed(() => (state.active + count.value) % count.value);
  60. const isCorrectDirection = computed(() => {
  61. const expect = props.vertical ? "vertical" : "horizontal";
  62. return touch.direction.value === expect;
  63. });
  64. const trackStyle = computed(() => {
  65. const style = {
  66. transitionDuration: `${state.swiping ? 0 : props.duration}ms`,
  67. transform: `translate${props.vertical ? "Y" : "X"}(${+state.offset.toFixed(2)}px)`
  68. };
  69. if (size.value) {
  70. const mainAxis = props.vertical ? "height" : "width";
  71. const crossAxis = props.vertical ? "width" : "height";
  72. style[mainAxis] = `${trackSize.value}px`;
  73. style[crossAxis] = props[crossAxis] ? `${props[crossAxis]}px` : "";
  74. }
  75. return style;
  76. });
  77. const getTargetActive = (pace) => {
  78. const {
  79. active
  80. } = state;
  81. if (pace) {
  82. if (props.loop) {
  83. return clamp(active + pace, -1, count.value);
  84. }
  85. return clamp(active + pace, 0, maxCount.value);
  86. }
  87. return active;
  88. };
  89. const getTargetOffset = (targetActive, offset = 0) => {
  90. let currentPosition = targetActive * size.value;
  91. if (!props.loop) {
  92. currentPosition = Math.min(currentPosition, -minOffset.value);
  93. }
  94. let targetOffset = offset - currentPosition;
  95. if (!props.loop) {
  96. targetOffset = clamp(targetOffset, minOffset.value, 0);
  97. }
  98. return targetOffset;
  99. };
  100. const move = ({
  101. pace = 0,
  102. offset = 0,
  103. emitChange
  104. }) => {
  105. if (count.value <= 1) {
  106. return;
  107. }
  108. const {
  109. active
  110. } = state;
  111. const targetActive = getTargetActive(pace);
  112. const targetOffset = getTargetOffset(targetActive, offset);
  113. if (props.loop) {
  114. if (children[0] && targetOffset !== minOffset.value) {
  115. const outRightBound = targetOffset < minOffset.value;
  116. children[0].setOffset(outRightBound ? trackSize.value : 0);
  117. }
  118. if (children[count.value - 1] && targetOffset !== 0) {
  119. const outLeftBound = targetOffset > 0;
  120. children[count.value - 1].setOffset(outLeftBound ? -trackSize.value : 0);
  121. }
  122. }
  123. state.active = targetActive;
  124. state.offset = targetOffset;
  125. if (emitChange && targetActive !== active) {
  126. emit("change", activeIndicator.value);
  127. }
  128. };
  129. const correctPosition = () => {
  130. state.swiping = true;
  131. if (state.active <= -1) {
  132. move({
  133. pace: count.value
  134. });
  135. } else if (state.active >= count.value) {
  136. move({
  137. pace: -count.value
  138. });
  139. }
  140. };
  141. const prev = () => {
  142. correctPosition();
  143. touch.reset();
  144. doubleRaf(() => {
  145. state.swiping = false;
  146. move({
  147. pace: -1,
  148. emitChange: true
  149. });
  150. });
  151. };
  152. const next = () => {
  153. correctPosition();
  154. touch.reset();
  155. doubleRaf(() => {
  156. state.swiping = false;
  157. move({
  158. pace: 1,
  159. emitChange: true
  160. });
  161. });
  162. };
  163. let autoplayTimer;
  164. const stopAutoplay = () => clearTimeout(autoplayTimer);
  165. const autoplay = () => {
  166. stopAutoplay();
  167. if (+props.autoplay > 0 && count.value > 1) {
  168. autoplayTimer = setTimeout(() => {
  169. next();
  170. autoplay();
  171. }, +props.autoplay);
  172. }
  173. };
  174. const initialize = (active = +props.initialSwipe) => {
  175. if (!root.value) {
  176. return;
  177. }
  178. const cb = () => {
  179. var _a, _b;
  180. if (!isHidden(root)) {
  181. const rect = {
  182. width: root.value.offsetWidth,
  183. height: root.value.offsetHeight
  184. };
  185. state.rect = rect;
  186. state.width = +((_a = props.width) != null ? _a : rect.width);
  187. state.height = +((_b = props.height) != null ? _b : rect.height);
  188. }
  189. if (count.value) {
  190. active = Math.min(count.value - 1, active);
  191. if (active === -1) {
  192. active = count.value - 1;
  193. }
  194. }
  195. state.active = active;
  196. state.swiping = true;
  197. state.offset = getTargetOffset(active);
  198. children.forEach((swipe) => {
  199. swipe.setOffset(0);
  200. });
  201. autoplay();
  202. };
  203. if (isHidden(root)) {
  204. nextTick().then(cb);
  205. } else {
  206. cb();
  207. }
  208. };
  209. const resize = () => initialize(state.active);
  210. let touchStartTime;
  211. const onTouchStart = (event) => {
  212. if (!props.touchable || // avoid resetting position on multi-finger touch
  213. event.touches.length > 1) return;
  214. touch.start(event);
  215. dragging = false;
  216. touchStartTime = Date.now();
  217. stopAutoplay();
  218. correctPosition();
  219. };
  220. const onTouchMove = (event) => {
  221. if (props.touchable && state.swiping) {
  222. touch.move(event);
  223. if (isCorrectDirection.value) {
  224. const isEdgeTouch = !props.loop && (state.active === 0 && delta.value > 0 || state.active === count.value - 1 && delta.value < 0);
  225. if (!isEdgeTouch) {
  226. preventDefault(event, props.stopPropagation);
  227. move({
  228. offset: delta.value
  229. });
  230. if (!dragging) {
  231. emit("dragStart", {
  232. index: activeIndicator.value
  233. });
  234. dragging = true;
  235. }
  236. }
  237. }
  238. }
  239. };
  240. const onTouchEnd = () => {
  241. if (!props.touchable || !state.swiping) {
  242. return;
  243. }
  244. const duration = Date.now() - touchStartTime;
  245. const speed = delta.value / duration;
  246. const shouldSwipe = Math.abs(speed) > 0.25 || Math.abs(delta.value) > size.value / 2;
  247. if (shouldSwipe && isCorrectDirection.value) {
  248. const offset = props.vertical ? touch.offsetY.value : touch.offsetX.value;
  249. let pace = 0;
  250. if (props.loop) {
  251. pace = offset > 0 ? delta.value > 0 ? -1 : 1 : 0;
  252. } else {
  253. pace = -Math[delta.value > 0 ? "ceil" : "floor"](delta.value / size.value);
  254. }
  255. move({
  256. pace,
  257. emitChange: true
  258. });
  259. } else if (delta.value) {
  260. move({
  261. pace: 0
  262. });
  263. }
  264. dragging = false;
  265. state.swiping = false;
  266. emit("dragEnd", {
  267. index: activeIndicator.value
  268. });
  269. autoplay();
  270. };
  271. const swipeTo = (index, options = {}) => {
  272. correctPosition();
  273. touch.reset();
  274. doubleRaf(() => {
  275. let targetIndex;
  276. if (props.loop && index === count.value) {
  277. targetIndex = state.active === 0 ? 0 : index;
  278. } else {
  279. targetIndex = index % count.value;
  280. }
  281. if (options.immediate) {
  282. doubleRaf(() => {
  283. state.swiping = false;
  284. });
  285. } else {
  286. state.swiping = false;
  287. }
  288. move({
  289. pace: targetIndex - state.active,
  290. emitChange: true
  291. });
  292. });
  293. };
  294. const renderDot = (_, index) => {
  295. const active = index === activeIndicator.value;
  296. const style = active ? {
  297. backgroundColor: props.indicatorColor
  298. } : void 0;
  299. return _createVNode("i", {
  300. "style": style,
  301. "class": bem("indicator", {
  302. active
  303. })
  304. }, null);
  305. };
  306. const renderIndicator = () => {
  307. if (slots.indicator) {
  308. return slots.indicator({
  309. active: activeIndicator.value,
  310. total: count.value
  311. });
  312. }
  313. if (props.showIndicators && count.value > 1) {
  314. return _createVNode("div", {
  315. "class": bem("indicators", {
  316. vertical: props.vertical
  317. })
  318. }, [Array(count.value).fill("").map(renderDot)]);
  319. }
  320. };
  321. useExpose({
  322. prev,
  323. next,
  324. state,
  325. resize,
  326. swipeTo
  327. });
  328. linkChildren({
  329. size,
  330. props,
  331. count,
  332. activeIndicator
  333. });
  334. watch(() => props.initialSwipe, (value) => initialize(+value));
  335. watch(count, () => initialize(state.active));
  336. watch(() => props.autoplay, autoplay);
  337. watch([windowWidth, windowHeight, () => props.width, () => props.height], resize);
  338. watch(usePageVisibility(), (visible) => {
  339. if (visible === "visible") {
  340. autoplay();
  341. } else {
  342. stopAutoplay();
  343. }
  344. });
  345. onMounted(initialize);
  346. onActivated(() => initialize(state.active));
  347. onPopupReopen(() => initialize(state.active));
  348. onDeactivated(stopAutoplay);
  349. onBeforeUnmount(stopAutoplay);
  350. useEventListener("touchmove", onTouchMove, {
  351. target: track
  352. });
  353. return () => {
  354. var _a;
  355. return _createVNode("div", {
  356. "ref": root,
  357. "class": bem()
  358. }, [_createVNode("div", {
  359. "ref": track,
  360. "style": trackStyle.value,
  361. "class": bem("track", {
  362. vertical: props.vertical
  363. }),
  364. "onTouchstartPassive": onTouchStart,
  365. "onTouchend": onTouchEnd,
  366. "onTouchcancel": onTouchEnd
  367. }, [(_a = slots.default) == null ? void 0 : _a.call(slots)]), renderIndicator()]);
  368. };
  369. }
  370. });
  371. export {
  372. SWIPE_KEY,
  373. stdin_default as default,
  374. swipeProps
  375. };