swiper-next
Version:
Vue3 的 Swiper 组件
762 lines (761 loc) • 23.9 kB
JavaScript
;
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;