UNPKG

swiper-next

Version:
762 lines (761 loc) 23.9 kB
"use strict"; const vue = require("vue"); const shared = require("@vue/shared"); const component = require("../../helpers/component.js"); const useEvent = require("../../helpers/useEvent.js"); const useTouchtrack = require("../../helpers/useTouchtrack.js"); const flatVNode = require("../../helpers/flatVNode.js"); const util = require("../../uni-core/helpers/util.js"); const icon = require("../../uni-core/helpers/icon.js"); const props = { indicatorDots: { type: [Boolean, String], default: false }, vertical: { type: [Boolean, String], default: false }, autoplay: { type: [Boolean, String], default: false }, circular: { type: [Boolean, String], default: false }, interval: { type: [Number, String], default: 5e3 }, duration: { type: [Number, String], default: 500 }, current: { type: [Number, String], default: 0 }, indicatorColor: { type: String, default: "" }, indicatorActiveColor: { type: String, default: "" }, previousMargin: { type: String, default: "" }, nextMargin: { type: String, default: "" }, currentItemId: { type: String, default: "" }, skipHiddenItemLayout: { type: [Boolean, String], default: false }, displayMultipleItems: { type: [Number, String], default: 1 }, disableTouch: { type: [Boolean, String], default: false }, navigation: { type: [Boolean, String], default: false }, navigationColor: { type: String, default: "#fff" }, navigationActiveColor: { type: String, default: "rgba(53, 53, 53, 0.6)" } }; function useState(props2) { const interval = vue.computed(() => { const interval2 = Number(props2.interval); return isNaN(interval2) ? 5e3 : interval2; }); const duration = vue.computed(() => { const duration2 = Number(props2.duration); return isNaN(duration2) ? 500 : duration2; }); const displayMultipleItems = vue.computed(() => { const displayMultipleItems2 = Math.round(props2.displayMultipleItems); return isNaN(displayMultipleItems2) ? 1 : displayMultipleItems2; }); const state = vue.reactive({ interval, duration, displayMultipleItems, current: Math.round(props2.current) || 0, currentItemId: props2.currentItemId, userTracking: false }); return state; } function useLayout(props2, state, swiperContexts, slideFrameRef, emit, trigger) { function cancelSchedule() { if (timer) { clearTimeout(timer); timer = null; } } let timer = null; let invalid = true; let viewportPosition = 0; let viewportMoveRatio = 1; let animating = null; let requestedAnimation = false; let contentTrackViewport = 0; let transitionStart; let currentChangeSource = ""; let animationFrame; const swiperEnabled = vue.computed(() => swiperContexts.value.length > state.displayMultipleItems); const circularEnabled = vue.computed(() => props2.circular && swiperEnabled.value); function checkCircularLayout(index) { if (!invalid) { for (let items = swiperContexts.value, n = items.length, i = index + state.displayMultipleItems, r = 0; r < n; r++) { const item = items[r]; const s = Math.floor(index / n) * n + r; const l = s + n; const c = s - n; const u = Math.max(index - (s + 1), s - i, 0); const d = Math.max(index - (l + 1), l - i, 0); const h = Math.max(index - (c + 1), c - i, 0); const p = Math.min(u, d, h); const position = [s, l, c][[u, d, h].indexOf(p)]; item.updatePosition(position, props2.vertical); } } } function updateViewport(index) { if (!(Math.floor(2 * viewportPosition) === Math.floor(2 * index) && Math.ceil(2 * viewportPosition) === Math.ceil(2 * index))) { if (circularEnabled.value) { checkCircularLayout(index); } } const x = props2.vertical ? "0" : `${100 * -index * viewportMoveRatio}%`; const y = props2.vertical ? `${100 * -index * viewportMoveRatio}%` : "0"; const transform = `translate(${x}, ${y}) translateZ(0)`; const slideFrame = slideFrameRef.value; if (slideFrame) { slideFrame.style.webkitTransform = transform; slideFrame.style.transform = transform; } viewportPosition = index; if (!transitionStart) { if (index % 1 === 0) { return; } transitionStart = index; } index -= Math.floor(transitionStart); const items = swiperContexts.value; if (index <= -(items.length - 1)) { index += items.length; } else if (index >= items.length) { index -= items.length; } index = transitionStart % 1 > 0.5 || transitionStart < 0 ? index - 1 : index; trigger("transition", {}, { dx: props2.vertical ? 0 : index * slideFrame.offsetWidth, dy: props2.vertical ? index * slideFrame.offsetHeight : 0 }); } function endViewportAnimation() { if (animating) { updateViewport(animating.toPos); animating = null; } } function normalizeCurrentValue(current) { const { length } = swiperContexts.value; if (!length) { return -1; } const index = (Math.round(current) % length + length) % length; if (circularEnabled.value) { if (length <= state.displayMultipleItems) { return 0; } } else if (index > length - state.displayMultipleItems) { return length - state.displayMultipleItems; } return index; } function cancelViewportAnimation() { animating = null; } function animateFrameFuncProto() { if (!animating) { requestedAnimation = false; return; } const innerAnimating = animating; const { toPos } = innerAnimating; const { acc } = innerAnimating; const { endTime } = innerAnimating; const { source } = innerAnimating; const time = endTime - Date.now(); if (time <= 0) { updateViewport(toPos); animating = null; requestedAnimation = false; transitionStart = null; const item = swiperContexts.value[state.current]; if (item) { const currentItemId = item.getItemId(); trigger("animationfinish", {}, { current: state.current, currentItemId, source }); } return; } const s = acc * time * time / 2; const l = toPos + s; updateViewport(l); animationFrame = requestAnimationFrame(animateFrameFuncProto); } function animateViewport(current, source, n) { cancelViewportAnimation(); const { duration } = state; const { length } = swiperContexts.value; let position = viewportPosition; if (circularEnabled.value) { if (n < 0) { for (; position < current; ) { position += length; } for (; position - length > current; ) { position -= length; } } else if (n > 0) { for (; position > current; ) { position -= length; } for (; position + length < current; ) { position += length; } if (position + length - current < current - position) { position += length; } } else { for (; position + length < current; ) { position += length; } for (; position - length > current; ) { position -= length; } if (position + length - current < current - position) { position += length; } } } else if (source === "click") { current = current + state.displayMultipleItems - 1 < length ? current : 0; } animating = { toPos: current, acc: 2 * (position - current) / (duration * duration), endTime: Date.now() + duration, source }; if (!requestedAnimation) { requestedAnimation = true; animationFrame = requestAnimationFrame(animateFrameFuncProto); } } function scheduleAutoplay() { cancelSchedule(); const items = swiperContexts.value; const callback = function() { timer = null; currentChangeSource = "autoplay"; if (circularEnabled.value) { state.current = normalizeCurrentValue(state.current + 1); } else { state.current = state.current + state.displayMultipleItems < items.length ? state.current + 1 : 0; } animateViewport(state.current, "autoplay", circularEnabled.value ? 1 : 0); timer = setTimeout(callback, state.interval); }; if (!(invalid || items.length <= state.displayMultipleItems)) { timer = setTimeout(callback, state.interval); } } function resetLayout() { cancelSchedule(); endViewportAnimation(); const items = swiperContexts.value; for (let i = 0; i < items.length; i++) { items[i].updatePosition(i, props2.vertical); } viewportMoveRatio = 1; const slideFrameEl = slideFrameRef.value; if (state.displayMultipleItems === 1 && items.length) { const itemRect = items[0].getBoundingClientRect(); const slideFrameRect = slideFrameEl.getBoundingClientRect(); viewportMoveRatio = itemRect.width / slideFrameRect.width; if (!(viewportMoveRatio > 0 && viewportMoveRatio < 1)) { viewportMoveRatio = 1; } } const position = viewportPosition; viewportPosition = -2; const { current } = state; if (current >= 0) { invalid = false; if (state.userTracking) { updateViewport(position + current - contentTrackViewport); contentTrackViewport = current; } else { updateViewport(current); if (props2.autoplay) { scheduleAutoplay(); } } } else { invalid = true; updateViewport(-state.displayMultipleItems - 1); } } vue.watch([() => props2.current, () => props2.currentItemId, () => [...swiperContexts.value]], () => { let current = -1; if (props2.currentItemId) { for (let i = 0, items = swiperContexts.value; i < items.length; i++) { const itemId = items[i].getItemId(); if (itemId === props2.currentItemId) { current = i; break; } } } if (current < 0) { current = Math.round(props2.current) || 0; } current = current < 0 ? 0 : current; if (state.current !== current) { currentChangeSource = ""; state.current = current; } }); vue.watch([() => props2.vertical, () => circularEnabled.value, () => state.displayMultipleItems, () => [...swiperContexts.value]], resetLayout); vue.watch(() => state.interval, () => { if (timer) { cancelSchedule(); scheduleAutoplay(); } }); function currentChanged(current, history) { const source = currentChangeSource; currentChangeSource = ""; const items = swiperContexts.value; if (!source) { const { length } = items; animateViewport(current, "", circularEnabled.value && history + (length - current) % length > length / 2 ? 1 : 0); } const item = items[current]; if (item) { state.currentItemId = item.getItemId(); const { currentItemId } = state; trigger("change", {}, { current: state.current, currentItemId, source }); } } vue.watch(() => state.current, (val, oldVal) => { currentChanged(val, oldVal); emit("update:current", val); }); vue.watch(() => state.currentItemId, (val) => { emit("update:currentItemId", val); }); function inintAutoplay(enable) { if (enable) { scheduleAutoplay(); } else { cancelSchedule(); } } vue.watch(() => props2.autoplay && !state.userTracking, inintAutoplay); inintAutoplay(props2.autoplay && !state.userTracking); vue.onMounted(() => { let userDirectionChecked = false; let contentTrackSpeed = 0; let contentTrackT = 0; function handleTrackStart() { cancelSchedule(); contentTrackViewport = viewportPosition; contentTrackSpeed = 0; contentTrackT = Date.now(); cancelViewportAnimation(); } function handleTrackMove(data) { const oldContentTrackT = contentTrackT; contentTrackT = Date.now(); const { length } = swiperContexts.value; const other = length - state.displayMultipleItems; function calc(val) { return 0.5 - 0.25 / (val + 0.5); } function move(oldVal, newVal) { let val = contentTrackViewport + oldVal; contentTrackSpeed = 0.6 * contentTrackSpeed + 0.4 * newVal; if (!circularEnabled.value) { if (val < 0 || val > other) { if (val < 0) { val = -calc(-val); } else { if (val > other) { val = other + calc(val - other); } } contentTrackSpeed = 0; } } updateViewport(val); } const time = contentTrackT - oldContentTrackT || 1; const slideFrameEl = slideFrameRef.value; if (props2.vertical) { move(-data.dy / slideFrameEl.offsetHeight, -data.ddy / time); } else { move(-data.dx / slideFrameEl.offsetWidth, -data.ddx / time); } } function handleTrackEnd(isCancel) { state.userTracking = false; const t = contentTrackSpeed / Math.abs(contentTrackSpeed); let n = 0; if (!isCancel && Math.abs(contentTrackSpeed) > 0.2) { n = 0.5 * t; } const current = normalizeCurrentValue(viewportPosition + n); if (isCancel) { updateViewport(contentTrackViewport); } else { currentChangeSource = "touch"; state.current = current; animateViewport(current, "touch", n !== 0 ? n : current === 0 && circularEnabled.value && viewportPosition >= 1 ? 1 : 0); } } useTouchtrack.useTouchtrack(slideFrameRef.value, (event) => { if (props2.disableTouch) { return; } if (!invalid) { if (event.detail.state === "start") { state.userTracking = true; userDirectionChecked = false; return handleTrackStart(); } if (event.detail.state === "end") { return handleTrackEnd(false); } if (event.detail.state === "cancel") { return handleTrackEnd(true); } if (state.userTracking) { if (!userDirectionChecked) { userDirectionChecked = true; const t = Math.abs(event.detail.dx); const n = Math.abs(event.detail.dy); if (t >= n && props2.vertical) { state.userTracking = false; } else { if (t <= n && !props2.vertical) { state.userTracking = false; } } if (!state.userTracking) { if (props2.autoplay) { scheduleAutoplay(); } return; } } handleTrackMove(event.detail); return false; } } }); }); vue.onUnmounted(() => { cancelSchedule(); cancelAnimationFrame(animationFrame); }); function onSwiperDotClick(index) { animateViewport(state.current = index, currentChangeSource = "click", circularEnabled.value ? 1 : 0); } return { onSwiperDotClick, circularEnabled, swiperEnabled }; } const _Swiper = /* @__PURE__ */ component.defineBuiltInComponent({ name: "Swiper", props, emits: ["change", "transition", "animationfinish", "update:current", "update:currentItemId"], // #if _X_ && !_NODE_JS_ // rootElement: { // name: 'uni-swiper', // class: UniSwiperElement, // }, // #endif setup(props2, { slots, emit }) { const rootRef = vue.ref(null); const trigger = useEvent.useCustomEvent(rootRef, emit); const slidesWrapperRef = vue.ref(null); const slideFrameRef = vue.ref(null); const state = useState(props2); const slidesStyle = vue.computed(() => { let style = {}; if (props2.nextMargin || props2.previousMargin) { style = props2.vertical ? { left: 0, right: 0, top: util.rpx2px(props2.previousMargin, true), bottom: util.rpx2px(props2.nextMargin, true) } : { top: 0, bottom: 0, left: util.rpx2px(props2.previousMargin, true), right: util.rpx2px(props2.nextMargin, true) }; } return style; }); const slideFrameStyle = vue.computed(() => { const value = `${Math.abs(100 / state.displayMultipleItems)}%`; return { width: props2.vertical ? "100%" : value, height: !props2.vertical ? "100%" : value }; }); let swiperItems = []; const originSwiperContexts = []; const swiperContexts = vue.ref([]); function updateSwiperContexts() { const contexts = []; for (let index = 0; index < swiperItems.length; index++) { let swiperItem = swiperItems[index]; if (!(swiperItem instanceof Element)) { swiperItem = swiperItem.el; } const swiperContext = originSwiperContexts.find((context) => swiperItem === context.rootRef.value); if (swiperContext) { contexts.push(vue.markRaw(swiperContext)); } } swiperContexts.value = contexts; } const addSwiperContext = function(swiperContext) { originSwiperContexts.push(swiperContext); updateSwiperContexts(); }; vue.provide("addSwiperContext", addSwiperContext); const removeSwiperContext = function(swiperContext) { const index = originSwiperContexts.indexOf(swiperContext); if (index >= 0) { originSwiperContexts.splice(index, 1); updateSwiperContexts(); } }; vue.provide("removeSwiperContext", removeSwiperContext); const { onSwiperDotClick, circularEnabled, swiperEnabled } = useLayout(props2, state, swiperContexts, slideFrameRef, emit, trigger); let createNavigationTsx = () => null; { createNavigationTsx = useSwiperNavigation(rootRef, props2, state, onSwiperDotClick, swiperContexts, circularEnabled, swiperEnabled); } return () => { const defaultSlots = slots.default && slots.default(); swiperItems = flatVNode.flatVNode(defaultSlots); return vue.createVNode("div", { "ref": rootRef, "class": "swiper-next" }, [vue.createVNode("div", { "ref": slidesWrapperRef, "class": "swiper-next__wrapper" }, [vue.createVNode("div", { "class": "swiper-next__slides", "style": slidesStyle.value }, [vue.createVNode("div", { "ref": slideFrameRef, "class": "swiper-next__slide-frame", "style": slideFrameStyle.value }, [defaultSlots])]), props2.indicatorDots && vue.createVNode("div", { "class": ["swiper-next__dots", props2.vertical ? "swiper-next__dots-vertical" : "swiper-next__dots-horizontal"] }, [swiperContexts.value.map((_, index, array) => vue.createVNode("div", { "onClick": () => onSwiperDotClick(index), "class": { "swiper-next__dot": true, "swiper-next__dot-active": index < state.current + state.displayMultipleItems && index >= state.current || index < state.current + state.displayMultipleItems - array.length }, "style": { background: index === state.current ? props2.indicatorActiveColor : props2.indicatorColor } }, null))]), createNavigationTsx()])]); }; } }); const useSwiperNavigation = (rootRef, props2, state, onSwiperDotClick, swiperContext, circularEnabled, swiperEnabled) => { let isNavigationAuto = false; let prevDisabled = false; let nextDisabled = false; const hideNavigation = vue.ref(false); vue.watchEffect(() => { isNavigationAuto = props2.navigation === "auto"; hideNavigation.value = props2.navigation !== true || isNavigationAuto; swiperAddMouseEvent(); }); vue.watchEffect(() => { const swiperItemLength = swiperContext.value.length; const notCircular = !circularEnabled.value; prevDisabled = state.current === 0 && notCircular; nextDisabled = state.current === swiperItemLength - 1 && notCircular || notCircular && state.current + state.displayMultipleItems >= swiperItemLength; if (!swiperEnabled.value) { prevDisabled = true; nextDisabled = true; isNavigationAuto && (hideNavigation.value = true); } }); function navigationHover(event, type) { const target = event.currentTarget; if (!target) return; target.style.backgroundColor = type === "over" ? props2.navigationActiveColor : ""; } const navigationAttr = { onMouseover: (event) => navigationHover(event, "over"), onMouseout: (event) => navigationHover(event, "out") }; function navigationClick($event, type, disabled) { $event.stopPropagation(); if (disabled) return; const swiperItemLength = swiperContext.value.length; let innerCurrent = state.current; switch (type) { case "prev": innerCurrent--; if (innerCurrent < 0 && circularEnabled.value) { innerCurrent = swiperItemLength - 1; } break; case "next": innerCurrent++; if (innerCurrent >= swiperItemLength && circularEnabled.value) { innerCurrent = 0; } break; } onSwiperDotClick(innerCurrent); } const createNavigationSVG = () => icon.createSvgIconVNode(icon.ICON_PATH_BACK, props2.navigationColor, 26); let setHideNavigationTimer; const innerMousemove = (e) => { clearTimeout(setHideNavigationTimer); const { clientX, clientY } = e; const { left, right, top, bottom, width, height } = rootRef.value.getBoundingClientRect(); let hide = false; if (props2.vertical) { hide = !(clientY - top < height / 3 || bottom - clientY < height / 3); } else { hide = !(clientX - left < width / 3 || right - clientX < width / 3); } if (hide) { return setHideNavigationTimer = setTimeout(() => { hideNavigation.value = hide; }, 300); } hideNavigation.value = hide; }; const innerMouseleave = () => { hideNavigation.value = true; }; function swiperAddMouseEvent() { if (rootRef.value) { rootRef.value.removeEventListener("mousemove", innerMousemove); rootRef.value.removeEventListener("mouseleave", innerMouseleave); if (isNavigationAuto) { rootRef.value.addEventListener("mousemove", innerMousemove); rootRef.value.addEventListener("mouseleave", innerMouseleave); } } } vue.onMounted(swiperAddMouseEvent); function createNavigationTsx() { const navigationClass = { "swiper-next__navigation-hide": hideNavigation.value, "swiper-next__navigation-vertical": props2.vertical }; if (props2.navigation) { return vue.createVNode(vue.Fragment, null, [vue.createVNode("div", vue.mergeProps({ "class": ["swiper-next__navigation swiper-next__navigation-prev", shared.extend({ "swiper-next__navigation-disabled": prevDisabled }, navigationClass)], "onClick": (e) => navigationClick(e, "prev", prevDisabled) }, navigationAttr), [createNavigationSVG()]), vue.createVNode("div", vue.mergeProps({ "class": ["swiper-next__navigation swiper-next__navigation-next", shared.extend({ "swiper-next__navigation-disabled": nextDisabled }, navigationClass)], "onClick": (e) => navigationClick(e, "next", nextDisabled) }, navigationAttr), [createNavigationSVG()])]); } return null; } return createNavigationTsx; }; module.exports = _Swiper;