UNPKG

@opentiny/vue-renderless

Version:

An enterprise-class UI component library, support both Vue.js 2 and Vue.js 3, as well as PC and mobile.

487 lines (486 loc) 17.4 kB
import "../chunk-G2ADBYYC.js"; import { getScrollContainer } from "@opentiny/utils"; import { isNull } from "@opentiny/utils"; let supportsPassive = false; if (typeof window !== "undefined") { supportsPassive = false; try { const opts = Object.defineProperty({}, "passive", { get() { supportsPassive = true; } }); window.addEventListener("test", null, opts); } catch (e) { } } const handleVisibilityChange = ({ api, emit, state }) => (isVisible, entry) => { if (state.ready) { if (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0) { emit("visible"); requestAnimationFrame(() => { api.updateVisibleItems(false); }); } else { emit("hidden"); } } }; const init = ({ api }) => () => { api.resetTemporary(); api.updateVisibleItems(true); }; const updateVisibleItems = ({ api, emit, props, state, vm }) => (checkItem, checkPositionDiff = false) => { const itemSize = props.itemSize; const gridItems = props.gridItems || 1; const itemSecondarySize = props.itemSecondarySize || itemSize; const minItemSize = state.temporary.computedMinItemSize; const typeField = props.typeField; const keyField = state.simpleArray ? null : props.keyField; const items = props.items; const count = items.length; const sizes = state.sizes; const views = state.temporary.views; const unusedViews = state.temporary.unusedViews; const pool = state.pool; const itemIndexByKey = state.itemIndexByKey; let startIndex, endIndex, visibleStartIndex, visibleEndIndex, totalSize; if (!count) { startIndex = endIndex = visibleStartIndex = visibleEndIndex = totalSize = 0; } else if (state.temporary.prerender) { startIndex = visibleStartIndex = 0; visibleEndIndex = endIndex = Math.min(props.prerender, items.length); totalSize = null; } else { const scroll = api.getScroll(); if (doCheckPositionDiff({ checkPositionDiff, scroll, state, itemSize, minItemSize })) { return { continuous: true }; } state.temporary.lastUpdateScrollPosition = scroll.start; const args2 = { props, scroll, vm, itemSize, count, sizes, startIndex, totalSize }; Object.assign(args2, { endIndex, items, visibleStartIndex, visibleEndIndex, gridItems }); const ret = computeRange(args2); startIndex = ret.startIndex; endIndex = ret.endIndex; visibleStartIndex = ret.visibleStartIndex; visibleEndIndex = ret.visibleEndIndex; totalSize = ret.totalSize; } if (endIndex - startIndex > props.itemsLimit) { throw new Error("[TINY Error][RecycleScroller] Rendered items limit reached"); } state.totalSize = totalSize; const continuous = startIndex <= state.temporary.endIndex && endIndex >= state.temporary.startIndex; const args = { continuous, pool, checkItem, itemIndexByKey, keyField, startIndex }; Object.assign(args, { endIndex, api, items, views, itemSize, sizes, typeField, unusedViews }); Object.assign(args, { emit, gridItems, itemSecondarySize }); computePool(args); state.temporary.startIndex = startIndex; state.temporary.endIndex = endIndex; if (props.emitUpdate) { emit("update", startIndex, endIndex, visibleStartIndex, visibleEndIndex); } clearTimeout(state.temporary.sortTimer); state.temporary.sortTimer = setTimeout(api.sortViews, props.updateInterval + 300); return { continuous }; }; const computedSizes = ({ props, state }) => () => { if (props.itemSize === null) { const sizes = { "-1": { accumulator: 0 } }; const items = props.items; const field = props.sizeField; const minItemSize = props.minItemSize; let computedMinItemSize = 1e4; let accumulator = 0; let current; for (let i = 0, len = items.length; i < len; i++) { current = items[i][field] || minItemSize; if (current < computedMinItemSize) { computedMinItemSize = current; } accumulator += current; sizes[i] = { accumulator, size: current }; } state.temporary.computedMinItemSize = computedMinItemSize; return sizes; } return []; }; const computedItemIndexByKey = (props) => () => { const { keyField, items } = props; const result = {}; for (let i = 0, l = items.length; i < l; i++) { result[items[i][keyField]] = i; } return result; }; const getScroll = ({ props, vm }) => () => { const { $el: el } = vm; const direction = props.direction; const isVertical = direction === "vertical"; let scrollRange; if (props.pageMode) { const boundRect = el.getBoundingClientRect(); const boundsSize = isVertical ? boundRect.height : boundRect.width; let size = isVertical ? window.innerHeight : window.innerWidth; let start = -(isVertical ? boundRect.top : boundRect.left); if (start < 0) { size += start; start = 0; } if (start + size > boundsSize) { size = boundsSize - start; } scrollRange = { start, end: start + size }; } else if (isVertical) { scrollRange = { start: el.scrollTop, end: el.scrollTop + el.clientHeight }; } else { scrollRange = { start: el.scrollLeft, end: el.scrollLeft + el.clientWidth }; } return scrollRange; }; const unuseView = (state) => (view, fake = false) => { const unusedViews = state.temporary.unusedViews; const { type: nrType } = view.nr; let unusedPool = unusedViews.get(nrType); if (!unusedPool) { unusedPool = []; unusedViews.set(nrType, unusedPool); } unusedPool.push(view); if (!fake) { view.nr.used = false; view.position = -9999; } }; let uid = 0; const addView = ({ markRaw, shallowReactive }) => (pool, index, item, key, type) => { const nr = markRaw({ id: uid++, index, used: true, key, type }); const view = shallowReactive({ item, position: 0, nr }); pool.push(view); return view; }; const sortViews = (state) => () => { state.pool.sort((viewA, viewB) => viewA.nr.index - viewB.nr.index); }; const handleScroll = ({ api, props, state }) => () => { if (!state.temporary.scrollDirty) { state.temporary.scrollDirty = true; if (state.temporary.updateTimeout) return; const requestUpdate = () => requestAnimationFrame(() => { state.temporary.scrollDirty = false; const { continuous } = api.updateVisibleItems(false, true); if (!continuous) { cancelAnimationFrame(state.temporary.refreshTimeout); state.temporary.refreshTimeout = requestAnimationFrame(() => api.updateVisibleItems(false)); } }); requestUpdate(); if (props.updateInterval) { state.temporary.updateTimeout = setTimeout(() => { state.temporary.updateTimeout = 0; if (state.temporary.scrollDirty) requestUpdate(); }, props.updateInterval); } } }; const handleResize = ({ api, emit, state }) => () => { emit("resize"); if (state.ready) api.updateVisibleItems(false); }; const applyPageMode = ({ api, props }) => () => { if (props.pageMode) { api.addListeners(); } else { api.removeListeners(); } }; const addListeners = ({ api, state }) => () => { state.listenerTarget = api.getListenerTarget(); const options = supportsPassive ? { passive: true } : false; state.listenerTarget.addEventListener("scroll", api.handleScroll, options); state.listenerTarget.addEventListener("resize", api.handleResize); }; const removeListeners = ({ api, state }) => () => { if (!state.listenerTarget) { return; } state.listenerTarget.removeEventListener("scroll", api.handleScroll); state.listenerTarget.removeEventListener("resize", api.handleResize); state.listenerTarget = null; }; const getListenerTarget = ({ props, vm }) => () => { let target = getScrollContainer(vm.$el.parentNode, props.direction === "vertical"); if (window.document && (target === window.document.documentElement || target === window.document.body)) { target = window; } return target; }; const scrollToPosition = ({ props, vm }) => (position) => { const direction = props.direction === "vertical" ? { scroll: "scrollTop", start: "top" } : { scroll: "scrollLeft", start: "left" }; let viewport, scrollDirection, scrollDistance; if (props.pageMode) { const viewportEl = getScrollContainer(vm.$el.parentNode, props.direction === "vertical"); const scrollTop = viewportEl.tagName === "HTML" ? 0 : viewportEl[direction.scroll]; const bounds = viewportEl.getBoundingClientRect(); const scroller = vm.$el.getBoundingClientRect(); const scrollerPosition = scroller[direction.start] - bounds[direction.start]; viewport = viewportEl; scrollDirection = direction.scroll; scrollDistance = position + scrollTop + scrollerPosition; } else { viewport = vm.$el; scrollDirection = direction.scroll; scrollDistance = position; } viewport[scrollDirection] = scrollDistance; }; const scrollToItem = ({ api, props, state }) => (index) => { const gridItems = props.gridItems || 1; let scroll; if (props.itemSize === null) { scroll = index > 0 ? state.sizes[index - 1].accumulator : 0; } else { scroll = Math.floor(index / gridItems) * props.itemSize; } api.scrollToPosition(scroll); }; const doCheckPositionDiff = ({ checkPositionDiff, scroll, state, itemSize, minItemSize }) => { if (checkPositionDiff) { let positionDiff = scroll.start - state.temporary.lastUpdateScrollPosition; if (positionDiff < 0) positionDiff = -positionDiff; if (itemSize === null && positionDiff < minItemSize || positionDiff < itemSize) { return true; } } }; const computeRange = (args) => { let { props, scroll, vm, itemSize, count, sizes, startIndex, totalSize } = args; let { endIndex, items, visibleStartIndex, visibleEndIndex, gridItems } = args; const buffer = props.buffer; let beforeSize = 0; scroll.start -= buffer; scroll.end += buffer; if (vm.$refs.before) { beforeSize = vm.$refs.before.scrollHeight; scroll.start -= beforeSize; } if (vm.$refs.after) { const afterSize = vm.$refs.after.scrollHeight; scroll.end += afterSize; } if (itemSize === null) { let args2 = { count, sizes, scroll, startIndex, totalSize, endIndex }; Object.assign(args2, { items, visibleStartIndex, beforeSize, visibleEndIndex }); const ret = computeRangeVariableSizeMode(args2); startIndex = ret.startIndex; endIndex = ret.endIndex; visibleStartIndex = ret.visibleStartIndex; visibleEndIndex = ret.visibleEndIndex; totalSize = ret.totalSize; } else { startIndex = ~~(scroll.start / itemSize * gridItems); const remainer = startIndex % gridItems; startIndex -= remainer; endIndex = Math.ceil(scroll.end / itemSize * gridItems); visibleStartIndex = Math.max(0, Math.floor((scroll.start - beforeSize) / itemSize * gridItems)); visibleEndIndex = Math.floor((scroll.end - beforeSize) / itemSize * gridItems); startIndex < 0 && (startIndex = 0); endIndex > count && (endIndex = count); visibleStartIndex < 0 && (visibleStartIndex = 0); visibleEndIndex > count && (visibleEndIndex = count); totalSize = Math.ceil(count / gridItems) * itemSize; } return { startIndex, endIndex, visibleStartIndex, visibleEndIndex, totalSize }; }; const computeRangeVariableSizeMode = (args) => { let { count, sizes, scroll, startIndex, totalSize, endIndex } = args; let { items, visibleStartIndex, beforeSize, visibleEndIndex } = args; let height, lower, upper, cursor, oldCursor; lower = 0; upper = count - 1; cursor = ~~(count / 2); do { oldCursor = cursor; height = sizes[cursor].accumulator; if (height < scroll.start) { lower = cursor; } else if (cursor < count - 1 && sizes[cursor + 1].accumulator > scroll.start) { upper = cursor; } cursor = ~~((lower + upper) / 2); } while (cursor !== oldCursor); cursor < 0 && (cursor = 0); startIndex = cursor; totalSize = sizes[count - 1].accumulator; for (endIndex = cursor; endIndex < count && sizes[endIndex].accumulator < scroll.end; endIndex++) { } if (endIndex === -1) { endIndex = items.length - 1; } else { endIndex++; endIndex > count && (endIndex = count); } for (visibleStartIndex = startIndex; visibleStartIndex < count && beforeSize + sizes[visibleStartIndex].accumulator < scroll.start; visibleStartIndex++) { } for (visibleEndIndex = visibleStartIndex; visibleEndIndex < count && beforeSize + sizes[visibleEndIndex].accumulator < scroll.end; visibleEndIndex++) { } return { startIndex, totalSize, endIndex, visibleStartIndex, visibleEndIndex }; }; const computePool = (args) => { let { continuous, pool, checkItem, itemIndexByKey, keyField, startIndex } = args; let { endIndex, api, items, views, itemSize, sizes, typeField, unusedViews } = args; let { emit, gridItems, itemSecondarySize } = args; let view = unuseInvisible({ continuous, pool, checkItem, itemIndexByKey, keyField, startIndex, endIndex, api }); const unusedIndex = continuous ? null : /* @__PURE__ */ new Map(); let item, type; for (let i = startIndex; i < endIndex; i++) { item = items[i]; const key = keyField ? item[keyField] : item; if (isNull(key)) { throw new Error(`[TINY Error][RecycleScroller] Key is ${key} on item (keyField is '${keyField}')`); } view = views.get(key); if (!itemSize && !sizes[i].size) { if (view) api.unuseView(view); continue; } type = item[typeField]; let unusedPool = unusedViews.get(type); let newlyUsedView = false; let args2 = { view, continuous, unusedPool, api, pool, i, item }; Object.assign(args2, { key, type, unusedIndex, unusedViews, views, newlyUsedView }); const ret = computePoolView(args2); view = ret.view; unusedPool = ret.unusedPool; newlyUsedView = ret.newlyUsedView; if (newlyUsedView) { if (i === items.length - 1) emit("scroll-end"); if (i === 0) emit("scroll-start"); } if (itemSize === null) { view.position = sizes[i - 1].accumulator; view.offset = 0; } else { view.position = Math.floor(i / gridItems) * itemSize; view.offset = i % gridItems * itemSecondarySize; } } }; const unuseInvisible = ({ continuous, pool, checkItem, itemIndexByKey, keyField, startIndex, endIndex, api }) => { let view; if (continuous) { for (let i = 0, l = pool.length; i < l; i++) { view = pool[i]; if (view.nr.used) { if (checkItem) { view.nr.index = itemIndexByKey[view.item[keyField]]; } if (isNull(view.nr.index) || view.nr.index < startIndex || view.nr.index >= endIndex) { api.unuseView(view); } } } } return view; }; const computePoolView = (args) => { let { view, continuous, unusedPool, api, pool, i, item } = args; let { key, type, unusedIndex, unusedViews, views, newlyUsedView } = args; let v; if (!view) { if (continuous) { if (unusedPool && unusedPool.length) { view = unusedPool.pop(); } else { view = api.addView(pool, i, item, key, type); } } else { v = unusedIndex.get(type) || 0; if (!unusedPool || v >= unusedPool.length) { view = api.addView(pool, i, item, key, type); api.unuseView(view, true); unusedPool = unusedViews.get(type); } view = unusedPool[v]; unusedIndex.set(type, v + 1); } views.delete(view.nr.key); view.nr.used = true; view.nr.index = i; view.nr.key = key; view.nr.type = type; views.set(key, view); newlyUsedView = true; } else { if (!view.nr.used) { view.nr.used = true; newlyUsedView = true; if (unusedPool) { const index = unusedPool.indexOf(view); if (~index) unusedPool.splice(index, 1); } } } view.item = item; return { view, unusedPool, newlyUsedView }; }; const computeViewStyle = ({ props, state }) => (view) => { const { direction, gridItems, itemSecondarySize, itemSize } = props; let transform = `translate${direction === "vertical" ? "Y" : "X"}(${view.position}px)`; transform = `${transform} translate${direction === "vertical" ? "X" : "Y"}(${view.offset}px)`; let width = gridItems ? `${direction === "vertical" ? itemSecondarySize || itemSize : itemSize}px` : void 0; let height = gridItems ? `${direction === "horizontal" ? itemSecondarySize || itemSize : itemSize}px` : void 0; return state.ready ? { transform, width, height } : null; }; const computeViewEvent = ({ props, state }) => (view) => { if (props.skipHover) { return {}; } else { return { mouseenter: () => state.hoverKey = view.nr.key, mouseleave: () => state.hoverKey = null }; } }; const resetTemporary = ({ state }) => () => { state.temporary = { startIndex: 0, endIndex: 0, views: /* @__PURE__ */ new Map(), unusedViews: /* @__PURE__ */ new Map(), scrollDirty: false, lastUpdateScrollPosition: 0 }; state.pool = []; }; export { addListeners, addView, applyPageMode, computeViewEvent, computeViewStyle, computedItemIndexByKey, computedSizes, getListenerTarget, getScroll, handleResize, handleScroll, handleVisibilityChange, init, removeListeners, resetTemporary, scrollToItem, scrollToPosition, sortViews, unuseView, updateVisibleItems };