Skip to content

Instantly share code, notes, and snippets.

@NateScarlet
Last active June 8, 2023 10:12
Show Gist options
  • Save NateScarlet/519ec66700e07b73bbf5398fca9ef90c to your computer and use it in GitHub Desktop.
Save NateScarlet/519ec66700e07b73bbf5398fca9ef90c to your computer and use it in GitHub Desktop.
vue composable for implement virtual scroll on x or/and y axis
import type { StyleValue } from 'vue';
import { computed, reactive, ref } from 'vue';
export default function useVirtualScroll({
x,
y,
}: {
x?: {
totalCount: () => number;
itemWidth: () => number;
maxWidth?: () => number;
};
y?: {
totalCount: () => number;
itemHeight: () => number;
maxHeight?: () => number;
};
}) {
const container = ref<HTMLElement>();
const state = reactive({
scrollTop: 0,
scrollLeft: 0,
});
const topPlaceholder = ref<HTMLElement>();
const visibleItemCountY = () =>
y
? Math.ceil(
(y.maxHeight?.() ?? document.body.clientHeight) / y.itemHeight()
)
: 0;
const offsetTop = () =>
(topPlaceholder.value?.clientTop ?? 0) - (container.value?.clientTop ?? 0);
const topIndex = () =>
y
? Math.max(
0,
Math.floor((state.scrollTop - offsetTop()) / y.itemHeight())
)
: 0;
const leftPlaceholder = ref<HTMLElement>();
const visibleItemCountX = () =>
x
? Math.ceil((x.maxWidth?.() ?? document.body.clientWidth) / x.itemWidth())
: 0;
const offsetLeft = () =>
(leftPlaceholder.value?.clientLeft ?? 0) -
(container.value?.clientLeft ?? 0);
const leftIndex = () =>
x
? Math.max(
0,
Math.floor((state.scrollLeft - offsetLeft()) / x.itemWidth())
)
: 0;
function yData<T>(value: (index: number) => T) {
if (!y) {
return [];
}
const s = [] as number[];
for (
let index = topIndex();
index < y.totalCount() && s.length < visibleItemCountY();
index += 1
) {
s.push(index);
}
return s.map((index) => {
return {
key: index,
index,
value: value(index),
itemAttrs: {
style: {
height: `${y.itemHeight()}px`,
overflowY: 'hidden',
} as StyleValue,
},
};
});
}
function xData<T>(value: (index: number) => T) {
if (!x) {
return [];
}
const s = [] as number[];
for (
let index = leftIndex();
index < x.totalCount() && s.length < visibleItemCountX();
index += 1
) {
s.push(index);
}
return s.map((index) => {
return {
key: index,
index,
value: value(index),
itemAttrs: {
style: {
width: `${x.itemWidth()}px`,
overflowX: 'hidden',
} as StyleValue,
},
};
});
}
const containerAttrs = computed(() => {
return {
style: {
...(x ? { overflowX: 'auto' } : {}),
...(y ? { overflowY: 'auto' } : {}),
} as StyleValue,
onScrollPassive: (e: UIEvent) => {
const el = e.target as HTMLElement;
state.scrollTop = el.scrollTop;
state.scrollLeft = el.scrollLeft;
container.value = el;
},
};
});
const topPlaceholderHeight = () =>
y ? topIndex() * y.itemHeight() - offsetTop() : 0;
const topPlaceholderAttrs = computed(() => {
const height = topPlaceholderHeight();
return {
ref: topPlaceholder,
style:
height > 0
? {
height: `${height}px`,
}
: { display: 'none' },
};
});
const bottomPlaceholderHeight = () =>
y
? y.itemHeight() * (y.totalCount() - visibleItemCountY() - topIndex())
: 0;
const bottomPlaceholderAttrs = computed(() => {
const height = bottomPlaceholderHeight();
return {
style:
height > 0
? {
height: `${height}px`,
}
: { display: 'none' },
};
});
const leftPlaceholderWidth = () =>
x ? leftIndex() * x.itemWidth() - offsetLeft() : 0;
const leftPlaceholderAttrs = computed(() => {
const width = leftPlaceholderWidth();
return {
ref: leftPlaceholder,
style:
width > 0
? {
width: `${width}px`,
}
: { display: 'none' },
};
});
const rightPlaceholderWidth = () =>
x
? x.itemWidth() * (x.totalCount() - visibleItemCountX() - leftIndex())
: 0;
const rightPlaceholderAttrs = computed(() => {
const width = rightPlaceholderWidth();
return {
style:
width > 0
? {
width: `${width}px`,
}
: { display: 'none' },
};
});
return reactive({
x: xData,
y: yData,
containerAttrs,
topPlaceholder,
leftPlaceholder,
topPlaceholderAttrs,
rightPlaceholderAttrs,
bottomPlaceholderAttrs,
leftPlaceholderAttrs,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment