<script lang="tsx">
|
import {
|
defineComponent,
|
computed,
|
ref,
|
unref,
|
reactive,
|
onMounted,
|
watch,
|
nextTick,
|
CSSProperties,
|
PropType,
|
} from 'vue';
|
import { useEventListener } from '@/hooks/event/useEventListener';
|
import { getSlot } from '@/utils/helper/tsxHelper';
|
|
type NumberOrNumberString = PropType<string | number | undefined>;
|
|
const props = {
|
height: [Number, String] as NumberOrNumberString,
|
maxHeight: [Number, String] as NumberOrNumberString,
|
maxWidth: [Number, String] as NumberOrNumberString,
|
minHeight: [Number, String] as NumberOrNumberString,
|
minWidth: [Number, String] as NumberOrNumberString,
|
width: [Number, String] as NumberOrNumberString,
|
bench: {
|
type: [Number, String] as NumberOrNumberString,
|
default: 0,
|
},
|
itemHeight: {
|
type: [Number, String] as NumberOrNumberString,
|
required: true,
|
},
|
items: {
|
type: Array,
|
default: () => [],
|
},
|
};
|
|
const prefixCls = 'virtual-scroll';
|
|
function convertToUnit(str: string | number | null | undefined, unit = 'px'): string | undefined {
|
if (str == null || str === '') {
|
return undefined;
|
} else if (isNaN(+str!)) {
|
return String(str);
|
} else {
|
return `${Number(str)}${unit}`;
|
}
|
}
|
|
export default defineComponent({
|
name: 'VirtualScroll',
|
props,
|
setup(props, { slots, expose }) {
|
const wrapElRef = ref<HTMLDivElement | null>(null);
|
const state = reactive({
|
first: 0,
|
last: 0,
|
scrollTop: 0,
|
});
|
|
const getBenchRef = computed(() => {
|
return parseInt(props.bench as string, 10);
|
});
|
|
const getItemHeightRef = computed(() => {
|
return parseInt(props.itemHeight as string, 10);
|
});
|
|
const getFirstToRenderRef = computed(() => {
|
return Math.max(0, state.first - unref(getBenchRef));
|
});
|
|
const getLastToRenderRef = computed(() => {
|
return Math.min((props.items || []).length, state.last + unref(getBenchRef));
|
});
|
|
const getContainerStyleRef = computed((): CSSProperties => {
|
return {
|
height: convertToUnit((props.items || []).length * unref(getItemHeightRef)),
|
};
|
});
|
|
const getWrapStyleRef = computed((): CSSProperties => {
|
const styles: Record<string, any> = {};
|
const height = convertToUnit(props.height);
|
const minHeight = convertToUnit(props.minHeight);
|
const minWidth = convertToUnit(props.minWidth);
|
const maxHeight = convertToUnit(props.maxHeight);
|
const maxWidth = convertToUnit(props.maxWidth);
|
const width = convertToUnit(props.width);
|
|
if (height) styles.height = height;
|
if (minHeight) styles.minHeight = minHeight;
|
if (minWidth) styles.minWidth = minWidth;
|
if (maxHeight) styles.maxHeight = maxHeight;
|
if (maxWidth) styles.maxWidth = maxWidth;
|
if (width) styles.width = width;
|
return styles;
|
});
|
|
watch([() => props.itemHeight, () => props.height], () => {
|
onScroll();
|
});
|
|
function getLast(first: number): number {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return 0;
|
}
|
const height = parseInt(props.height || 0, 10) || wrapEl.clientHeight;
|
|
return first + Math.ceil(height / unref(getItemHeightRef));
|
}
|
|
function getFirst(): number {
|
return Math.floor(state.scrollTop / unref(getItemHeightRef));
|
}
|
|
function onScroll() {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return;
|
}
|
state.scrollTop = wrapEl.scrollTop;
|
state.first = getFirst();
|
state.last = getLast(state.first);
|
}
|
|
function scrollToTop() {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return;
|
}
|
wrapEl.scrollTop = 0;
|
}
|
|
function scrollToBottom() {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return;
|
}
|
wrapEl.scrollTop = wrapEl.scrollHeight;
|
}
|
|
function scrollToItem(index: number) {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return;
|
}
|
const i = index - 1 > 0 ? index - 1 : 0;
|
wrapEl.scrollTop = i * unref(getItemHeightRef);
|
}
|
|
function renderChildren() {
|
const { items = [] } = props;
|
return items.slice(unref(getFirstToRenderRef), unref(getLastToRenderRef)).map(genChild);
|
}
|
|
function genChild(item: any, index: number) {
|
index += unref(getFirstToRenderRef);
|
const top = convertToUnit(index * unref(getItemHeightRef));
|
return (
|
<div class={`${prefixCls}__item`} style={{ top }} key={index}>
|
{getSlot(slots, 'default', { index, item })}
|
</div>
|
);
|
}
|
|
expose({
|
wrapElRef,
|
scrollToTop,
|
scrollToItem,
|
scrollToBottom,
|
});
|
|
onMounted(() => {
|
state.last = getLast(0);
|
nextTick(() => {
|
const wrapEl = unref(wrapElRef);
|
if (!wrapEl) {
|
return;
|
}
|
useEventListener({
|
el: wrapEl,
|
name: 'scroll',
|
listener: onScroll,
|
wait: 0,
|
});
|
});
|
});
|
|
return () => (
|
<div class={prefixCls} style={unref(getWrapStyleRef)} ref={wrapElRef}>
|
<div class={`${prefixCls}__container`} style={unref(getContainerStyleRef)}>
|
{renderChildren()}
|
</div>
|
</div>
|
);
|
},
|
});
|
</script>
|
<style scoped lang="less">
|
.virtual-scroll {
|
display: block;
|
position: relative;
|
flex: 1 1 auto;
|
width: 100%;
|
max-width: 100%;
|
overflow: auto;
|
|
&__container {
|
display: block;
|
}
|
|
&__item {
|
position: absolute;
|
right: 0;
|
left: 0;
|
}
|
}
|
</style>
|