UNPKG

@opensig/opendesign

Version:

<p align="center"><img src="../docs/public/opendesign-logo-light.png"/></p> <h1 align="center">opendesign</h1> <p align="center">一个 Vue 3 组件库</p> <p align="center"><b>皮肤可定制,使用 TypeScript</b></p>

386 lines (385 loc) 13 kB
"use strict"; const vue = require("vue"); require("../scrollbar/index.js"); const types = require("./types.js"); const is = require("../_utils/is.js"); const onResize = require("../directives/on-resize.js"); const helper = require("../_utils/helper.js"); const vScrollbar = require("../scrollbar/vScrollbar.js"); const _hoisted_1 = { class: "o-virtual-list" }; const _hoisted_2 = { key: 1, class: "o-virtual-render-item" }; const _sfc_main = /* @__PURE__ */ vue.defineComponent({ __name: "OVirtualList", props: types.virtualListProps, emits: ["renderChange"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emits = __emit; const scrollbarProps = vue.computed(() => { if (props.scrollbar === true) { return { showType: "always", size: "medium" }; } return props.scrollbar; }); const listData = vue.ref([]); vue.watch( () => props.list, (value) => { listData.value = value.map((item, index) => ({ id: item.id, data: item, index })); }, { immediate: true } ); const defaultStartIndex = vue.computed(() => { if (is.isUndefined(props.defaultStartIndex)) { return 0; } return Math.max(Math.min(props.defaultStartIndex, props.list.length - 1), 0); }); const visibleStartIndex = vue.ref(defaultStartIndex.value ?? 0); let visibleStartId; const renderCount = vue.ref(1); const startIndex = vue.computed(() => { return Math.max(visibleStartIndex.value - props.buffer, 0); }); const endIndex = vue.computed(() => { return Math.min(visibleStartIndex.value + renderCount.value + props.buffer - 1, listData.value.length - 1); }); let lastVisibleStartIndex = visibleStartIndex.value; let lastRenderCount = renderCount.value; const emitRenderChange = () => { if (lastVisibleStartIndex !== visibleStartIndex.value || lastRenderCount !== renderCount.value) { emits("renderChange", { start: startIndex.value, end: endIndex.value, count: renderCount.value, visible: visibleStartIndex.value }); lastVisibleStartIndex = visibleStartIndex.value; lastRenderCount = renderCount.value; } }; const renderList = vue.computed(() => { return listData.value.slice(startIndex.value, endIndex.value + 1); }); vue.watch(listData, (value) => { if (!is.isUndefined(visibleStartId) && wrapperRef.value) { const top = wrapperRef.value.scrollTop - listMetaData[visibleStartIndex.value].top; const index = value.findIndex((item) => item.id === visibleStartId); if (index >= 0) { visibleStartIndex.value = index; wrapperRef.value.scrollTop = listMetaData[index].top + top; } } }); const wrapperRef = vue.ref(); const contentSize = vue.ref((props.itemSize ? props.itemSize : props.defaultItemSize) * listData.value.length); const containerSize = vue.ref({ height: 0, width: 0 }); const onContainerResize = () => { if (!wrapperRef.value) { return; } containerSize.value.height = wrapperRef.value.offsetHeight; containerSize.value.width = wrapperRef.value.offsetWidth; if (containerSize.value.height === 0) { offset.value = 0; } if (!initialScroll) { if (contentSize.value < containerSize.value.height) { visibleStartIndex.value = 0; } return; } const scrollTop = wrapperRef.value.scrollTop; for (let i = visibleStartIndex.value; i >= 0; i--) { const meta = listMetaData[i]; if (meta.top <= scrollTop) { visibleStartIndex.value = i; break; } } let count = renderCount.value; for (let i = endIndex.value; i < listMetaData.length; i++) { const meta = listMetaData[i]; if (meta.top < scrollTop + containerSize.value.height) { count++; } } renderCount.value = count; emitRenderChange(); }; const contentStyle = vue.computed(() => ({ "--content-height": `${contentSize.value}px` })); const offset = vue.ref(0); const renderListStyle = vue.computed(() => { return { "--offsetY": `${offset.value}px` }; }); let initialScroll = props.itemSize ? true : false; const scrollToView = (index, align = "start", behavior = "instant") => { if (!wrapperRef.value) { return; } const toIndex = Math.max(Math.min(listMetaData.length - 1, index), 0); const item = listMetaData[toIndex]; const itemTop = item.top; const cSize = wrapperRef.value.offsetHeight; let _align = align; if (_align === "nearest") { const currScrollTop = wrapperRef.value.scrollTop; if (currScrollTop > itemTop) { _align = "start"; } else if (currScrollTop + cSize < itemTop) { _align = "end"; } else { return; } } let scrollTop = itemTop; if (_align !== "start") { const itemSize = listMetaData[toIndex].size; if (_align === "center") { scrollTop = itemTop - cSize / 2 + itemSize / 2; } else if (_align === "end") { scrollTop = itemTop - cSize + itemSize; } else if (typeof _align === "number") { scrollTop = itemTop - _align; } } wrapperRef.value.scrollTo({ top: scrollTop, behavior: props.itemSize ? behavior : "instant" }); }; let listMetaData = []; vue.watch( [() => props.itemSize, () => listData.value], ([propSize, dataList]) => { const itemSize = propSize ? propSize : props.defaultItemSize; let lastTop = 0; listMetaData = dataList.map((item, index) => { if (!propSize) { const m = listMetaData.find((mItem) => mItem.id === item.id); if (m && m.measured) { lastTop = m.bottom; return m; } } const metaItem = { id: item.id, index, size: itemSize, top: lastTop, bottom: lastTop + itemSize, measured: propSize ? true : false, isScrolling: false }; lastTop += itemSize; return metaItem; }); contentSize.value = listMetaData[listMetaData.length - 1].bottom; }, { immediate: true } ); const updateMeta = (start = 0) => { for (let i = start + 1; i < listMetaData.length; i++) { const lastMeta = listMetaData[i - 1]; const meta = listMetaData[i]; meta.top = lastMeta.bottom; meta.bottom = meta.top + meta.size; } const last = listMetaData[listMetaData.length - 1]; last.bottom = last.top + last.size; contentSize.value = last.bottom; }; const updateVisibleCount = (scrollOffset) => { var _a; let scrollSize = scrollOffset; if (is.isUndefined(scrollSize)) { scrollSize = ((_a = wrapperRef.value) == null ? void 0 : _a.scrollTop) ?? 0; } const { height: containerHeight } = containerSize.value; if (!wrapperRef.value || !containerHeight) { return; } let render = 1; for (let i = visibleStartIndex.value + 1; i < listMetaData.length; i++) { const meta = listMetaData[i]; if (meta.top < scrollSize + containerHeight) { render++; } } renderCount.value = render; emitRenderChange(); }; const debounceUpdateVisibleCount = helper.debounceRAF(updateVisibleCount); const getStartIndex = (scrollOffset) => { let start = 0; let end = listMetaData.length - 1; while (start < end) { const mid = Math.floor((start + end) / 2); const { top, bottom } = listMetaData[mid]; if (top <= scrollOffset && bottom > scrollOffset) { return mid; } else if (bottom === scrollOffset) { return mid + 1; } else if (bottom < scrollOffset) { start = mid; } else if (top > scrollOffset) { end = mid; } } return start; }; const onScroll = () => { var _a; const scrollOffset = ((_a = wrapperRef.value) == null ? void 0 : _a.scrollTop) ?? 0; if (props.itemSize) { visibleStartIndex.value = Math.floor(scrollOffset / props.itemSize); } else { visibleStartIndex.value = getStartIndex(scrollOffset); } offset.value = listMetaData[startIndex.value].top; visibleStartId = listMetaData[visibleStartIndex.value].id; debounceUpdateVisibleCount(scrollOffset); }; const onItemResize = (en, index) => { const el = en.target; const meta = listMetaData[index]; const size = el.offsetHeight; if (meta.measured && meta.size === size) { return; } if (meta.measured === false && wrapperRef.value && wrapperRef.value.scrollTop > meta.top) { wrapperRef.value.scrollTop += size - meta.size; } meta.size = size; meta.measured = true; meta.bottom = meta.top + meta.size; updateMeta(index); if (index === defaultStartIndex.value && !initialScroll) { vue.nextTick(() => { scrollToView(defaultStartIndex.value); initialScroll = true; }); } debounceUpdateVisibleCount(); }; const init = () => { if (!wrapperRef.value) { return; } if (props.itemSize) { scrollToView(defaultStartIndex.value); } }; vue.onMounted(() => { init(); }); __expose({ scrollToView }); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ vue.withDirectives((vue.openBlock(), vue.createElementBlock( "div", { class: "o-virtual-list-wrapper", ref_key: "wrapperRef", ref: wrapperRef, onScrollPassive: onScroll }, [ vue.createElementVNode( "div", { class: "o-virtual-body", style: vue.normalizeStyle(contentStyle.value) }, [ vue.createElementVNode( "div", { class: "o-virtual-render-list", style: vue.normalizeStyle(renderListStyle.value) }, [ (vue.openBlock(true), vue.createElementBlock( vue.Fragment, null, vue.renderList(renderList.value, (item) => { return vue.openBlock(), vue.createElementBlock( vue.Fragment, { key: item.index }, [ props.itemSize ? (vue.openBlock(), vue.createElementBlock( "div", { key: 0, class: "o-virtual-render-item", style: vue.normalizeStyle({ height: props.itemSize + "px" }) }, [ vue.renderSlot(_ctx.$slots, "default", { item: item.data, index: item.index }) ], 4 /* STYLE */ )) : vue.withDirectives((vue.openBlock(), vue.createElementBlock("div", _hoisted_2, [ vue.renderSlot(_ctx.$slots, "default", { item: item.data, index: item.index }) ])), [ [vue.unref(onResize.vOnResize), (en) => onItemResize(en, item.index)] ]) ], 64 /* STABLE_FRAGMENT */ ); }), 128 /* KEYED_FRAGMENT */ )) ], 4 /* STYLE */ ) ], 4 /* STYLE */ ) ], 32 /* NEED_HYDRATION */ )), [ [vue.unref(onResize.vOnResize), onContainerResize], [vue.unref(vScrollbar.vScrollbar), scrollbarProps.value] ]) ]); }; } }); module.exports = _sfc_main;