// --- 0) Configuration ---
const FOLDER_PATH = '"Main/Comparison tables"'; // Keep your folder path as is, or translate if needed
const WORDS_PER_MINUTE = 200;
const USE_RELATIVE_MODIFIED_TIME = true;
const ITEMS_PER_PAGE = 15; // Number of items per page
// --- 1) Asynchronously load pages and metrics ---
const rawPagesQuery = dv.pages(FOLDER_PATH)
.sort(p => p.file.mtime, 'desc');
const pageDataPromises = rawPagesQuery.map(async (p) => {
let content = "", words = 0;
try {
content = await dv.io.load(p.file.path);
words = content.split(/\s+/).filter(w => w.length > 0).length;
} catch (e) { console.warn(`DataviewJS: Error loading ${p.file.path}:`, e); }
const minutes = Math.ceil(words / WORDS_PER_MINUTE);
const created = p.file.ctime.toISODate();
const modified = p.file.mtime.toISODate();
return [dv.fileLink(p.file.path, false, p.file.name), words, minutes, created, modified];
});
const pages = await Promise.all(pageDataPromises);
let filteredAndSortedData = [...pages]; // Data after filtering and sorting
let currentPage = 1;
// --- UI Setup ---
const container = dv.container;
const summaryElement = document.createElement('div');
summaryElement.style.cssText = `margin-bottom: 10px; font-size: 0.9em; color: var(--text-muted);`;
container.appendChild(summaryElement);
if (pages.length === 0) {
summaryElement.textContent = `Files in folder ${FOLDER_PATH.replace(/"/g, '')} not found.`;
return; // Stop script execution
}
const controlsContainer = document.createElement('div');
controlsContainer.style.cssText = `display: flex; align-items: center; margin-bottom: 15px; gap: 10px; flex-wrap: wrap;`;
container.appendChild(controlsContainer);
const filterInput = document.createElement('input');
filterInput.type = 'text';
filterInput.placeholder = 'Filter by table name…';
filterInput.style.cssText = `
flex-grow: 1; padding: 8px 10px; box-sizing: border-box;
border: 1px solid var(--text-faint); background-color: var(--background-secondary);
color: var(--text-normal); border-radius: 5px; font-size: 0.9em; min-width: 200px;`;
controlsContainer.appendChild(filterInput);
const resetButton = document.createElement('button');
resetButton.textContent = "Reset";
resetButton.classList.add('mod-cta');
resetButton.style.padding = "8px 12px";
resetButton.style.fontSize = "0.9em";
controlsContainer.appendChild(resetButton);
const table = document.createElement('table');
table.classList.add('dataview', 'table-view-table');
container.appendChild(table);
const paginationContainer = document.createElement('div');
paginationContainer.style.cssText = `margin-top: 15px; display: flex; justify-content: center; align-items: center; gap: 10px;`;
paginationContainer.classList.add('pagination-controls'); // Add class for styles
container.appendChild(paginationContainer);
const headers = [
{ text: "Table Name", sortable: true, type: "link" },
{ text: "Words", sortable: true, type: "number", align: "right" },
{ text: "🕒 Min", sortable: true, type: "number", align: "right" }, // You might want to change "Min" to "Mins" or "Read Time"
{ text: "Created", sortable: true, type: "date", align: "center" },
{ text: "Modified", sortable: true, type: "date", align: "center" }
];
const thead = table.createTHead();
const headerRow = thead.insertRow();
let currentSort = { columnIndex: 4, asc: false }; // Default sort by "Modified" descending
function updateHeadersAppearance() {
headerRow.childNodes.forEach((node, idx) => {
if (node.nodeName === "TH" && headers[idx].sortable) {
node.classList.remove('sort-active');
let textContent = headers[idx].text;
if (idx === currentSort.columnIndex && currentSort.columnIndex !== -1) {
node.classList.add('sort-active');
textContent += currentSort.asc ? ' <span class="sort-indicator sort-asc">▲</span>' : ' <span class="sort-indicator sort-desc">▼</span>';
}
node.innerHTML = textContent;
}
});
}
headers.forEach((h, i) => {
const th = document.createElement('th');
th.innerHTML = h.text;
if (h.align) th.style.textAlign = h.align;
if (h.sortable) {
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
const newSortAsc = (currentSort.columnIndex === i) ? !currentSort.asc : true;
currentSort = { columnIndex: i, asc: newSortAsc };
updateHeadersAppearance();
applyFiltersAndSort();
});
}
headerRow.appendChild(th);
});
const tbody = table.createTBody();
function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function renderCurrentPage() {
tbody.innerHTML = '';
const currentFilterTerm = filterInput.value.trim().toLowerCase(); // Ensure filter term is lowercase for matching
const totalPages = Math.ceil(filteredAndSortedData.length / ITEMS_PER_PAGE);
if (currentPage < 1) currentPage = 1;
if (currentPage > totalPages && totalPages > 0) currentPage = totalPages;
if (totalPages === 0 && filteredAndSortedData.length === 0) currentPage = 1;
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, filteredAndSortedData.length); // Ensure endIndex does not exceed array bounds
const pageItems = filteredAndSortedData.slice(startIndex, endIndex);
// Update summary text
if (filteredAndSortedData.length === 0) {
if (currentFilterTerm) {
summaryElement.textContent = `No results for "${currentFilterTerm}" (Total in database: ${pages.length}).`;
} else {
summaryElement.textContent = `No data to display. (Total in database: ${pages.length}).`;
}
} else {
let recordsRange = `${startIndex + 1}-${endIndex}`;
let summaryText = `Showing ${recordsRange} of ${filteredAndSortedData.length}`;
if (currentFilterTerm) {
summaryText += ` (filtered). Total in database: ${pages.length}.`;
} else {
summaryText += `. Total in database: ${pages.length}.`;
}
summaryElement.textContent = summaryText;
}
pageItems.forEach(rowData => {
const tr = tbody.insertRow();
rowData.forEach((cellData, cellIndex) => {
const td = tr.insertCell();
const headerConfig = headers[cellIndex];
if (headerConfig.align) td.style.textAlign = headerConfig.align;
if (headerConfig.type === "link" && typeof cellData === 'object' && cellData.path) {
const linkEl = document.createElement('a');
let linkText = cellData.display || cellData.path.split('/').pop().replace(/\.md$/, '');
if (currentFilterTerm && cellIndex === 0) { // Highlight only in "Table Name" column
try {
const regex = new RegExp(`(${escapeRegExp(currentFilterTerm)})`, 'gi');
linkText = linkText.replace(regex, '<span class="filter-highlight">$1</span>');
} catch (e) { /* ignore regex errors */ }
}
linkEl.innerHTML = linkText; // Use innerHTML for highlighted text
linkEl.classList.add('internal-link');
linkEl.dataset.href = cellData.path;
linkEl.addEventListener('click', (e) => {
e.preventDefault();
app.workspace.openLinkText(cellData.path, dv.currentFilePath || "", e.ctrlKey || e.metaKey);
});
td.appendChild(linkEl);
} else if (headerConfig.type === "date" && typeof cellData === 'string') {
try {
const dateObj = dv.luxon.DateTime.fromISO(cellData);
if (dateObj.isValid) {
const modifiedHeaderIndex = headers.findIndex(h => h.text === "Modified"); // Find "Modified" header index
if (USE_RELATIVE_MODIFIED_TIME && cellIndex === modifiedHeaderIndex) {
td.textContent = dateObj.toRelative({locale: dv.settings.defaultLocale}) || dateObj.toLocaleString(dv.luxon.DateTime.DATE_MED, {locale: dv.settings.defaultLocale});
} else {
// Example: "May 20, 2025" (omitting weekday for brevity)
td.textContent = dateObj.toLocaleString(dv.luxon.DateTime.DATE_MED, {locale: dv.settings.defaultLocale});
}
} else { td.textContent = cellData; } // Show original string if date is invalid
} catch (e) { td.textContent = cellData; } // Fallback on error
} else {
td.textContent = cellData != null ? cellData.toString() : ""; // Handle null/undefined
}
});
});
renderPaginationControls();
}
function renderPaginationControls() {
paginationContainer.innerHTML = '';
const totalItems = filteredAndSortedData.length;
const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
if (totalPages <= 1) return; // Don't show pagination if only one page or no data
const prevButton = document.createElement('button');
prevButton.textContent = 'Previous';
prevButton.disabled = currentPage === 1;
prevButton.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderCurrentPage();
}
});
paginationContainer.appendChild(prevButton);
const pageInfo = document.createElement('span');
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
pageInfo.style.margin = "0 10px";
paginationContainer.appendChild(pageInfo);
const nextButton = document.createElement('button');
nextButton.textContent = 'Next';
nextButton.disabled = currentPage === totalPages;
nextButton.addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
renderCurrentPage();
}
});
paginationContainer.appendChild(nextButton);
}
function applyFiltersAndSort() {
// 1. Filtering
const term = filterInput.value.toLowerCase().trim();
if (term) {
filteredAndSortedData = pages.filter(row => {
const linkData = row[0]; // dv.fileLink object
const fileName = linkData.display ? linkData.display.toLowerCase() : (linkData.path ? linkData.path.toLowerCase() : "");
return fileName.includes(term);
});
} else {
filteredAndSortedData = [...pages]; // Reset to all pages if filter is empty
}
// 2. Sorting
if (currentSort.columnIndex !== -1) {
const h = headers[currentSort.columnIndex];
filteredAndSortedData.sort((a, b) => {
let va = a[currentSort.columnIndex], vb = b[currentSort.columnIndex];
if (h.type === "link") { // For file links, sort by display name or path
va = va.display ? va.display.toString() : (va.path ? va.path.toString() : "");
vb = vb.display ? vb.display.toString() : (vb.path ? vb.path.toString() : "");
}
if (h.type === "number") {
va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; // Handle NaN
return currentSort.asc ? va - vb : vb - va;
} else { // For strings and dates (ISO dates sort correctly as strings)
return currentSort.asc
? String(va).localeCompare(String(vb), dv.settings.defaultLocale, { sensitivity: 'base', numeric: (h.type !== "link" && h.type !== "date") })
: String(vb).localeCompare(String(va), dv.settings.defaultLocale, { sensitivity: 'base', numeric: (h.type !== "link" && h.type !== "date") });
}
});
}
currentPage = 1; // Reset to first page after filtering/sorting
renderCurrentPage(); // Render the current page
}
// Initial setup
updateHeadersAppearance();
applyFiltersAndSort();
// Event Listeners
filterInput.addEventListener('input', applyFiltersAndSort);
resetButton.addEventListener('click', () => {
filterInput.value = "";
currentSort = { columnIndex: 4, asc: false }; // Reset sort to "Modified" desc
updateHeadersAppearance();
applyFiltersAndSort();
});
const style = document.createElement('style');
style.textContent = `
.dataview.table-view-table th { white-space: nowrap; }
.dataview.table-view-table td { vertical-align: middle; }
.dataview.table-view-table tbody tr:nth-child(even) {
background-color: var(--background-secondary); /* Zebra striping */
}
.dataview.table-view-table tbody tr:hover {
background-color: var(--background-modifier-hover);
}
.sort-indicator { font-size: 0.8em; color: var(--text-muted); margin-left: 4px; }
.dataview.table-view-table th.sort-active {
background-color: var(--background-primary-alt); /* Active sort column header */
color: var(--text-accent);
}
.dataview.table-view-table th.sort-active .sort-indicator {
color: var(--text-accent);
}
.dataview.table-view-table td a.internal-link { cursor: pointer; }
.filter-highlight { /* Style for highlighted filter term */
background-color: var(--text-highlight-bg);
color: var(--text-normal);
font-weight: bold;
padding: 0 1px;
border-radius: 2px;
}
/* Styles for pagination controls */
.pagination-controls button {
padding: 6px 10px;
font-size: 0.9em;
border: 1px solid var(--text-faint);
background-color: var(--background-secondary);
color: var(--text-normal);
border-radius: 4px;
cursor: pointer;
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: default;
}
.pagination-controls button:not(:disabled):hover {
background-color: var(--background-modifier-hover);
}
`;
container.appendChild(style);