@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>
387 lines (386 loc) • 13 kB
JavaScript
import { defineComponent, computed, ref, watch, onMounted, createElementBlock, openBlock, withDirectives, createElementVNode, normalizeStyle, Fragment, renderList, renderSlot, unref, nextTick } from "vue";
import "../scrollbar/index.mjs";
import { virtualListProps } from "./types.mjs";
import { isUndefined } from "../_utils/is.mjs";
import { vOnResize } from "../directives/on-resize.mjs";
import { debounceRAF } from "../_utils/helper.mjs";
import { vScrollbar } from "../scrollbar/vScrollbar.mjs";
const _hoisted_1 = { class: "o-virtual-list" };
const _hoisted_2 = {
key: 1,
class: "o-virtual-render-item"
};
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "OVirtualList",
props: virtualListProps,
emits: ["renderChange"],
setup(__props, { expose: __expose, emit: __emit }) {
const props = __props;
const emits = __emit;
const scrollbarProps = computed(() => {
if (props.scrollbar === true) {
return {
showType: "always",
size: "medium"
};
}
return props.scrollbar;
});
const listData = ref([]);
watch(
() => props.list,
(value) => {
listData.value = value.map((item, index) => ({
id: item.id,
data: item,
index
}));
},
{
immediate: true
}
);
const defaultStartIndex = computed(() => {
if (isUndefined(props.defaultStartIndex)) {
return 0;
}
return Math.max(Math.min(props.defaultStartIndex, props.list.length - 1), 0);
});
const visibleStartIndex = ref(defaultStartIndex.value ?? 0);
let visibleStartId;
const renderCount = ref(1);
const startIndex = computed(() => {
return Math.max(visibleStartIndex.value - props.buffer, 0);
});
const endIndex = 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$1 = computed(() => {
return listData.value.slice(startIndex.value, endIndex.value + 1);
});
watch(listData, (value) => {
if (!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 = ref();
const contentSize = ref((props.itemSize ? props.itemSize : props.defaultItemSize) * listData.value.length);
const containerSize = 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 = computed(() => ({
"--content-height": `${contentSize.value}px`
}));
const offset = ref(0);
const renderListStyle = 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 = [];
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 (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 = 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) {
nextTick(() => {
scrollToView(defaultStartIndex.value);
initialScroll = true;
});
}
debounceUpdateVisibleCount();
};
const init = () => {
if (!wrapperRef.value) {
return;
}
if (props.itemSize) {
scrollToView(defaultStartIndex.value);
}
};
onMounted(() => {
init();
});
__expose({
scrollToView
});
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", _hoisted_1, [
withDirectives((openBlock(), createElementBlock(
"div",
{
class: "o-virtual-list-wrapper",
ref_key: "wrapperRef",
ref: wrapperRef,
onScrollPassive: onScroll
},
[
createElementVNode(
"div",
{
class: "o-virtual-body",
style: normalizeStyle(contentStyle.value)
},
[
createElementVNode(
"div",
{
class: "o-virtual-render-list",
style: normalizeStyle(renderListStyle.value)
},
[
(openBlock(true), createElementBlock(
Fragment,
null,
renderList(renderList$1.value, (item) => {
return openBlock(), createElementBlock(
Fragment,
{
key: item.index
},
[
props.itemSize ? (openBlock(), createElementBlock(
"div",
{
key: 0,
class: "o-virtual-render-item",
style: normalizeStyle({ height: props.itemSize + "px" })
},
[
renderSlot(_ctx.$slots, "default", {
item: item.data,
index: item.index
})
],
4
/* STYLE */
)) : withDirectives((openBlock(), createElementBlock("div", _hoisted_2, [
renderSlot(_ctx.$slots, "default", {
item: item.data,
index: item.index
})
])), [
[unref(vOnResize), (en) => onItemResize(en, item.index)]
])
],
64
/* STABLE_FRAGMENT */
);
}),
128
/* KEYED_FRAGMENT */
))
],
4
/* STYLE */
)
],
4
/* STYLE */
)
],
32
/* NEED_HYDRATION */
)), [
[unref(vOnResize), onContainerResize],
[unref(vScrollbar), scrollbarProps.value]
])
]);
};
}
});
export {
_sfc_main as default
};