vue-virtual-list-observer
Version:
keyword: **intersectionObserver scroll-list Scroll animation,quick scroll resolved**
440 lines (439 loc) • 13.8 kB
JavaScript
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