167 lines
4.3 KiB
JavaScript

import { watchDebounced } from "@vueuse/core";
export const useDataTable = (initialData, initialColumns) => {
const data = ref(initialData);
const columns = ref(initialColumns);
const sortBy = ref("");
const sortDesc = ref(false);
const searchQuery = ref("");
const currentPage = ref(1);
const itemsPerPage = ref(10);
const itemsPerPageOptions = ref([5, 10, 25, 50, 100]);
const columnFilters = ref({});
// Debounced search to prevent excessive filtering
const debouncedSearch = ref(searchQuery.value);
watchDebounced(
searchQuery,
(newValue) => {
debouncedSearch.value = newValue;
currentPage.value = 1;
},
{ debounce: 300 }
);
// Memoize cell value getter
const getCellValue = memoize((item, column) => {
if (column.path) {
return column.path.split(".").reduce((obj, key) => obj?.[key], item);
}
return item[column.key];
});
// Create index for faster searching
const searchIndex = computed(() => {
const index = new Map();
data.value.forEach((item, idx) => {
const searchableValues = columns.value
.map((column) => getCellValue(item, column))
.filter(Boolean)
.map((value) => value.toString().toLowerCase())
.join(" ");
index.set(idx, searchableValues);
});
return index;
});
const filteredData = computed(() => {
// Start with indices instead of full objects for better memory usage
let indices = [...data.value.keys()];
// Apply column filters
Object.entries(columnFilters.value).forEach(([key, value]) => {
if (value) {
const column = columns.value.find((col) => col.key === key);
indices = indices.filter((idx) => {
const item = data.value[idx];
const cellValue = getCellValue(item, column);
return cellValue
?.toString()
.toLowerCase()
.includes(value.toLowerCase());
});
}
});
// Apply search filter using index
if (debouncedSearch.value) {
const searchTerm = debouncedSearch.value.toLowerCase();
indices = indices.filter((idx) =>
searchIndex.value.get(idx).includes(searchTerm)
);
}
// Apply sorting only to visible page
if (sortBy.value) {
const sortColumn = columns.value.find((col) => col.key === sortBy.value);
indices.sort((aIdx, bIdx) => {
const aVal = getCellValue(data.value[aIdx], sortColumn);
const bVal = getCellValue(data.value[bIdx], sortColumn);
if (sortDesc.value) {
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
}
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
});
}
return indices;
});
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value;
const end = start + itemsPerPage.value;
// Only map indices to full objects for visible page
return filteredData.value.slice(start, end).map((idx) => data.value[idx]);
});
const totalPages = computed(() =>
Math.ceil(filteredData.value.length / itemsPerPage.value)
);
// Cache sort results
const sortCache = new Map();
const toggleSort = (column) => {
if (column.sortable) {
if (sortBy.value === column.key) {
sortDesc.value = !sortDesc.value;
} else {
sortBy.value = column.key;
sortDesc.value = false;
}
// Clear cache when sort changes
sortCache.clear();
}
};
const setItemsPerPage = (value) => {
itemsPerPage.value = parseInt(value, 10);
currentPage.value = 1;
};
// Debounced column filter
const setColumnFilter = debounce((column, value) => {
columnFilters.value[column] = value;
currentPage.value = 1;
}, 300);
return {
data,
columns,
sortBy,
sortDesc,
searchQuery,
currentPage,
itemsPerPage,
paginatedData,
totalPages,
toggleSort,
itemsPerPageOptions,
columnFilters,
setItemsPerPage,
setColumnFilter,
filteredData,
getCellValue,
};
};
// Utility functions
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key);
};
}
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}