Files
VRCX/src/lib/table/useVrcxVueTable.js
2026-03-05 20:42:15 +09:00

495 lines
11 KiB
JavaScript

import {
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
isFunction,
useVueTable
} from '@tanstack/vue-table';
import { computed, ref, unref, watch } from 'vue';
/**
*
* @param str
*/
function safeJsonParse(str) {
if (!str) {
return null;
}
try {
return JSON.parse(str);
} catch {
return null;
}
}
/**
*
* @param fn
* @param wait
*/
function debounce(fn, wait) {
let t = 0;
return (...args) => {
if (t) {
clearTimeout(t);
}
t = setTimeout(() => fn(...args), wait);
};
}
/**
*
* @param sizing
* @param columns
*/
function filterSizingByColumns(sizing, columns) {
if (!sizing || typeof sizing !== 'object') {
return {};
}
const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean));
const out = {};
for (const [key, value] of Object.entries(sizing)) {
if (ids.has(key)) {
out[key] = value;
}
}
return out;
}
/**
*
* @param sorting
* @param columns
*/
function filterSortingByColumns(sorting, columns) {
if (!Array.isArray(sorting)) {
return [];
}
const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean));
return sorting.filter((s) => s && ids.has(s.id));
}
/**
*
* @param order
* @param columns
*/
function filterOrderByColumns(order, columns) {
if (!Array.isArray(order)) {
return [];
}
const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean));
return order.filter((id) => ids.has(id));
}
/**
*
* @param visibility
* @param columns
*/
function filterVisibilityByColumns(visibility, columns) {
if (!visibility || typeof visibility !== 'object') {
return {};
}
const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean));
const out = {};
for (const [key, value] of Object.entries(visibility)) {
if (ids.has(key)) {
out[key] = value;
}
}
return out;
}
/**
*
* @param col
*/
function getColumnId(col) {
return col?.id ?? col?.accessorKey ?? null;
}
/**
*
* @param columns
*/
function findStretchColumnId(columns) {
if (!Array.isArray(columns)) {
return null;
}
for (const col of columns) {
if (col?.meta?.['stretch']) {
return getColumnId(col);
}
}
return null;
}
/**
*
* @param updaterOrValue
* @param targetRef
*/
function setRef(updaterOrValue, targetRef) {
targetRef.value = isFunction(updaterOrValue)
? updaterOrValue(targetRef.value)
: updaterOrValue;
}
/**
*
* @param func
*/
function resolveMaybeGetter(func) {
return typeof func === 'function' ? func() : unref(func);
}
/**
*
* @param columns
* @param enabled
* @param spacerId
* @param stretchAfterId
*/
function withSpacerColumn(columns, enabled, spacerId, stretchAfterId) {
if (!enabled) {
return columns;
}
if (!Array.isArray(columns)) {
return columns;
}
const id = spacerId ?? '__spacer';
if (columns.some((c) => getColumnId(c) === id)) {
return columns;
}
const spacerColumn = {
id,
header: () => null,
cell: () => null,
enableSorting: false,
size: 0,
minSize: 0,
meta: { thClass: 'p-0', tdClass: 'p-0' }
};
if (stretchAfterId) {
const idx = columns.findIndex((c) => getColumnId(c) === stretchAfterId);
if (idx !== -1) {
return [
...columns.slice(0, idx + 1),
spacerColumn,
...columns.slice(idx + 1)
];
}
}
return [...columns, spacerColumn];
}
/**
*
* @param options
*/
export function useVrcxVueTable(options) {
const {
getRowId,
getRowCanExpand,
enablePagination = true,
initialPagination,
enableSorting = true,
initialSorting,
enableFiltering = true,
enableExpanded = false,
initialExpanded,
enablePinning = false,
initialColumnPinning,
enableColumnResizing = true,
columnResizeMode = 'onChange',
initialColumnSizing,
enableColumnReorder = true,
initialColumnOrder,
enableColumnVisibility = true,
initialColumnVisibility,
fillRemainingSpace = true,
spacerColumnId = '__spacer',
persistKey,
persistColumnSizing = true,
persistSorting = true,
persistColumnOrder = true,
persistColumnVisibility = true,
persistDebounceMs = 200,
tableOptions = {}
} = options ?? {};
const hasData = options && 'data' in options;
const hasColumns = options && 'columns' in options;
if (!hasData) console.warn('useVrcxVueTable: `data` is required');
if (!hasColumns) console.warn('useVrcxVueTable: `columns` is required');
const expanded = ref(initialExpanded ?? {});
const pagination = ref(initialPagination ?? { pageIndex: 0, pageSize: 50 });
const columnPinning = ref(initialColumnPinning ?? { left: [], right: [] });
const columnSizing = ref(initialColumnSizing ?? {});
const columnOrder = ref(initialColumnOrder ?? []);
const columnVisibility = ref(initialColumnVisibility ?? {});
const storageKey = persistKey ? `vrcx:table:${persistKey}` : null;
/**
*
*/
function readPersisted() {
if (!storageKey) {
return null;
}
return safeJsonParse(localStorage.getItem(storageKey));
}
/**
*
* @param patch
*/
function writePersisted(patch) {
if (!storageKey) {
return;
}
const cur = safeJsonParse(localStorage.getItem(storageKey)) ?? {};
const next = { ...cur, ...patch, updatedAt: Date.now() };
localStorage.setItem(storageKey, JSON.stringify(next));
}
const persisted = readPersisted();
let resolvedSorting = initialSorting ?? [];
if (persisted && persistSorting && Array.isArray(persisted.sorting)) {
resolvedSorting = persisted.sorting;
}
const sorting = ref(resolvedSorting);
if (persisted && persistColumnSizing && persisted.columnSizing) {
columnSizing.value = persisted.columnSizing;
}
if (
persisted &&
persistColumnOrder &&
Array.isArray(persisted.columnOrder)
) {
columnOrder.value = persisted.columnOrder;
}
if (persisted && persistColumnVisibility && persisted.columnVisibility) {
columnVisibility.value = persisted.columnVisibility;
}
const state = {};
const handlers = {};
const rowModels = {};
const extra = {};
/**
*
* @param enabled
* @param key
* @param r
* @param onChangeKey
* @param rowModelPart
* @param extraPart
*/
function register(enabled, key, r, onChangeKey, rowModelPart, extraPart) {
if (!enabled) {
return;
}
Object.defineProperty(state, key, {
enumerable: true,
get: () => r.value
});
if (onChangeKey) handlers[onChangeKey] = (val) => setRef(val, r);
if (rowModelPart) Object.assign(rowModels, rowModelPart);
if (extraPart) Object.assign(extra, extraPart);
}
register(enableSorting, 'sorting', sorting, 'onSortingChange', {
getSortedRowModel: getSortedRowModel()
});
register(enablePagination, 'pagination', pagination, 'onPaginationChange', {
getPaginationRowModel: getPaginationRowModel()
});
register(
enableExpanded,
'expanded',
expanded,
'onExpandedChange',
{ getExpandedRowModel: getExpandedRowModel() },
{ getRowCanExpand }
);
register(
enablePinning,
'columnPinning',
columnPinning,
'onColumnPinningChange'
);
register(
enableColumnResizing,
'columnSizing',
columnSizing,
'onColumnSizingChange',
null,
{ enableColumnResizing: true, columnResizeMode }
);
register(
enableColumnReorder,
'columnOrder',
columnOrder,
'onColumnOrderChange'
);
register(
enableColumnVisibility,
'columnVisibility',
columnVisibility,
'onColumnVisibilityChange'
);
if (enableFiltering) {
Object.assign(rowModels, {
getFilteredRowModel: getFilteredRowModel()
});
}
const dataSource = computed(() => options.data);
const columnsSource = computed(() => resolveMaybeGetter(options.columns));
const table = useVueTable({
data: dataSource,
get columns() {
const cols = columnsSource.value;
const stretchAfterId = findStretchColumnId(cols);
return withSpacerColumn(
cols,
fillRemainingSpace,
spacerColumnId,
stretchAfterId
);
},
getRowId,
getCoreRowModel: getCoreRowModel(),
...rowModels,
...handlers,
...extra,
state,
...tableOptions
});
watch(
columnsSource,
(next) => {
table.setOptions((prev) => ({
...prev,
columns: withSpacerColumn(
next,
fillRemainingSpace,
spacerColumnId,
findStretchColumnId(next)
)
}));
table.setState((prev) => ({ ...prev }));
},
{ immediate: true }
);
const persistWrite = debounce(
(payload) => writePersisted(payload),
persistDebounceMs
);
if (storageKey && persistColumnSizing) {
watch(
columnSizing,
(val) => {
const cols = table.getAllLeafColumns?.() ?? [];
persistWrite({
columnSizing: filterSizingByColumns(val, cols)
});
},
{ deep: true }
);
}
if (storageKey && persistSorting) {
watch(
sorting,
(val) => {
const cols = table.getAllLeafColumns?.() ?? [];
persistWrite({
sorting: filterSortingByColumns(val, cols)
});
},
{ deep: true }
);
}
if (storageKey && persistColumnOrder) {
watch(
columnOrder,
(val) => {
const cols = table.getAllLeafColumns?.() ?? [];
persistWrite({
columnOrder: filterOrderByColumns(val, cols)
});
},
{ deep: true }
);
}
if (storageKey && persistColumnVisibility) {
watch(
columnVisibility,
(val) => {
const cols = table.getAllLeafColumns?.() ?? [];
persistWrite({
columnVisibility: filterVisibilityByColumns(val, cols)
});
},
{ deep: true }
);
}
return {
table,
sorting,
pagination,
expanded,
columnPinning,
columnSizing,
columnOrder,
columnVisibility
};
}