@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
JavaScript
;
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;