Skip to content

Commit bfde094

Browse files
committed
refactor: get rid of ResourceListTableVirtual and add virlual scroll to the resourceListTable
1 parent cb62f04 commit bfde094

File tree

3 files changed

+146
-831
lines changed

3 files changed

+146
-831
lines changed

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
<!-- table -->
33
<div class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
44
:class="{'rounded-default': !noRoundings}"
5+
:style="isVirtualScrollEnabled ? { maxHeight: `${containerHeight}px` } : {}"
6+
@scroll="handleScroll"
7+
ref="containerRef"
58
>
69
<!-- skelet loader -->
710
<div role="status" v-if="!resource || !resource.columns"
@@ -82,10 +85,12 @@
8285

8386
</td>
8487
</tr>
85-
88+
<!-- Top spacer(virtual scroll) -->
89+
<tr v-if="isVirtualScrollEnabled && spacerHeight > 0">
90+
<td :colspan="resource?.columns.length + 2" :style="{ height: `${spacerHeight}px` }"></td>
91+
</tr>
8692
<component
87-
v-else
88-
v-for="(row, rowI) in rows"
93+
v-for="(row, rowI) in rowsToRender"
8994
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
9095
:key="`row_${row._primaryKeyValue}`"
9196
:record="row"
@@ -95,7 +100,8 @@
95100
@click="onClick($event, row)"
96101
ref="rowRefs"
97102
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
98-
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
103+
:class="{'border-b': rowI !== rowsToRender.length - 1, 'cursor-pointer': row._clickUrl !== null}"
104+
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
99105
>
100106
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
101107
<Checkbox
@@ -225,6 +231,12 @@
225231

226232
</td>
227233
</component>
234+
<!-- Bottom spacer(virtual scroll) -->
235+
<tr v-if="isVirtualScrollEnabled && totalHeight > 0">
236+
<td :colspan="resource?.columns.length + 2"
237+
:style="{ height: `${Math.max(0, totalHeight - (endIndex + 1) * (props.itemHeight || 52.5))}px` }">
238+
</td>
239+
</tr>
228240
</tbody>
229241
</table>
230242
</div>
@@ -359,10 +371,23 @@ const props = defineProps<{
359371
noRoundings?: boolean,
360372
customActionsInjection?: any[],
361373
tableBodyStartInjection?: any[],
374+
containerHeight?: number,
375+
itemHeight?: number,
376+
bufferSize?: number,
362377
customActionIconsThreeDotsMenuItems?: any[]
363378
tableRowReplaceInjection?: AdminForthComponentDeclarationFull,
379+
isVirtualScrollEnabled: boolean
364380
}>();
365381
382+
//select between all rows or rows, that should be rendered in virtual scroll
383+
const rowsToRender = computed(() => {
384+
if (!props.isVirtualScrollEnabled) {
385+
return props.rows || [];
386+
} else {
387+
return visibleRows.value;
388+
}
389+
});
390+
366391
// emits, update page
367392
const emits = defineEmits([
368393
'update:page',
@@ -628,6 +653,119 @@ function validatePageInput() {
628653
page.value = validPage;
629654
pageInput.value = validPage.toString();
630655
}
656+
/*
657+
*___________________________________________________________________
658+
* |
659+
* Virtual Scroll Implementation |
660+
*___________________________________________________________________|
661+
*/
662+
// Add throttle utility
663+
const throttle = (fn: Function, delay: number) => {
664+
let lastCall = 0;
665+
return (...args: any[]) => {
666+
const now = Date.now();
667+
if (now - lastCall >= delay) {
668+
lastCall = now;
669+
fn(...args);
670+
}
671+
};
672+
};
673+
// Virtual scroll state
674+
const containerRef = ref<HTMLElement | null>(null);
675+
const scrollTop = ref(0);
676+
const visibleRows = ref<any[]>([]);
677+
const startIndex = ref(0);
678+
const endIndex = ref(0);
679+
const totalHeight = ref(0);
680+
const spacerHeight = ref(0);
681+
const rowHeightsMap = ref<{[key: string]: number}>({});
682+
const rowPositions = ref<number[]>([]);
683+
// Calculate row positions based on heights
684+
const calculateRowPositions = () => {
685+
if (!props.rows) return;
686+
687+
let currentPosition = 0;
688+
rowPositions.value = props.rows.map((row) => {
689+
const height = rowHeightsMap.value[`row_${row._primaryKeyValue}`] || props.itemHeight || 52.5;
690+
const position = currentPosition;
691+
currentPosition += height;
692+
return position;
693+
});
694+
totalHeight.value = currentPosition;
695+
};
696+
// Calculate visible rows based on scroll position
697+
const calculateVisibleRows = () => {
698+
if (!props.rows?.length) {
699+
visibleRows.value = props.rows || [];
700+
return;
701+
}
702+
const buffer = props.bufferSize || 5;
703+
const containerHeight = props.containerHeight || 900;
704+
705+
// For single item or small datasets, show all rows
706+
if (props.rows.length <= buffer * 2 + 1) {
707+
startIndex.value = 0;
708+
endIndex.value = props.rows.length - 1;
709+
visibleRows.value = props.rows;
710+
spacerHeight.value = 0;
711+
return;
712+
}
713+
714+
// Binary search for start index
715+
let low = 0;
716+
let high = rowPositions.value.length - 1;
717+
const targetPosition = scrollTop.value;
718+
719+
while (low <= high) {
720+
const mid = Math.floor((low + high) / 2);
721+
if (rowPositions.value[mid] <= targetPosition) {
722+
low = mid + 1;
723+
} else {
724+
high = mid - 1;
725+
}
726+
}
727+
728+
const newStartIndex = Math.max(0, low - 1 - buffer);
729+
const newEndIndex = Math.min(
730+
props.rows.length - 1,
731+
newStartIndex + Math.ceil(containerHeight / (props.itemHeight || 52.5)) + buffer * 2
732+
);
733+
// Ensure at least one row is visible
734+
if (newEndIndex < newStartIndex) {
735+
startIndex.value = 0;
736+
endIndex.value = Math.min(props.rows.length - 1, Math.ceil(containerHeight / (props.itemHeight || 52.5)));
737+
} else {
738+
startIndex.value = newStartIndex;
739+
endIndex.value = newEndIndex;
740+
}
741+
742+
visibleRows.value = props.rows.slice(startIndex.value, endIndex.value + 1);
743+
spacerHeight.value = startIndex.value > 0 ? rowPositions.value[startIndex.value - 1] : 0;
744+
};
745+
// Throttled scroll handler
746+
const handleScroll = throttle((e: Event) => {
747+
if (!props.isVirtualScrollEnabled) return;
748+
const target = e.target as HTMLElement;
749+
scrollTop.value = target.scrollTop;
750+
calculateVisibleRows();
751+
}, 16);
752+
// Update row height when it changes
753+
const updateRowHeight = (rowId: string, height: number) => {
754+
if (!props.isVirtualScrollEnabled) return;
755+
if (rowHeightsMap.value[rowId] !== height) {
756+
rowHeightsMap.value[rowId] = height;
757+
calculateRowPositions();
758+
calculateVisibleRows();
759+
}
760+
};
761+
// Watch for changes in rows
762+
watch(() => props.rows, () => {
763+
if (props.rows) {
764+
calculateRowPositions();
765+
calculateVisibleRows();
766+
}
767+
}, { immediate: true });
768+
631769
632770
</script>
633771

0 commit comments

Comments
 (0)