UNPKG

vue-virtual-list-observer

Version:

keyword: **intersectionObserver scroll-list Scroll animation,quick scroll resolved**

440 lines (439 loc) 13.8 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { openBlock, createElementBlock, Fragment, renderList, renderSlot } from "vue"; class ListScroll { constructor(options) { __publicField(this, "getWindowFirstIndex", (isScrollDown) => { const { currentIndex } = this.domDataCache; const increment = Math.floor(this.listSize / 2); let firstIndex; if (isScrollDown) { firstIndex = currentIndex + increment; } else { firstIndex = currentIndex - increment; } if (firstIndex < 0) { firstIndex = 0; } return firstIndex; }); this.opsCheck(options); } opsCheck(ops) { if (!ops || typeof ops !== "object") { throw new Error("options are illegal"); } const { itemHeight, container, listSize, renderFunction, getDomHeightFunction, _setScoll, scrollDom, scrollAnima } = ops; if (!itemHeight || typeof itemHeight !== "number") { throw new Error("itemHeight is illegal"); } if (!renderFunction || typeof renderFunction !== "function") { throw new Error("lastItemId is illegal"); } if (!listSize) { throw new Error("listSize is illegal"); } if (!container || !container.nodeType) { throw new Error("root is illegal"); } this.itemHeight = itemHeight; this.container = container; this.listSize = listSize; this.renderFunction = renderFunction; this.getDomHeightFunction = getDomHeightFunction; this.firstItem = document.getElementsByClassName("_first"); this.lastItem = document.getElementsByClassName("_last"); this.startEntry = 0; this._setScoll = _setScoll; this.scrollDom = scrollDom; this.scrollAnima = scrollAnima; this.domDataCache = { currentPaddingTop: 0, currentPaddingBottom: 0, topSentinelPreviousY: 0, topSentinelPreviousRatio: 0, bottomSentinelPreviousY: 0, bottomSentinelPreviousRatio: 0, currentIndex: 0 }; } updateDomDataCache(params) { Object.assign(this.domDataCache, params); } getArrSum(arr, start, end) { let resArr = 0; for (let i = start; i < end; i++) { resArr += arr[i]; } return resArr; } adjustPaddings(isScrollDown, firstIndex) { if (firstIndex === 0 && isScrollDown === false && this.startEntry === 0) { this.startEntry = 1; return false; } const { container, itemHeight } = this; const { currentPaddingTop, currentPaddingBottom } = this.domDataCache; let newCurrentPaddingTop, newCurrentPaddingBottom; let remPaddingsVal = itemHeight * Math.floor(this.listSize / 2); let addCount = Math.floor(this.listSize / 2); if (isScrollDown) { let domHeight = this.getDomHeightFunction(); let start = firstIndex - addCount; let end = firstIndex; remPaddingsVal = this.getArrSum(domHeight, start, end); newCurrentPaddingTop = currentPaddingTop + remPaddingsVal; if (currentPaddingBottom === 0) { newCurrentPaddingBottom = 0; } else { newCurrentPaddingBottom = currentPaddingBottom - remPaddingsVal; } } else { if (firstIndex !== 0) { let domHeight = this.getDomHeightFunction(); let start = firstIndex + this.listSize; let end = start + addCount; remPaddingsVal = this.getArrSum(domHeight, start, end); } newCurrentPaddingTop = currentPaddingTop + remPaddingsVal; newCurrentPaddingBottom = currentPaddingBottom + remPaddingsVal; if (currentPaddingTop === 0) { newCurrentPaddingTop = 0; } else { newCurrentPaddingTop = currentPaddingTop - remPaddingsVal; } } if (firstIndex === 0) { newCurrentPaddingBottom = 0; } container.style.paddingBottom = `${newCurrentPaddingBottom}px`; container.style.paddingTop = `${newCurrentPaddingTop}px`; this.updateDomDataCache({ currentPaddingTop: newCurrentPaddingTop, currentPaddingBottom: newCurrentPaddingBottom }); } async topItemCb(entry) { const { topSentinelPreviousY, topSentinelPreviousRatio } = this.domDataCache; const currentY = entry.boundingClientRect.top; const currentRatio = entry.intersectionRatio; const isIntersecting = entry.isIntersecting; if (currentY > topSentinelPreviousY && isIntersecting && currentRatio >= topSentinelPreviousRatio) { const firstIndex = this.getWindowFirstIndex(false); await this.renderFunction(firstIndex); await this.adjustPaddings(false, firstIndex); await this.updateDomDataCache({ currentIndex: firstIndex, topSentinelPreviousY: currentY, topSentinelPreviousRatio: currentRatio }); } else { this.updateDomDataCache({ topSentinelPreviousY: currentY, topSentinelPreviousRatio: currentRatio }); } } async bottomItemCb(entry) { const { bottomSentinelPreviousY, bottomSentinelPreviousRatio } = this.domDataCache; const currentY = entry.boundingClientRect.top; const currentRatio = entry.intersectionRatio; const isIntersecting = entry.isIntersecting; if (currentY < bottomSentinelPreviousY && currentRatio >= bottomSentinelPreviousRatio && isIntersecting) { const firstIndex = this.getWindowFirstIndex(true); await this.renderFunction(firstIndex); await this.adjustPaddings(true, firstIndex); await this.updateDomDataCache({ currentIndex: firstIndex, bottomSentinelPreviousY: currentY, bottomSentinelPreviousRatio: currentRatio }); } else { this.updateDomDataCache({ bottomSentinelPreviousY: currentY, bottomSentinelPreviousRatio: currentRatio }); } } initIntersectionObserver() { const options = {}; const callback = (entries) => { entries.forEach((entry) => { if (entry.target.className.split(" ").includes("_first")) { this.topItemCb(entry); } else if (entry.target.className.split(" ").includes("_last")) { this.bottomItemCb(entry); } }); }; this.observer = new IntersectionObserver(callback, options); this.observer.observe(this.firstItem[0]); this.observer.observe(this.lastItem[0]); } scrollToTop() { this.domDataCache = { currentIndex: 0, currentPaddingTop: 0, currentPaddingBottom: 0, topSentinelPreviousY: 0, topSentinelPreviousRatio: 0, bottomSentinelPreviousY: 0, bottomSentinelPreviousRatio: 0 }; if (this.scrollAnima) { this.animationToTop(this.scrollDom); } else { this.renderFunction(0); this.scrollDom.scrollTop = 0; this.container.style.paddingTop = `0px`; this.container.style.paddingBottom = `0px`; } this.startEntry = 0; this.startObserver(); } async scrollToIndex(firstIndex) { this.domDataCache; await this.renderFunction(firstIndex); await this.getPadding(firstIndex); this.updateDomDataCache({ currentIndex: firstIndex }); } getPadding(firstIndex) { let start = 0; let end = firstIndex; let domHeight = this.getDomHeightFunction(); const distance = this.getArrSum(domHeight, start, end); this.container.style.paddingBottom = `0px`; this.container.style.paddingTop = `${distance}px`; this.scrollDom.scrollTop = distance; this.updateDomDataCache({ currentPaddingTop: distance, currentPaddingBottom: 0, bottomSentinelPreviousY: 0, topSentinelPreviousY: distance }); } startObserver() { this.initIntersectionObserver(); } animationToTop(scrollDomIns) { const beginTime = Date.now(); const beginValue = scrollDomIns.scrollTop; const rAF = window.requestAnimationFrame || ((func) => setTimeout(func, 16)); const frameFunc = () => { const progress = (Date.now() - beginTime) / 500; if (progress < 1) { const cubic = (value) => Math.pow(value, 3); const easeInOutCubic = (value) => value < 0.5 ? cubic(value * 2) / 2 : 1 - cubic((1 - value) * 2) / 2; scrollDomIns.scrollTop = beginValue * (1 - easeInOutCubic(progress)); rAF(frameFunc); } else { scrollDomIns.scrollTop = 0; } }; rAF(frameFunc); } } var listDOM_vue_vue_type_style_index_0_scoped_true_lang = ""; var _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main = { name: "ListDOM", data() { return { itemsArr: null, arrDom: [], arrDomHeight: [], arrDomHeightPrefix: [], listScrollIns: null, container: null, judgeToTop: 0 }; }, emits: ["request"], props: { resArr: { type: Array, default: [], required: true }, visualDomCount: { type: Number, default: 5, required: true }, domHeight: { type: Number, default: 150 }, listHeight: { type: Number }, scrollInstance: { required: true }, scrollAnima: { type: Boolean, default: true } }, inject: ["request"], mounted() { const scrollDom = this.scrollInstance(); this.arrDom = this.resArr; this.container = document.getElementById("list"); document.querySelectorAll("#m-listContainer li"); const renderPage = async (firstIndex) => { let end = 0; let count = 0; while (firstIndex + this.visualDomCount * 5 > this.resArr.length && count < 5) { await this.request(); count++; if (this.judgeToTop === 1) { this.judgeToTop = 0; break; } } const UpdateDOMList = () => { if (firstIndex + this.visualDomCount * 5 > this.resArr.length) { end = this.resArr.length; this.arrDom = this.resArr.slice(firstIndex); } else { end = firstIndex + this.visualDomCount * 5; this.arrDom = this.resArr.slice(firstIndex, firstIndex + this.visualDomCount * 5); } this.$nextTick(() => { for (let i = firstIndex; i < end; i++) { this.arrDomHeight[i] = this.$refs.itemDom[i - firstIndex].clientHeight; } }); }; await UpdateDOMList(); }; const getDomHeight = () => { return this.arrDomHeight; }; this.$nextTick(() => { const Node = document.querySelectorAll(".list-item"); Node[0].classList.add("_first"); Node[Node.length - 1].classList.add("_last"); renderPage(0); const renderFunction = async (firstIndex) => { await renderPage(firstIndex); }; const getDomHeightFunction = () => { return getDomHeight(); }; this.listScrollIns = new ListScroll({ container: this.container, listSize: this.visualDomCount * 5, itemHeight: this.domHeight, renderFunction, getDomHeightFunction, scrollDom, scrollAnima: this.scrollAnima }); this.listScrollIns.startObserver(); }); this.MonitorScroll(); }, methods: { scrollToTop: function() { this.scrollInstance(); this.listScrollIns.scrollToTop(); this.judgeToTop = 1; }, getScroll: function() { return this.$refs.list.scrollTop; }, getSize: function() { return this.resArr.length; }, scrollToIndex: function(index) { this.listScrollIns.scrollToIndex(index); }, MonitorScroll: function() { setInterval(() => { let paddingTop = parseInt(this.container.style.paddingTop.slice(0, -2)); const scrollDom = this.scrollInstance(); let scrollVTop = scrollDom.scrollTop; if (paddingTop > scrollVTop + 200) { let [index, paddingV] = this.getIndex(scrollVTop); this.scrollToIndex(index); } else { const CHeight = this.$refs.list.clientHeight; const PBottom = parseInt(this.container.style.paddingBottom.slice(0, -2)); if (scrollVTop > CHeight - PBottom + 100) { let [index, paddingV] = this.getIndex(scrollVTop); this.scrollToIndex(index); } } }, 100); }, getIndex: function(topIns) { let count = 0; let DomIndex = 0; let DomsHeight = 0; for (let i = 0; i < this.arrDomHeight.length; i++) { count += this.arrDomHeight[i]; if (count <= topIns) { DomIndex = i; DomsHeight = count; } else { return [DomIndex, DomsHeight]; } } return [DomIndex, DomsHeight]; } } }; const _hoisted_1 = { class: "list", id: "list", ref: "list" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1, [ (openBlock(true), createElementBlock(Fragment, null, renderList($data.arrDom, (item, index) => { return openBlock(), createElementBlock("div", { class: "list-item", key: index, ref_for: true, ref: "itemDom" }, [ renderSlot(_ctx.$slots, "list", { item }, void 0, true) ]); }), 128)), renderSlot(_ctx.$slots, "footer", {}, void 0, true) ], 512); } var listDOM = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-b21dcff2"]]); export { listDOM as ListDOM }; //# sourceMappingURL=vue-virtual-list.es.js.map