UNPKG

vue3-seamless-scroll-modify

Version:
562 lines (533 loc) 16.9 kB
import { defineComponent, ref, computed, watch, nextTick, onBeforeMount, onMounted, createVNode, getCurrentInstance, Fragment } from 'vue'; /* eslint-disable no-undefined,no-param-reassign,no-shadow */ /** * Throttle execution of a function. Especially useful for rate limiting * execution of handlers on events like resize and scroll. * * @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) * are most useful. * @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, * as-is, to `callback` when the throttled-function is executed. * @param {object} [options] - An object to configure options. * @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds * while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed * one final time after the last throttled-function call. (After the throttled-function has not been called for * `delay` milliseconds, the internal counter is reset). * @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback * immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that * callback will never executed if both noLeading = true and noTrailing = true. * @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is * false (at end), schedule `callback` to execute after `delay` ms. * * @returns {Function} A new, throttled, function. */ function throttle (delay, callback, options) { var _ref = options || {}, _ref$noTrailing = _ref.noTrailing, noTrailing = _ref$noTrailing === void 0 ? false : _ref$noTrailing, _ref$noLeading = _ref.noLeading, noLeading = _ref$noLeading === void 0 ? false : _ref$noLeading, _ref$debounceMode = _ref.debounceMode, debounceMode = _ref$debounceMode === void 0 ? undefined : _ref$debounceMode; /* * After wrapper has stopped being called, this timeout ensures that * `callback` is executed at the proper times in `throttle` and `end` * debounce modes. */ var timeoutID; var cancelled = false; // Keep track of the last time `callback` was executed. var lastExec = 0; // Function to clear existing timeout function clearExistingTimeout() { if (timeoutID) { clearTimeout(timeoutID); } } // Function to cancel next exec function cancel(options) { var _ref2 = options || {}, _ref2$upcomingOnly = _ref2.upcomingOnly, upcomingOnly = _ref2$upcomingOnly === void 0 ? false : _ref2$upcomingOnly; clearExistingTimeout(); cancelled = !upcomingOnly; } /* * The `wrapper` function encapsulates all of the throttling / debouncing * functionality and when executed will limit the rate at which `callback` * is executed. */ function wrapper() { for (var _len = arguments.length, arguments_ = new Array(_len), _key = 0; _key < _len; _key++) { arguments_[_key] = arguments[_key]; } var self = this; var elapsed = Date.now() - lastExec; if (cancelled) { return; } // Execute `callback` and update the `lastExec` timestamp. function exec() { lastExec = Date.now(); callback.apply(self, arguments_); } /* * If `debounceMode` is true (at begin) this is used to clear the flag * to allow future `callback` executions. */ function clear() { timeoutID = undefined; } if (!noLeading && debounceMode && !timeoutID) { /* * Since `wrapper` is being called for the first time and * `debounceMode` is true (at begin), execute `callback` * and noLeading != true. */ exec(); } clearExistingTimeout(); if (debounceMode === undefined && elapsed > delay) { if (noLeading) { /* * In throttle mode with noLeading, if `delay` time has * been exceeded, update `lastExec` and schedule `callback` * to execute after `delay` ms. */ lastExec = Date.now(); if (!noTrailing) { timeoutID = setTimeout(debounceMode ? clear : exec, delay); } } else { /* * In throttle mode without noLeading, if `delay` time has been exceeded, execute * `callback`. */ exec(); } } else if (noTrailing !== true) { /* * In trailing throttle mode, since `delay` time has not been * exceeded, schedule `callback` to execute `delay` ms after most * recent execution. * * If `debounceMode` is true (at begin), schedule `clear` to execute * after `delay` ms. * * If `debounceMode` is false (at end), schedule `callback` to * execute after `delay` ms. */ timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === undefined ? delay - elapsed : delay); } } wrapper.cancel = cancel; // Return the wrapper function. return wrapper; } function useExpose(apis) { const instance = getCurrentInstance(); if (instance) { Object.assign(instance.proxy, apis); } } const Props = { // 是否开启自动滚动 modelValue: { type: Boolean, default: true }, // 原始数据列表 list: { type: Array, required: true, default: [] }, // 步进速度,step 需是单步大小的约数 step: { type: Number, default: 1 }, // 开启滚动的数据量 limitScrollNum: { type: Number, default: 3 }, // 是否开启鼠标悬停 hover: { type: Boolean, default: false }, // 控制滚动方向 direction: { type: String, default: "up" }, // 单步运动停止的高度 singleHeight: { type: Number, default: 0 }, // 单步运动停止的宽度 singleWidth: { type: Number, default: 0 }, // 单步停止等待时间 (默认值 1000ms) singleWaitTime: { type: Number, default: 1000 }, // 是否开启 rem 度量 isRemUnit: { type: Boolean, default: false }, // 开启数据更新监听 isWatch: { type: Boolean, default: true }, // 动画时间 delay: { type: Number, default: 0 }, // 动画方式 ease: { type: [String, Object], default: "ease-in" }, // 动画循环次数,-1 表示一直动画 count: { type: Number, default: -1 }, // 拷贝几份滚动列表 copyNum: { type: Number, default: 1 }, // 开启鼠标悬停时支持滚轮滚动 wheel: { type: Boolean, default: false }, // 启用单行滚动 singleLine: { type: Boolean, default: false } }; globalThis.window.cancelAnimationFrame = function () { return globalThis.window.cancelAnimationFrame || // @ts-ignore globalThis.window.webkitCancelAnimationFrame || // @ts-ignore globalThis.window.mozCancelAnimationFrame || // @ts-ignore globalThis.window.oCancelAnimationFrame || // @ts-ignore globalThis.window.msCancelAnimationFrame || function (id) { return globalThis.window.clearTimeout(id); }; }(); globalThis.window.requestAnimationFrame = function () { return globalThis.window.requestAnimationFrame || // @ts-ignore globalThis.window.webkitRequestAnimationFrame || // @ts-ignore globalThis.window.mozRequestAnimationFrame || // @ts-ignore globalThis.window.oRequestAnimationFrame || // @ts-ignore globalThis.window.msRequestAnimationFrame || function (callback) { return globalThis.window.setTimeout(callback, 1000 / 60); }; }(); function dataWarm(list) { if (list && typeof list !== "boolean" && list.length > 100) { console.warn(`数据达到了${list.length}条有点多哦~,可能会造成部分老旧浏览器卡顿。`); } } const Vue3SeamlessScroll = defineComponent({ name: "vue3-seamless-scroll", inheritAttrs: false, props: Props, emits: ["stop", "count", "move"], setup(_props, { slots, emit, attrs }) { const props = _props; const scrollRef = ref(null); const slotListRef = ref(null); const realBoxRef = ref(null); const reqFrame = ref(null); const singleWaitTimeout = ref(null); const realBoxWidth = ref(0); const realBoxHeight = ref(0); const xPos = ref(0); const yPos = ref(0); const isHover = ref(false); const _count = ref(0); const isScroll = computed(() => props.list ? props.list.length >= props.limitScrollNum : false); const realBoxStyle = computed(() => { return { width: realBoxWidth.value ? `${realBoxWidth.value}px` : "auto", transform: `translate(${xPos.value}px,${yPos.value}px)`, // @ts-ignore transition: `all ${typeof props.ease === "string" ? props.ease : "cubic-bezier(" + props.ease.x1 + "," + props.ease.y1 + "," + props.ease.x2 + "," + props.ease.y2 + ")"} ${props.delay}ms`, overflow: "hidden", display: props.singleLine ? "flex" : "block" }; }); const isHorizontal = computed(() => props.direction == "left" || props.direction == "right"); const floatStyle = computed(() => { return isHorizontal.value ? { float: "left", overflow: "hidden", display: props.singleLine ? "flex" : "block", flexShrink: props.singleLine ? 0 : 1 } : { overflow: "hidden" }; }); const baseFontSize = computed(() => { return props.isRemUnit ? parseInt(globalThis.window.getComputedStyle(globalThis.document.documentElement, null).fontSize) : 1; }); const realSingleStopWidth = computed(() => props.singleWidth * baseFontSize.value); const realSingleStopHeight = computed(() => props.singleHeight * baseFontSize.value); const step = computed(() => { let singleStep; let _step = props.step; if (isHorizontal.value) { singleStep = realSingleStopWidth.value; } else { singleStep = realSingleStopHeight.value; } if (singleStep > 0 && singleStep % _step > 0) { console.error("如果设置了单步滚动,step 需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~"); } return _step; }); const cancle = () => { cancelAnimationFrame(reqFrame.value); reqFrame.value = null; }; const animation = (_direction, _step, isWheel) => { reqFrame.value = requestAnimationFrame(function () { const h = realBoxHeight.value / 2; const w = realBoxWidth.value / 2; if (_direction === "up") { if (Math.abs(yPos.value) >= h) { yPos.value = 0; _count.value += 1; emit("count", _count.value); } yPos.value -= _step; } else if (_direction === "down") { if (yPos.value >= 0) { yPos.value = h * -1; _count.value += 1; emit("count", _count.value); } yPos.value += _step; } else if (_direction === "left") { if (Math.abs(xPos.value) >= w) { xPos.value = 0; _count.value += 1; emit("count", _count.value); } xPos.value -= _step; } else if (_direction === "right") { if (xPos.value >= 0) { xPos.value = w * -1; _count.value += 1; emit("count", _count.value); } xPos.value += _step; } if (isWheel) { return; } let { singleWaitTime } = props; if (singleWaitTimeout.value) { clearTimeout(singleWaitTimeout.value); } if (!!realSingleStopHeight.value) { if (Math.abs(yPos.value) % realSingleStopHeight.value < _step) { singleWaitTimeout.value = setTimeout(() => { move(); }, singleWaitTime); } else { move(); } } else if (!!realSingleStopWidth.value) { if (Math.abs(xPos.value) % realSingleStopWidth.value < _step) { singleWaitTimeout.value = setTimeout(() => { move(); }, singleWaitTime); } else { move(); } } else { move(); } }); }; // 回到初始位置 const move = () => { cancle(); if (isHover.value || !isScroll.value || _count.value === props.count) { emit("stop", _count.value); _count.value = 0; return; } animation(props.direction, step.value, false); }; const initMove = () => { dataWarm(props.list); if (isHorizontal.value) { let slotListWidth = slotListRef.value.offsetWidth; slotListWidth = slotListWidth * 2 + 1; realBoxWidth.value = slotListWidth; } if (isScroll.value) { realBoxHeight.value = realBoxRef.value.offsetHeight; if (props.modelValue) { move(); } } else { cancle(); yPos.value = xPos.value = 0; } }; const startMove = () => { isHover.value = false; move(); }; const stopMove = () => { isHover.value = true; if (singleWaitTimeout.value) { clearTimeout(singleWaitTimeout.value); } cancle(); }; const hoverStop = computed(() => props.hover && props.modelValue && isScroll.value); const throttleFunc = throttle(30, e => { cancle(); const singleHeight = !!realSingleStopHeight.value ? realSingleStopHeight.value : 15; if (e.deltaY < 0) { animation("down", singleHeight, true); } if (e.deltaY > 0) { animation("up", singleHeight, true); } }); const onWheel = e => { throttleFunc(e); }; const reset = () => { cancle(); isHover.value = false; initMove(); }; const Reset = () => { reset(); }; useExpose({ Reset }); watch(() => props.list, () => { if (props.isWatch) { nextTick(() => { reset(); }); } }, { deep: true }); watch(() => props.modelValue, newValue => { if (newValue) { startMove(); } else { stopMove(); } }); watch(() => props.count, newValue => { if (newValue !== 0) { startMove(); } }); onBeforeMount(() => { cancle(); clearTimeout(singleWaitTimeout.value); }); onMounted(() => { if (isScroll.value) { initMove(); } }); const { default: $default, html } = slots; const copyNum = new Array(props.copyNum).fill(null); const getHtml = () => { return createVNode(Fragment, null, [createVNode("div", { "ref": slotListRef, "style": floatStyle.value }, [$default && $default()]), isScroll.value ? copyNum.map(() => { if (html && typeof html === "function") { return createVNode("div", { "style": floatStyle.value }, [html()]); } else { return createVNode("div", { "style": floatStyle.value }, [$default && $default()]); } }) : null]); }; return () => createVNode("div", { "ref": scrollRef, "class": attrs.class }, [props.wheel && props.hover ? createVNode("div", { "ref": realBoxRef, "style": realBoxStyle.value, "onMouseenter": () => { if (hoverStop.value) { stopMove(); } }, "onMouseleave": () => { if (hoverStop.value) { startMove(); } }, "onWheel": e => { if (hoverStop.value) { onWheel(e); } } }, [getHtml()]) : createVNode("div", { "ref": realBoxRef, "style": realBoxStyle.value, "onMouseenter": () => { if (hoverStop.value) { stopMove(); } }, "onMouseleave": () => { if (hoverStop.value) { startMove(); } } }, [getHtml()])]); } }); const install = function (app, options = {}) { app.component(options.name || Vue3SeamlessScroll.name, Vue3SeamlessScroll); }; function index (app) { app.use(install); } export { Vue3SeamlessScroll, index as default };