Markdown Table Filtering and Sorting: Complete Interactive Guide for Dynamic Data Presentation
Interactive Markdown table filtering and sorting transforms static documentation into dynamic, user-friendly interfaces that enable real-time data exploration and content discovery. By implementing client-side filtering algorithms, sortable column headers, and advanced search functionality, technical teams can create engaging documentation experiences that scale efficiently with large datasets while maintaining the simplicity and portability of standard Markdown table syntax.
Why Implement Interactive Table Features?
Professional interactive table functionality provides essential benefits for modern documentation systems:
- Enhanced User Experience: Enable users to quickly find relevant information in large datasets without manual scanning
- Data Exploration: Provide tools for users to analyze and understand complex information through sorting and filtering
- Improved Accessibility: Support keyboard navigation and screen reader compatibility for inclusive content access
- Performance Optimization: Reduce cognitive load by showing only relevant information based on user-defined criteria
- Scalable Documentation: Handle growing datasets without compromising usability or requiring manual content reorganization
Foundation Interactive Table Implementation
Basic Client-Side Filtering
Essential HTML and JavaScript setup for interactive Markdown tables:
<!-- Basic interactive table structure -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Markdown Tables</title>
<style>
.table-container {
max-width: 100%;
overflow-x: auto;
margin: 20px 0;
}
.interactive-table {
width: 100%;
border-collapse: collapse;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
.interactive-table th,
.interactive-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e1e5e9;
vertical-align: top;
}
.interactive-table th {
background-color: #f6f8fa;
font-weight: 600;
position: relative;
cursor: pointer;
user-select: none;
}
.interactive-table th:hover {
background-color: #f1f3f4;
}
.sort-indicator {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
opacity: 0.5;
font-size: 12px;
}
.sort-indicator.active {
opacity: 1;
}
.sort-asc::after {
content: "▲";
}
.sort-desc::after {
content: "▼";
}
.table-controls {
margin-bottom: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.search-input {
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 14px;
min-width: 250px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 14px;
background-color: white;
}
.clear-filters {
padding: 8px 16px;
background-color: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.clear-filters:hover {
background-color: #f1f3f4;
}
.row-hidden {
display: none;
}
.no-results {
text-align: center;
padding: 40px;
color: #656d76;
font-style: italic;
}
.results-count {
font-size: 14px;
color: #656d76;
margin-left: auto;
}
</style>
</head>
<body>
<div class="table-container">
<div class="table-controls">
<input
type="text"
class="search-input"
placeholder="Search all columns..."
id="globalSearch"
>
<select class="filter-select" id="categoryFilter">
<option value="">All Categories</option>
</select>
<select class="filter-select" id="statusFilter">
<option value="">All Statuses</option>
</select>
<button class="clear-filters" id="clearFilters">Clear Filters</button>
<span class="results-count" id="resultsCount"></span>
</div>
<table class="interactive-table" id="dataTable">
<thead>
<tr>
<th data-column="name" data-type="string">
Name
<span class="sort-indicator"></span>
</th>
<th data-column="category" data-type="string">
Category
<span class="sort-indicator"></span>
</th>
<th data-column="status" data-type="string">
Status
<span class="sort-indicator"></span>
</th>
<th data-column="priority" data-type="number">
Priority
<span class="sort-indicator"></span>
</th>
<th data-column="date" data-type="date">
Last Updated
<span class="sort-indicator"></span>
</th>
<th data-column="size" data-type="number">
Size (MB)
<span class="sort-indicator"></span>
</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Data rows will be populated by JavaScript -->
</tbody>
</table>
<div class="no-results" id="noResults" style="display: none;">
No results found matching your criteria.
</div>
</div>
<script src="interactive-table.js"></script>
</body>
</html>
Comprehensive JavaScript Implementation
// interactive-table.js - Complete interactive table functionality
class InteractiveTable {
constructor(tableId, options = {}) {
this.table = document.getElementById(tableId);
this.tbody = this.table.querySelector('tbody');
this.thead = this.table.querySelector('thead');
this.noResults = document.getElementById('noResults');
this.resultsCount = document.getElementById('resultsCount');
this.options = {
enableSearch: true,
enableSort: true,
enableFilter: true,
caseSensitive: false,
highlightMatches: true,
debounceDelay: 300,
pageSize: 50,
enablePagination: false,
...options
};
this.data = [];
this.filteredData = [];
this.currentSort = { column: null, direction: 'asc' };
this.filters = new Map();
this.searchTerm = '';
this.currentPage = 1;
this.init();
}
init() {
this.setupEventListeners();
this.populateFilterOptions();
this.updateResultsCount();
}
setupEventListeners() {
// Global search
if (this.options.enableSearch) {
const searchInput = document.getElementById('globalSearch');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.handleGlobalSearch(e.target.value);
}, this.options.debounceDelay);
});
}
}
// Column sorting
if (this.options.enableSort) {
this.thead.addEventListener('click', (e) => {
const header = e.target.closest('th[data-column]');
if (header) {
this.handleSort(header.dataset.column, header.dataset.type);
}
});
}
// Filters
if (this.options.enableFilter) {
const categoryFilter = document.getElementById('categoryFilter');
const statusFilter = document.getElementById('statusFilter');
if (categoryFilter) {
categoryFilter.addEventListener('change', (e) => {
this.setFilter('category', e.target.value);
});
}
if (statusFilter) {
statusFilter.addEventListener('change', (e) => {
this.setFilter('status', e.target.value);
});
}
}
// Clear filters
const clearButton = document.getElementById('clearFilters');
if (clearButton) {
clearButton.addEventListener('click', () => {
this.clearAllFilters();
});
}
// Keyboard navigation
this.table.addEventListener('keydown', (e) => {
this.handleKeyboardNavigation(e);
});
}
setData(data) {
this.data = data;
this.filteredData = [...data];
this.renderTable();
this.populateFilterOptions();
this.updateResultsCount();
}
handleGlobalSearch(searchTerm) {
this.searchTerm = searchTerm.toLowerCase().trim();
this.applyFilters();
}
handleSort(column, type = 'string') {
if (this.currentSort.column === column) {
this.currentSort.direction = this.currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
this.currentSort.column = column;
this.currentSort.direction = 'asc';
}
this.sortData(column, type, this.currentSort.direction);
this.updateSortIndicators();
this.renderTable();
}
sortData(column, type, direction) {
this.filteredData.sort((a, b) => {
let aVal = a[column];
let bVal = b[column];
// Handle different data types
switch (type) {
case 'number':
aVal = parseFloat(aVal) || 0;
bVal = parseFloat(bVal) || 0;
break;
case 'date':
aVal = new Date(aVal);
bVal = new Date(bVal);
break;
case 'string':
default:
aVal = String(aVal).toLowerCase();
bVal = String(bVal).toLowerCase();
break;
}
let comparison = 0;
if (aVal < bVal) comparison = -1;
if (aVal > bVal) comparison = 1;
return direction === 'desc' ? -comparison : comparison;
});
}
setFilter(column, value) {
if (value === '') {
this.filters.delete(column);
} else {
this.filters.set(column, value);
}
this.applyFilters();
}
applyFilters() {
this.filteredData = this.data.filter(row => {
// Apply column filters
for (const [column, filterValue] of this.filters) {
const cellValue = String(row[column]).toLowerCase();
const filter = filterValue.toLowerCase();
if (!cellValue.includes(filter)) {
return false;
}
}
// Apply global search
if (this.searchTerm) {
const searchableText = Object.values(row)
.join(' ')
.toLowerCase();
if (!searchableText.includes(this.searchTerm)) {
return false;
}
}
return true;
});
this.renderTable();
this.updateResultsCount();
}
renderTable() {
if (this.filteredData.length === 0) {
this.tbody.innerHTML = '';
this.table.style.display = 'none';
this.noResults.style.display = 'block';
return;
}
this.table.style.display = 'table';
this.noResults.style.display = 'none';
const startIndex = this.options.enablePagination
? (this.currentPage - 1) * this.options.pageSize
: 0;
const endIndex = this.options.enablePagination
? startIndex + this.options.pageSize
: this.filteredData.length;
const visibleData = this.filteredData.slice(startIndex, endIndex);
this.tbody.innerHTML = visibleData.map(row => {
const cells = Object.entries(row).map(([key, value]) => {
let cellContent = this.formatCellValue(key, value);
// Highlight search matches
if (this.options.highlightMatches && this.searchTerm) {
cellContent = this.highlightSearchTerm(cellContent, this.searchTerm);
}
return `<td data-column="${key}">${cellContent}</td>`;
}).join('');
return `<tr>${cells}</tr>`;
}).join('');
}
formatCellValue(column, value) {
// Custom formatting based on column type
switch (column) {
case 'date':
return new Date(value).toLocaleDateString();
case 'size':
return `${parseFloat(value).toFixed(1)} MB`;
case 'priority':
const priority = parseInt(value);
const badges = {
1: '<span class="priority-badge high">High</span>',
2: '<span class="priority-badge medium">Medium</span>',
3: '<span class="priority-badge low">Low</span>'
};
return badges[priority] || value;
case 'status':
const statusClass = value.toLowerCase().replace(/\s+/g, '-');
return `<span class="status-badge status-${statusClass}">${value}</span>`;
default:
return this.escapeHtml(String(value));
}
}
highlightSearchTerm(text, searchTerm) {
if (!searchTerm) return text;
const regex = new RegExp(`(${this.escapeRegExp(searchTerm)})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
updateSortIndicators() {
// Clear all indicators
this.thead.querySelectorAll('.sort-indicator').forEach(indicator => {
indicator.className = 'sort-indicator';
});
// Set active indicator
if (this.currentSort.column) {
const header = this.thead.querySelector(`[data-column="${this.currentSort.column}"] .sort-indicator`);
if (header) {
header.className = `sort-indicator active sort-${this.currentSort.direction}`;
}
}
}
populateFilterOptions() {
// Populate category filter
const categoryFilter = document.getElementById('categoryFilter');
if (categoryFilter) {
const categories = [...new Set(this.data.map(row => row.category))].sort();
categoryFilter.innerHTML = '<option value="">All Categories</option>' +
categories.map(cat => `<option value="${cat}">${cat}</option>`).join('');
}
// Populate status filter
const statusFilter = document.getElementById('statusFilter');
if (statusFilter) {
const statuses = [...new Set(this.data.map(row => row.status))].sort();
statusFilter.innerHTML = '<option value="">All Statuses</option>' +
statuses.map(status => `<option value="${status}">${status}</option>`).join('');
}
}
updateResultsCount() {
if (this.resultsCount) {
const total = this.data.length;
const filtered = this.filteredData.length;
if (filtered === total) {
this.resultsCount.textContent = `${total} items`;
} else {
this.resultsCount.textContent = `${filtered} of ${total} items`;
}
}
}
clearAllFilters() {
this.filters.clear();
this.searchTerm = '';
// Reset UI controls
document.getElementById('globalSearch').value = '';
document.getElementById('categoryFilter').value = '';
document.getElementById('statusFilter').value = '';
this.filteredData = [...this.data];
this.renderTable();
this.updateResultsCount();
}
handleKeyboardNavigation(e) {
const focusedRow = this.tbody.querySelector('tr:focus');
if (!focusedRow) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
const nextRow = focusedRow.nextElementSibling;
if (nextRow) nextRow.focus();
break;
case 'ArrowUp':
e.preventDefault();
const prevRow = focusedRow.previousElementSibling;
if (prevRow) prevRow.focus();
break;
case 'Enter':
case ' ':
e.preventDefault();
this.handleRowAction(focusedRow);
break;
}
}
handleRowAction(row) {
// Custom action when row is selected
row.classList.toggle('selected');
console.log('Row action:', row);
}
// Utility functions
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Export functionality
exportToCSV() {
const headers = Object.keys(this.data[0] || {});
const csvContent = [
headers.join(','),
...this.filteredData.map(row =>
headers.map(header => `"${String(row[header]).replace(/"/g, '""')}"`).join(',')
)
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'table-data.csv';
link.click();
window.URL.revokeObjectURL(url);
}
// API for external control
getFilteredData() {
return this.filteredData;
}
getCurrentFilters() {
return {
search: this.searchTerm,
filters: Object.fromEntries(this.filters),
sort: this.currentSort
};
}
restoreState(state) {
if (state.search) {
document.getElementById('globalSearch').value = state.search;
this.searchTerm = state.search;
}
if (state.filters) {
Object.entries(state.filters).forEach(([column, value]) => {
this.setFilter(column, value);
const select = document.getElementById(`${column}Filter`);
if (select) select.value = value;
});
}
if (state.sort && state.sort.column) {
this.currentSort = state.sort;
this.sortData(state.sort.column, 'string', state.sort.direction);
this.updateSortIndicators();
}
this.renderTable();
this.updateResultsCount();
}
}
// Sample data structure
const sampleData = [
{
name: "User Authentication API",
category: "Backend",
status: "Active",
priority: 1,
date: "2025-10-01",
size: 2.5
},
{
name: "Frontend Dashboard",
category: "Frontend",
status: "In Development",
priority: 2,
date: "2025-09-28",
size: 15.3
},
{
name: "Database Migration Scripts",
category: "Database",
status: "Completed",
priority: 3,
date: "2025-09-25",
size: 0.8
},
{
name: "Payment Processing Service",
category: "Backend",
status: "Testing",
priority: 1,
date: "2025-10-03",
size: 4.7
},
{
name: "Mobile App Components",
category: "Mobile",
status: "Active",
priority: 2,
date: "2025-09-30",
size: 8.2
},
{
name: "Analytics Dashboard",
category: "Frontend",
status: "Planning",
priority: 3,
date: "2025-09-15",
size: 12.1
},
{
name: "Email Notification System",
category: "Backend",
status: "Active",
priority: 2,
date: "2025-09-22",
size: 3.4
},
{
name: "User Profile Management",
category: "Frontend",
status: "In Development",
priority: 1,
date: "2025-10-02",
size: 6.9
}
];
// Initialize the interactive table
document.addEventListener('DOMContentLoaded', function() {
const interactiveTable = new InteractiveTable('dataTable', {
enableSearch: true,
enableSort: true,
enableFilter: true,
highlightMatches: true,
debounceDelay: 250
});
interactiveTable.setData(sampleData);
// Add export functionality
const exportButton = document.createElement('button');
exportButton.textContent = 'Export CSV';
exportButton.className = 'clear-filters';
exportButton.addEventListener('click', () => {
interactiveTable.exportToCSV();
});
document.querySelector('.table-controls').appendChild(exportButton);
});
Advanced Filtering Techniques
Custom Filter Components
// advanced-filters.js - Sophisticated filtering system
class AdvancedFilterSystem {
constructor(table, options = {}) {
this.table = table;
this.options = {
enableRangeFilters: true,
enableRegexSearch: false,
enableMultiSelect: true,
enableDateRanges: true,
...options
};
this.filterDefinitions = new Map();
this.activeFilters = new Map();
this.filterOperators = {
'equals': (value, filter) => value === filter,
'contains': (value, filter) => String(value).toLowerCase().includes(filter.toLowerCase()),
'startsWith': (value, filter) => String(value).toLowerCase().startsWith(filter.toLowerCase()),
'endsWith': (value, filter) => String(value).toLowerCase().endsWith(filter.toLowerCase()),
'greaterThan': (value, filter) => parseFloat(value) > parseFloat(filter),
'lessThan': (value, filter) => parseFloat(value) < parseFloat(filter),
'between': (value, filter) => {
const num = parseFloat(value);
return num >= filter.min && num <= filter.max;
},
'dateAfter': (value, filter) => new Date(value) > new Date(filter),
'dateBefore': (value, filter) => new Date(value) < new Date(filter),
'dateRange': (value, filter) => {
const date = new Date(value);
return date >= new Date(filter.start) && date <= new Date(filter.end);
},
'regex': (value, filter) => {
try {
return new RegExp(filter, 'i').test(String(value));
} catch {
return false;
}
}
};
this.init();
}
init() {
this.createAdvancedFilterUI();
this.setupFilterEvents();
}
defineColumnFilter(column, filterConfig) {
this.filterDefinitions.set(column, {
type: filterConfig.type || 'text',
operators: filterConfig.operators || ['contains'],
options: filterConfig.options || [],
label: filterConfig.label || column,
...filterConfig
});
}
createAdvancedFilterUI() {
const container = document.createElement('div');
container.className = 'advanced-filters';
container.innerHTML = `
<div class="filter-header">
<h3>Advanced Filters</h3>
<button class="toggle-filters" id="toggleAdvanced">Show Filters</button>
</div>
<div class="filter-panel" id="filterPanel" style="display: none;">
<div class="filter-controls" id="filterControls"></div>
<div class="filter-actions">
<button class="apply-filters" id="applyFilters">Apply Filters</button>
<button class="clear-filters" id="clearAdvancedFilters">Clear All</button>
<button class="save-filter-set" id="saveFilterSet">Save Filter Set</button>
</div>
</div>
`;
// Insert before the table
this.table.parentNode.insertBefore(container, this.table);
this.setupAdvancedFilterEvents(container);
}
setupAdvancedFilterEvents(container) {
const toggleButton = container.querySelector('#toggleAdvanced');
const filterPanel = container.querySelector('#filterPanel');
toggleButton.addEventListener('click', () => {
const isVisible = filterPanel.style.display !== 'none';
filterPanel.style.display = isVisible ? 'none' : 'block';
toggleButton.textContent = isVisible ? 'Show Filters' : 'Hide Filters';
});
container.querySelector('#applyFilters').addEventListener('click', () => {
this.applyAdvancedFilters();
});
container.querySelector('#clearAdvancedFilters').addEventListener('click', () => {
this.clearAllAdvancedFilters();
});
container.querySelector('#saveFilterSet').addEventListener('click', () => {
this.saveFilterSet();
});
}
createFilterControl(column, config) {
const controlContainer = document.createElement('div');
controlContainer.className = 'filter-control';
switch (config.type) {
case 'text':
controlContainer.innerHTML = this.createTextFilter(column, config);
break;
case 'select':
controlContainer.innerHTML = this.createSelectFilter(column, config);
break;
case 'multiselect':
controlContainer.innerHTML = this.createMultiSelectFilter(column, config);
break;
case 'range':
controlContainer.innerHTML = this.createRangeFilter(column, config);
break;
case 'date':
controlContainer.innerHTML = this.createDateFilter(column, config);
break;
case 'daterange':
controlContainer.innerHTML = this.createDateRangeFilter(column, config);
break;
}
return controlContainer;
}
createTextFilter(column, config) {
return `
<div class="filter-group">
<label class="filter-label">${config.label}</label>
<div class="text-filter-container">
<select class="filter-operator" data-column="${column}">
${config.operators.map(op =>
`<option value="${op}">${this.getOperatorLabel(op)}</option>`
).join('')}
</select>
<input type="text"
class="filter-value"
data-column="${column}"
placeholder="Enter ${config.label.toLowerCase()}...">
</div>
</div>
`;
}
createSelectFilter(column, config) {
return `
<div class="filter-group">
<label class="filter-label">${config.label}</label>
<select class="filter-select" data-column="${column}">
<option value="">All ${config.label}</option>
${config.options.map(option =>
`<option value="${option.value || option}">${option.label || option}</option>`
).join('')}
</select>
</div>
`;
}
createMultiSelectFilter(column, config) {
return `
<div class="filter-group">
<label class="filter-label">${config.label}</label>
<div class="multiselect-container">
<button class="multiselect-toggle" data-column="${column}">
Select ${config.label}
<span class="multiselect-count"></span>
</button>
<div class="multiselect-dropdown" style="display: none;">
${config.options.map(option => `
<label class="multiselect-option">
<input type="checkbox"
value="${option.value || option}"
data-column="${column}">
<span>${option.label || option}</span>
</label>
`).join('')}
</div>
</div>
</div>
`;
}
createRangeFilter(column, config) {
return `
<div class="filter-group">
<label class="filter-label">${config.label}</label>
<div class="range-filter-container">
<input type="number"
class="range-min"
data-column="${column}"
placeholder="Min ${config.label.toLowerCase()}"
min="${config.min || 0}"
max="${config.max || 100}">
<span class="range-separator">to</span>
<input type="number"
class="range-max"
data-column="${column}"
placeholder="Max ${config.label.toLowerCase()}"
min="${config.min || 0}"
max="${config.max || 100}">
</div>
</div>
`;
}
createDateRangeFilter(column, config) {
return `
<div class="filter-group">
<label class="filter-label">${config.label}</label>
<div class="date-range-container">
<input type="date"
class="date-start"
data-column="${column}">
<span class="date-separator">to</span>
<input type="date"
class="date-end"
data-column="${column}">
</div>
</div>
`;
}
getOperatorLabel(operator) {
const labels = {
'equals': 'Equals',
'contains': 'Contains',
'startsWith': 'Starts With',
'endsWith': 'Ends With',
'greaterThan': 'Greater Than',
'lessThan': 'Less Than',
'regex': 'Regex Pattern'
};
return labels[operator] || operator;
}
applyAdvancedFilters() {
this.activeFilters.clear();
// Collect all filter values
document.querySelectorAll('.filter-control').forEach(control => {
const column = control.querySelector('[data-column]')?.dataset.column;
if (!column) return;
const config = this.filterDefinitions.get(column);
if (!config) return;
const filterValue = this.extractFilterValue(control, config);
if (filterValue) {
this.activeFilters.set(column, filterValue);
}
});
// Apply filters to table
this.filterTableData();
}
extractFilterValue(control, config) {
switch (config.type) {
case 'text':
const operator = control.querySelector('.filter-operator').value;
const value = control.querySelector('.filter-value').value.trim();
return value ? { operator, value } : null;
case 'select':
const selectValue = control.querySelector('.filter-select').value;
return selectValue ? { operator: 'equals', value: selectValue } : null;
case 'multiselect':
const checkedOptions = [...control.querySelectorAll('input[type="checkbox"]:checked')]
.map(cb => cb.value);
return checkedOptions.length > 0 ? { operator: 'in', value: checkedOptions } : null;
case 'range':
const min = control.querySelector('.range-min').value;
const max = control.querySelector('.range-max').value;
if (min || max) {
return {
operator: 'between',
value: { min: min || -Infinity, max: max || Infinity }
};
}
return null;
case 'daterange':
const startDate = control.querySelector('.date-start').value;
const endDate = control.querySelector('.date-end').value;
if (startDate || endDate) {
return {
operator: 'dateRange',
value: { start: startDate, end: endDate }
};
}
return null;
}
return null;
}
filterTableData() {
const rows = this.table.querySelectorAll('tbody tr');
let visibleCount = 0;
rows.forEach(row => {
let shouldShow = true;
for (const [column, filter] of this.activeFilters) {
const cell = row.querySelector(`[data-column="${column}"]`);
if (!cell) continue;
const cellValue = cell.textContent.trim();
const operator = this.filterOperators[filter.operator];
if (operator && !operator(cellValue, filter.value)) {
shouldShow = false;
break;
}
}
row.style.display = shouldShow ? '' : 'none';
if (shouldShow) visibleCount++;
});
this.updateFilterResults(visibleCount, rows.length);
}
updateFilterResults(visible, total) {
let resultInfo = document.querySelector('.filter-results-info');
if (!resultInfo) {
resultInfo = document.createElement('div');
resultInfo.className = 'filter-results-info';
this.table.parentNode.insertBefore(resultInfo, this.table);
}
if (visible === total) {
resultInfo.textContent = `Showing all ${total} items`;
} else {
resultInfo.textContent = `Showing ${visible} of ${total} items`;
}
}
clearAllAdvancedFilters() {
this.activeFilters.clear();
// Reset all filter controls
document.querySelectorAll('.filter-control input').forEach(input => {
if (input.type === 'checkbox') {
input.checked = false;
} else {
input.value = '';
}
});
document.querySelectorAll('.filter-control select').forEach(select => {
select.selectedIndex = 0;
});
// Show all rows
this.table.querySelectorAll('tbody tr').forEach(row => {
row.style.display = '';
});
this.updateFilterResults(this.table.querySelectorAll('tbody tr').length,
this.table.querySelectorAll('tbody tr').length);
}
saveFilterSet() {
const filterSet = {
name: prompt('Enter name for this filter set:'),
filters: Object.fromEntries(this.activeFilters),
timestamp: new Date().toISOString()
};
if (filterSet.name) {
const savedSets = JSON.parse(localStorage.getItem('tableFilterSets') || '[]');
savedSets.push(filterSet);
localStorage.setItem('tableFilterSets', JSON.stringify(savedSets));
alert(`Filter set "${filterSet.name}" saved successfully!`);
}
}
loadFilterSet(filterSet) {
this.clearAllAdvancedFilters();
// Apply saved filters
Object.entries(filterSet.filters).forEach(([column, filter]) => {
this.activeFilters.set(column, filter);
// Update UI controls to reflect loaded filters
this.updateFilterControlsFromState(column, filter);
});
this.filterTableData();
}
updateFilterControlsFromState(column, filter) {
const control = document.querySelector(`.filter-control [data-column="${column}"]`);
if (!control) return;
const config = this.filterDefinitions.get(column);
if (!config) return;
// Update UI based on filter type and value
// Implementation would vary based on filter type
switch (config.type) {
case 'text':
control.closest('.filter-control').querySelector('.filter-operator').value = filter.operator;
control.closest('.filter-control').querySelector('.filter-value').value = filter.value;
break;
case 'select':
control.value = filter.value;
break;
// Add cases for other filter types...
}
}
}
// Enhanced CSS for advanced filters
const advancedFilterStyles = `
.advanced-filters {
margin: 20px 0;
border: 1px solid #e1e5e9;
border-radius: 8px;
background-color: #fafbfc;
}
.filter-header {
padding: 15px 20px;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.toggle-filters {
padding: 6px 12px;
background-color: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.filter-panel {
padding: 20px;
}
.filter-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-label {
font-weight: 500;
font-size: 14px;
color: #24292f;
}
.text-filter-container {
display: flex;
gap: 8px;
}
.filter-operator {
flex: 0 0 auto;
width: 120px;
}
.filter-value {
flex: 1;
}
.range-filter-container {
display: flex;
align-items: center;
gap: 8px;
}
.range-separator,
.date-separator {
color: #656d76;
font-size: 14px;
}
.multiselect-container {
position: relative;
}
.multiselect-toggle {
width: 100%;
padding: 8px 12px;
background-color: white;
border: 1px solid #d0d7de;
border-radius: 6px;
text-align: left;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.multiselect-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #d0d7de;
border-top: none;
border-radius: 0 0 6px 6px;
max-height: 200px;
overflow-y: auto;
z-index: 10;
}
.multiselect-option {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
gap: 8px;
}
.multiselect-option:hover {
background-color: #f6f8fa;
}
.filter-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.filter-actions button {
padding: 8px 16px;
border: 1px solid #d0d7de;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.apply-filters {
background-color: #2da44e;
color: white;
border-color: #2da44e;
}
.apply-filters:hover {
background-color: #2c974b;
}
.filter-results-info {
margin: 10px 0;
font-size: 14px;
color: #656d76;
text-align: right;
}
.priority-badge,
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.priority-badge.high {
background-color: #ffeef0;
color: #d1242f;
}
.priority-badge.medium {
background-color: #fff8e1;
color: #bf8700;
}
.priority-badge.low {
background-color: #f0f9ff;
color: #0969da;
}
.status-badge.active {
background-color: #dcfdf7;
color: #059669;
}
.status-badge.in-development {
background-color: #fef3c7;
color: #d97706;
}
.status-badge.completed {
background-color: #dcfce7;
color: #16a34a;
}
.status-badge.testing {
background-color: #e0e7ff;
color: #4338ca;
}
.status-badge.planning {
background-color: #f3f4f6;
color: #6b7280;
}
`;
// Inject advanced filter styles
const styleSheet = document.createElement('style');
styleSheet.textContent = advancedFilterStyles;
document.head.appendChild(styleSheet);
Server-Side Integration Patterns
API-Based Filtering and Sorting
// server-side-integration.js - Backend integration for large datasets
class ServerSideTableManager {
constructor(apiEndpoint, options = {}) {
this.apiEndpoint = apiEndpoint;
this.options = {
pageSize: 25,
enableServerSort: true,
enableServerFilter: true,
cacheResults: true,
debounceDelay: 500,
...options
};
this.currentPage = 1;
this.totalPages = 1;
this.totalItems = 0;
this.currentFilters = {};
this.currentSort = {};
this.isLoading = false;
this.cache = new Map();
this.init();
}
init() {
this.createServerTableUI();
this.setupServerSideEvents();
this.loadInitialData();
}
createServerTableUI() {
const container = document.createElement('div');
container.className = 'server-side-table-container';
container.innerHTML = `
<div class="server-table-controls">
<div class="search-section">
<input type="text"
id="serverSearch"
class="server-search-input"
placeholder="Search across all data...">
<button id="serverSearchBtn" class="search-button">Search</button>
</div>
<div class="filter-section">
<select id="serverCategoryFilter" class="server-filter-select">
<option value="">All Categories</option>
</select>
<select id="serverStatusFilter" class="server-filter-select">
<option value="">All Statuses</option>
</select>
<button id="clearServerFilters" class="clear-button">Clear Filters</button>
</div>
<div class="pagination-info">
<span id="serverPaginationInfo"></span>
</div>
</div>
<div class="server-table-wrapper">
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="loading-spinner"></div>
<span>Loading data...</span>
</div>
<table class="server-side-table" id="serverTable">
<thead>
<tr>
<th data-column="name" class="sortable-header">
Name
<span class="sort-icon"></span>
</th>
<th data-column="category" class="sortable-header">
Category
<span class="sort-icon"></span>
</th>
<th data-column="status" class="sortable-header">
Status
<span class="sort-icon"></span>
</th>
<th data-column="priority" class="sortable-header">
Priority
<span class="sort-icon"></span>
</th>
<th data-column="date" class="sortable-header">
Date
<span class="sort-icon"></span>
</th>
<th data-column="size" class="sortable-header">
Size
<span class="sort-icon"></span>
</th>
</tr>
</thead>
<tbody id="serverTableBody">
</tbody>
</table>
</div>
<div class="server-pagination" id="serverPagination">
<button id="firstPage" class="page-button">First</button>
<button id="prevPage" class="page-button">Previous</button>
<div class="page-numbers" id="pageNumbers"></div>
<button id="nextPage" class="page-button">Next</button>
<button id="lastPage" class="page-button">Last</button>
</div>
`;
document.body.appendChild(container);
}
setupServerSideEvents() {
// Search functionality
const searchInput = document.getElementById('serverSearch');
const searchButton = document.getElementById('serverSearchBtn');
let searchTimeout;
const handleSearch = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.performSearch(searchInput.value);
}, this.options.debounceDelay);
};
searchInput.addEventListener('input', handleSearch);
searchButton.addEventListener('click', () => {
this.performSearch(searchInput.value);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.performSearch(searchInput.value);
}
});
// Sorting functionality
document.getElementById('serverTable').addEventListener('click', (e) => {
const header = e.target.closest('.sortable-header');
if (header) {
this.handleServerSort(header.dataset.column);
}
});
// Filtering functionality
document.getElementById('serverCategoryFilter').addEventListener('change', (e) => {
this.setServerFilter('category', e.target.value);
});
document.getElementById('serverStatusFilter').addEventListener('change', (e) => {
this.setServerFilter('status', e.target.value);
});
document.getElementById('clearServerFilters').addEventListener('click', () => {
this.clearAllServerFilters();
});
// Pagination functionality
this.setupPaginationEvents();
}
setupPaginationEvents() {
document.getElementById('firstPage').addEventListener('click', () => {
this.goToPage(1);
});
document.getElementById('prevPage').addEventListener('click', () => {
this.goToPage(Math.max(1, this.currentPage - 1));
});
document.getElementById('nextPage').addEventListener('click', () => {
this.goToPage(Math.min(this.totalPages, this.currentPage + 1));
});
document.getElementById('lastPage').addEventListener('click', () => {
this.goToPage(this.totalPages);
});
}
async loadInitialData() {
await this.fetchData();
await this.loadFilterOptions();
}
async fetchData(resetPage = false) {
if (resetPage) {
this.currentPage = 1;
}
const params = new URLSearchParams({
page: this.currentPage,
pageSize: this.options.pageSize,
...this.currentFilters,
...this.currentSort
});
// Check cache first
const cacheKey = params.toString();
if (this.options.cacheResults && this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey);
this.renderServerData(cachedData);
return cachedData;
}
this.setLoading(true);
try {
const response = await fetch(`${this.apiEndpoint}?${params}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Cache the response
if (this.options.cacheResults) {
this.cache.set(cacheKey, data);
// Limit cache size
if (this.cache.size > 50) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
this.renderServerData(data);
return data;
} catch (error) {
console.error('Error fetching data:', error);
this.handleError(error);
} finally {
this.setLoading(false);
}
}
renderServerData(data) {
const tbody = document.getElementById('serverTableBody');
if (!data.items || data.items.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="no-data">
No data found matching your criteria.
</td>
</tr>
`;
this.updatePagination(0, 0, 0);
return;
}
tbody.innerHTML = data.items.map(item => `
<tr>
<td>${this.escapeHtml(item.name)}</td>
<td>${this.escapeHtml(item.category)}</td>
<td><span class="status-badge status-${item.status.toLowerCase().replace(/\s+/g, '-')}">${item.status}</span></td>
<td><span class="priority-badge priority-${item.priority}">${this.formatPriority(item.priority)}</span></td>
<td>${this.formatDate(item.date)}</td>
<td>${item.size} MB</td>
</tr>
`).join('');
this.totalPages = Math.ceil(data.total / this.options.pageSize);
this.totalItems = data.total;
this.updatePagination(data.total, this.currentPage, this.totalPages);
this.updateSortIndicators();
}
updatePagination(total, currentPage, totalPages) {
const paginationInfo = document.getElementById('serverPaginationInfo');
const start = total === 0 ? 0 : (currentPage - 1) * this.options.pageSize + 1;
const end = Math.min(currentPage * this.options.pageSize, total);
paginationInfo.textContent = `Showing ${start}-${end} of ${total} results`;
// Update pagination buttons
document.getElementById('firstPage').disabled = currentPage === 1;
document.getElementById('prevPage').disabled = currentPage === 1;
document.getElementById('nextPage').disabled = currentPage === totalPages || totalPages === 0;
document.getElementById('lastPage').disabled = currentPage === totalPages || totalPages === 0;
// Update page numbers
this.renderPageNumbers(currentPage, totalPages);
}
renderPageNumbers(currentPage, totalPages) {
const pageNumbers = document.getElementById('pageNumbers');
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
// Adjust start page if we're near the end
if (endPage - startPage + 1 < maxVisible) {
startPage = Math.max(1, endPage - maxVisible + 1);
}
let html = '';
for (let i = startPage; i <= endPage; i++) {
html += `
<button class="page-number ${i === currentPage ? 'active' : ''}"
data-page="${i}">${i}</button>
`;
}
pageNumbers.innerHTML = html;
// Add click events to page numbers
pageNumbers.querySelectorAll('.page-number').forEach(button => {
button.addEventListener('click', (e) => {
const page = parseInt(e.target.dataset.page);
this.goToPage(page);
});
});
}
async performSearch(searchTerm) {
this.currentFilters.search = searchTerm.trim();
await this.fetchData(true);
}
async handleServerSort(column) {
if (this.currentSort.column === column) {
this.currentSort.direction = this.currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
this.currentSort.column = column;
this.currentSort.direction = 'asc';
}
await this.fetchData(true);
}
async setServerFilter(filterType, value) {
if (value === '') {
delete this.currentFilters[filterType];
} else {
this.currentFilters[filterType] = value;
}
await this.fetchData(true);
}
async clearAllServerFilters() {
this.currentFilters = {};
// Reset UI
document.getElementById('serverSearch').value = '';
document.getElementById('serverCategoryFilter').value = '';
document.getElementById('serverStatusFilter').value = '';
await this.fetchData(true);
}
async goToPage(page) {
if (page !== this.currentPage && page >= 1 && page <= this.totalPages) {
this.currentPage = page;
await this.fetchData();
}
}
updateSortIndicators() {
// Clear all sort indicators
document.querySelectorAll('.sort-icon').forEach(icon => {
icon.className = 'sort-icon';
});
// Set active sort indicator
if (this.currentSort.column) {
const header = document.querySelector(`[data-column="${this.currentSort.column}"] .sort-icon`);
if (header) {
header.className = `sort-icon active ${this.currentSort.direction}`;
}
}
}
async loadFilterOptions() {
try {
const response = await fetch(`${this.apiEndpoint}/filters`);
const filterData = await response.json();
// Populate category filter
const categoryFilter = document.getElementById('serverCategoryFilter');
categoryFilter.innerHTML = '<option value="">All Categories</option>' +
filterData.categories.map(cat => `<option value="${cat}">${cat}</option>`).join('');
// Populate status filter
const statusFilter = document.getElementById('serverStatusFilter');
statusFilter.innerHTML = '<option value="">All Statuses</option>' +
filterData.statuses.map(status => `<option value="${status}">${status}</option>`).join('');
} catch (error) {
console.warn('Could not load filter options:', error);
}
}
setLoading(isLoading) {
this.isLoading = isLoading;
const overlay = document.getElementById('loadingOverlay');
overlay.style.display = isLoading ? 'flex' : 'none';
// Disable interactive elements while loading
const controls = document.querySelectorAll('.server-table-controls input, .server-table-controls select, .server-table-controls button, .sortable-header');
controls.forEach(control => {
control.disabled = isLoading;
if (isLoading) {
control.style.pointerEvents = 'none';
} else {
control.style.pointerEvents = '';
}
});
}
handleError(error) {
const tbody = document.getElementById('serverTableBody');
tbody.innerHTML = `
<tr>
<td colspan="6" class="error-message">
<div class="error-content">
<strong>Error loading data</strong>
<p>${error.message}</p>
<button class="retry-button" onclick="this.closest('tr').style.display='none'; location.reload()">
Retry
</button>
</div>
</td>
</tr>
`;
}
// Utility methods
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatDate(dateString) {
return new Date(dateString).toLocaleDateString();
}
formatPriority(priority) {
const priorities = { 1: 'High', 2: 'Medium', 3: 'Low' };
return priorities[priority] || priority;
}
// Public API methods
getCurrentState() {
return {
page: this.currentPage,
filters: { ...this.currentFilters },
sort: { ...this.currentSort }
};
}
restoreState(state) {
this.currentPage = state.page || 1;
this.currentFilters = state.filters || {};
this.currentSort = state.sort || {};
// Update UI to reflect state
this.updateUIFromState();
this.fetchData();
}
updateUIFromState() {
if (this.currentFilters.search) {
document.getElementById('serverSearch').value = this.currentFilters.search;
}
if (this.currentFilters.category) {
document.getElementById('serverCategoryFilter').value = this.currentFilters.category;
}
if (this.currentFilters.status) {
document.getElementById('serverStatusFilter').value = this.currentFilters.status;
}
}
clearCache() {
this.cache.clear();
}
}
// Server-side styles
const serverSideStyles = `
.server-side-table-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
margin: 20px 0;
}
.server-table-controls {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background-color: #f6f8fa;
border-radius: 8px;
}
.search-section {
display: flex;
gap: 8px;
align-items: center;
}
.server-search-input {
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 14px;
min-width: 300px;
}
.search-button,
.clear-button {
padding: 8px 16px;
background-color: #2da44e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.clear-button {
background-color: #f6f8fa;
color: #24292f;
border: 1px solid #d0d7de;
}
.search-button:hover {
background-color: #2c974b;
}
.clear-button:hover {
background-color: #f1f3f4;
}
.filter-section {
display: flex;
gap: 10px;
align-items: center;
}
.server-filter-select {
padding: 8px 12px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 14px;
background-color: white;
}
.pagination-info {
margin-left: auto;
font-size: 14px;
color: #656d76;
}
.server-table-wrapper {
position: relative;
overflow-x: auto;
border: 1px solid #d0d7de;
border-radius: 8px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
gap: 15px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f4f6;
border-top: 3px solid #2da44e;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.server-side-table {
width: 100%;
border-collapse: collapse;
background-color: white;
}
.server-side-table th,
.server-side-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e1e5e9;
}
.server-side-table th {
background-color: #f6f8fa;
font-weight: 600;
position: relative;
}
.sortable-header {
cursor: pointer;
user-select: none;
padding-right: 30px;
}
.sortable-header:hover {
background-color: #f1f3f4;
}
.sort-icon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
opacity: 0.3;
font-size: 12px;
}
.sort-icon.active {
opacity: 1;
}
.sort-icon.asc::after {
content: "▲";
}
.sort-icon.desc::after {
content: "▼";
}
.sort-icon:not(.active)::after {
content: "↕";
}
.server-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 20px;
padding: 20px;
}
.page-button,
.page-number {
padding: 8px 12px;
border: 1px solid #d0d7de;
background-color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.page-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-number.active {
background-color: #2da44e;
color: white;
border-color: #2da44e;
}
.page-button:hover:not(:disabled),
.page-number:hover:not(.active) {
background-color: #f6f8fa;
}
.no-data,
.error-message {
text-align: center;
padding: 40px;
color: #656d76;
font-style: italic;
}
.error-content {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.retry-button {
padding: 8px 16px;
background-color: #2da44e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
`;
// Inject server-side styles
const serverStyleSheet = document.createElement('style');
serverStyleSheet.textContent = serverSideStyles;
document.head.appendChild(serverStyleSheet);
Integration with Static Site Generators
Interactive table functionality integrates seamlessly with comprehensive documentation systems. When combined with navigation menu structures and site organization, dynamic filtering and sorting enable sophisticated content discovery experiences that scale effectively with growing documentation repositories.
For advanced content presentation, interactive tables work effectively with custom CSS styling frameworks to create branded documentation interfaces that maintain consistent visual identity while providing powerful data manipulation capabilities that enhance user engagement and information accessibility.
When building comprehensive content management systems, table interactivity complements performance optimization strategies by implementing efficient client-side processing that reduces server load while maintaining responsive user experiences across different device types and connection speeds.
Jekyll Integration Example
<!-- _includes/interactive-table.html - Jekyll template for interactive tables -->
<div class="interactive-table-container" data-table-config='{{ include.config | jsonify }}'>
{% if include.search %}
<div class="table-controls">
<input type="text" class="table-search" placeholder="Search {{ include.title | default: 'table' }}...">
{% if include.filters %}
<div class="table-filters">
{% for filter in include.filters %}
<select class="table-filter" data-column="{{ filter.column }}">
<option value="">All {{ filter.label }}</option>
{% for option in filter.options %}
<option value="{{ option.value | default: option }}">{{ option.label | default: option }}</option>
{% endfor %}
</select>
{% endfor %}
</div>
{% endif %}
<button class="clear-filters">Clear Filters</button>
</div>
{% endif %}
<div class="table-wrapper">
<table class="interactive-table">
<thead>
<tr>
{% for header in include.headers %}
<th data-column="{{ header.key }}"
data-type="{{ header.type | default: 'string' }}"
{% if header.sortable != false %}class="sortable"{% endif %}>
{{ header.label | default: header.key }}
{% if header.sortable != false %}
<span class="sort-indicator"></span>
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in include.data %}
<tr>
{% for header in include.headers %}
<td data-column="{{ header.key }}">
{% assign cell_value = row[header.key] %}
{% case header.type %}
{% when 'date' %}
{{ cell_value | date: "%B %d, %Y" }}
{% when 'currency' %}
${{ cell_value | round: 2 }}
{% when 'boolean' %}
{% if cell_value %}✅{% else %}❌{% endif %}
{% when 'link' %}
<a href="{{ cell_value.url }}" {% if cell_value.external %}target="_blank" rel="noopener"{% endif %}>
{{ cell_value.text | default: cell_value.url }}
</a>
{% when 'badge' %}
<span class="badge badge-{{ cell_value | slugify }}">{{ cell_value }}</span>
{% else %}
{{ cell_value }}
{% endcase %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if include.pagination %}
<div class="table-pagination">
<div class="pagination-info">
<span class="results-count"></span>
</div>
<div class="pagination-controls">
<button class="page-btn" data-page="first">First</button>
<button class="page-btn" data-page="prev">Previous</button>
<div class="page-numbers"></div>
<button class="page-btn" data-page="next">Next</button>
<button class="page-btn" data-page="last">Last</button>
</div>
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize interactive tables
document.querySelectorAll('.interactive-table-container').forEach(container => {
const config = JSON.parse(container.dataset.tableConfig || '{}');
new InteractiveTable(container.querySelector('.interactive-table'), config);
});
});
</script>
Hugo Shortcode Implementation
<div class="hugo-interactive-table" id="_container">
<div class="table-controls">
<input type="text"
class="global-search"
placeholder="Search table..."
data-table="">
<div class="filter-controls" id="_filters">
<!-- Filters will be populated by JavaScript -->
</div>
<div class="table-info">
<span class="row-count" id="_count"></span>
</div>
</div>
<table class="interactive-data-table" id="">
<thead>
<tr>
<th data-column=""
class="sortable-column">
<span class="sort-icon" data-column=""></span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td data-column="">
<a href=""
target="_blank" rel="noopener">
</a>
null
</td>
</tr>
</tbody>
</table>
</div>
<script>
(function() {
const tableId = '';
const container = document.getElementById(tableId + '_container');
const table = document.getElementById(tableId);
if (table && typeof InteractiveTable !== 'undefined') {
new InteractiveTable(table, {
enableSearch: ,
enableSort: ,
enableFilter: ,
containerId: tableId + '_container'
});
}
})();
</script>
Performance Considerations and Optimization
Memory Management for Large Datasets
// performance-optimized-table.js - Memory-efficient table handling
class PerformanceOptimizedTable {
constructor(tableElement, options = {}) {
this.table = tableElement;
this.options = {
virtualScrolling: true,
lazyLoading: true,
maxVisibleRows: 100,
bufferSize: 20,
debounceDelay: 300,
enableWorker: true,
chunkSize: 1000,
...options
};
this.allData = [];
this.filteredData = [];
this.visibleData = [];
this.viewportStart = 0;
this.viewportEnd = 0;
this.scrollContainer = null;
this.worker = null;
this.init();
}
init() {
this.setupVirtualScrolling();
this.initializeWorker();
this.setupEventListeners();
}
setupVirtualScrolling() {
if (!this.options.virtualScrolling) return;
// Create virtual scrolling container
this.scrollContainer = document.createElement('div');
this.scrollContainer.className = 'virtual-scroll-container';
this.scrollContainer.style.height = '400px';
this.scrollContainer.style.overflowY = 'auto';
// Create virtual content
this.virtualContent = document.createElement('div');
this.virtualContent.className = 'virtual-content';
// Move table into virtual container
this.table.parentNode.insertBefore(this.scrollContainer, this.table);
this.virtualContent.appendChild(this.table);
this.scrollContainer.appendChild(this.virtualContent);
// Setup scroll event with throttling
let scrollTimeout;
this.scrollContainer.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
this.handleVirtualScroll();
}, 16); // ~60fps
});
}
initializeWorker() {
if (!this.options.enableWorker || !window.Worker) return;
const workerScript = `
self.onmessage = function(e) {
const { type, data, options } = e.data;
switch (type) {
case 'FILTER_DATA':
const filtered = filterData(data.items, data.filters, options);
self.postMessage({ type: 'FILTER_RESULT', data: filtered });
break;
case 'SORT_DATA':
const sorted = sortData(data.items, data.column, data.direction, data.type);
self.postMessage({ type: 'SORT_RESULT', data: sorted });
break;
case 'SEARCH_DATA':
const searchResult = searchData(data.items, data.query, options);
self.postMessage({ type: 'SEARCH_RESULT', data: searchResult });
break;
}
};
function filterData(items, filters, options) {
return items.filter(item => {
for (const [column, filter] of Object.entries(filters)) {
const value = String(item[column] || '').toLowerCase();
const filterValue = String(filter).toLowerCase();
if (!value.includes(filterValue)) {
return false;
}
}
return true;
});
}
function sortData(items, column, direction, type) {
return items.sort((a, b) => {
let aVal = a[column];
let bVal = b[column];
switch (type) {
case 'number':
aVal = parseFloat(aVal) || 0;
bVal = parseFloat(bVal) || 0;
break;
case 'date':
aVal = new Date(aVal);
bVal = new Date(bVal);
break;
default:
aVal = String(aVal).toLowerCase();
bVal = String(bVal).toLowerCase();
}
let comparison = 0;
if (aVal < bVal) comparison = -1;
if (aVal > bVal) comparison = 1;
return direction === 'desc' ? -comparison : comparison;
});
}
function searchData(items, query, options) {
const searchTerm = query.toLowerCase();
return items.filter(item => {
const searchableText = Object.values(item).join(' ').toLowerCase();
return searchableText.includes(searchTerm);
});
}
`;
const blob = new Blob([workerScript], { type: 'application/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
this.worker.onmessage = (e) => {
this.handleWorkerMessage(e.data);
};
}
setData(data) {
this.allData = data;
this.filteredData = [...data];
this.updateVirtualScroll();
}
handleVirtualScroll() {
const scrollTop = this.scrollContainer.scrollTop;
const containerHeight = this.scrollContainer.clientHeight;
const rowHeight = 40; // Estimated row height
// Calculate visible range
const startIndex = Math.floor(scrollTop / rowHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / rowHeight) + this.options.bufferSize,
this.filteredData.length
);
this.viewportStart = Math.max(0, startIndex - this.options.bufferSize);
this.viewportEnd = endIndex;
this.renderVisibleRows();
this.updateScrollerHeight();
}
renderVisibleRows() {
const tbody = this.table.querySelector('tbody');
const visibleRows = this.filteredData.slice(this.viewportStart, this.viewportEnd);
// Create row HTML
const rowsHTML = visibleRows.map((row, index) => {
const actualIndex = this.viewportStart + index;
const cells = Object.entries(row).map(([key, value]) =>
`<td data-column="${key}">${this.formatCellValue(key, value)}</td>`
).join('');
return `<tr data-index="${actualIndex}" style="transform: translateY(${actualIndex * 40}px)">${cells}</tr>`;
}).join('');
tbody.innerHTML = rowsHTML;
}
updateScrollerHeight() {
const totalHeight = this.filteredData.length * 40; // Estimated total height
this.virtualContent.style.height = `${totalHeight}px`;
this.table.style.position = 'relative';
}
// Chunked data processing for large datasets
async processLargeDataset(data, operation) {
const chunks = [];
const chunkSize = this.options.chunkSize;
for (let i = 0; i < data.length; i += chunkSize) {
chunks.push(data.slice(i, i + chunkSize));
}
const results = [];
for (const chunk of chunks) {
const result = await this.processChunk(chunk, operation);
results.push(...result);
// Yield control to prevent UI blocking
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
async processChunk(chunk, operation) {
return new Promise((resolve) => {
if (this.worker) {
// Use web worker for processing
this.worker.postMessage({
type: operation.type,
data: { items: chunk, ...operation.data },
options: operation.options || {}
});
// Worker will handle the response
resolve([]);
} else {
// Fallback to main thread processing
const result = this.processChunkSync(chunk, operation);
resolve(result);
}
});
}
processChunkSync(chunk, operation) {
switch (operation.type) {
case 'FILTER_DATA':
return chunk.filter(item => {
for (const [column, filter] of Object.entries(operation.data.filters)) {
const value = String(item[column] || '').toLowerCase();
const filterValue = String(filter).toLowerCase();
if (!value.includes(filterValue)) {
return false;
}
}
return true;
});
case 'SEARCH_DATA':
const searchTerm = operation.data.query.toLowerCase();
return chunk.filter(item => {
const searchableText = Object.values(item).join(' ').toLowerCase();
return searchableText.includes(searchTerm);
});
default:
return chunk;
}
}
handleWorkerMessage(message) {
switch (message.type) {
case 'FILTER_RESULT':
case 'SORT_RESULT':
case 'SEARCH_RESULT':
this.filteredData = message.data;
this.updateVirtualScroll();
break;
}
}
async performSearch(query) {
if (this.allData.length > this.options.chunkSize) {
this.filteredData = await this.processLargeDataset(this.allData, {
type: 'SEARCH_DATA',
data: { query }
});
} else {
if (this.worker) {
this.worker.postMessage({
type: 'SEARCH_DATA',
data: { items: this.allData, query }
});
} else {
this.filteredData = this.processChunkSync(this.allData, {
type: 'SEARCH_DATA',
data: { query }
});
}
}
this.updateVirtualScroll();
}
updateVirtualScroll() {
if (this.options.virtualScrolling) {
this.handleVirtualScroll();
} else {
this.renderAllRows();
}
}
renderAllRows() {
const tbody = this.table.querySelector('tbody');
const rowsHTML = this.filteredData.map((row, index) => {
const cells = Object.entries(row).map(([key, value]) =>
`<td data-column="${key}">${this.formatCellValue(key, value)}</td>`
).join('');
return `<tr data-index="${index}">${cells}</tr>`;
}).join('');
tbody.innerHTML = rowsHTML;
}
formatCellValue(column, value) {
// Implement cell formatting logic
return String(value);
}
// Memory management
destroy() {
if (this.worker) {
this.worker.terminate();
}
// Clear data references
this.allData = null;
this.filteredData = null;
this.visibleData = null;
// Remove event listeners
if (this.scrollContainer) {
this.scrollContainer.removeEventListener('scroll', this.handleVirtualScroll);
}
}
// Performance monitoring
getPerformanceMetrics() {
return {
totalRows: this.allData?.length || 0,
filteredRows: this.filteredData?.length || 0,
visibleRows: this.viewportEnd - this.viewportStart,
memoryUsage: this.estimateMemoryUsage(),
isUsingWorker: !!this.worker,
isVirtualScrolling: this.options.virtualScrolling
};
}
estimateMemoryUsage() {
// Rough estimation of memory usage
const dataSize = JSON.stringify(this.allData || []).length;
const filteredSize = JSON.stringify(this.filteredData || []).length;
return {
totalData: `${Math.round(dataSize / 1024)}KB`,
filteredData: `${Math.round(filteredSize / 1024)}KB`,
estimated: true
};
}
}
// Performance monitoring utility
class TablePerformanceMonitor {
constructor() {
this.metrics = {
renderTimes: [],
searchTimes: [],
filterTimes: [],
sortTimes: []
};
}
startMeasurement(operation) {
return {
operation,
startTime: performance.now(),
startMemory: performance.memory ? performance.memory.usedJSHeapSize : 0
};
}
endMeasurement(measurement) {
const endTime = performance.now();
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
const result = {
operation: measurement.operation,
duration: endTime - measurement.startTime,
memoryDelta: endMemory - measurement.startMemory,
timestamp: Date.now()
};
this.metrics[measurement.operation + 'Times'].push(result);
// Keep only last 100 measurements
if (this.metrics[measurement.operation + 'Times'].length > 100) {
this.metrics[measurement.operation + 'Times'].shift();
}
return result;
}
getAveragePerformance(operation) {
const times = this.metrics[operation + 'Times'] || [];
if (times.length === 0) return null;
const avgDuration = times.reduce((sum, t) => sum + t.duration, 0) / times.length;
const avgMemory = times.reduce((sum, t) => sum + Math.abs(t.memoryDelta), 0) / times.length;
return {
operation,
averageDuration: Math.round(avgDuration * 100) / 100,
averageMemoryDelta: Math.round(avgMemory / 1024), // KB
sampleCount: times.length
};
}
generatePerformanceReport() {
const operations = ['render', 'search', 'filter', 'sort'];
const report = {};
operations.forEach(op => {
report[op] = this.getAveragePerformance(op);
});
return report;
}
}
Troubleshooting Common Implementation Issues
Browser Compatibility Problems
Problem: Interactive features not working in older browsers
Solutions:
// browser-compatibility.js - Cross-browser compatibility layer
class CompatibilityLayer {
constructor() {
this.init();
}
init() {
this.polyfillIntersectionObserver();
this.polyfillCustomEvents();
this.polyfillArrayMethods();
this.handleIECompatibility();
}
polyfillIntersectionObserver() {
if (!window.IntersectionObserver) {
// Fallback for lazy loading
window.IntersectionObserver = function(callback, options) {
this.observe = function(element) {
// Simple visibility check fallback
const checkVisibility = () => {
const rect = element.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
if (isVisible) {
callback([{
target: element,
isIntersecting: true
}]);
}
};
// Check on scroll
window.addEventListener('scroll', checkVisibility);
checkVisibility(); // Initial check
};
this.disconnect = function() {
// Cleanup would go here
};
};
}
}
polyfillCustomEvents() {
if (!window.CustomEvent) {
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: null };
const evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
}
}
polyfillArrayMethods() {
// Array.from polyfill
if (!Array.from) {
Array.from = function(arrayLike, mapFn, thisArg) {
const items = Object(arrayLike);
const len = parseInt(items.length) || 0;
const result = new Array(len);
for (let i = 0; i < len; i++) {
if (i in items) {
if (mapFn) {
result[i] = mapFn.call(thisArg, items[i], i);
} else {
result[i] = items[i];
}
}
}
return result;
};
}
// Array.includes polyfill
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement) {
return this.indexOf(searchElement) !== -1;
};
}
}
handleIECompatibility() {
// Check if IE
const isIE = navigator.userAgent.indexOf('MSIE') !== -1 ||
navigator.userAgent.indexOf('Trident/') !== -1;
if (isIE) {
// Add IE-specific handling
document.documentElement.className += ' ie-browser';
// Object.assign polyfill
if (!Object.assign) {
Object.assign = function(target) {
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i];
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
}
}
}
// Fallback for modern features
createCompatibleEventListener(element, event, handler, options) {
if (element.addEventListener) {
element.addEventListener(event, handler, options);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
}
}
createCompatibleQuerySelector(selector) {
if (document.querySelectorAll) {
return document.querySelectorAll(selector);
} else {
// Fallback for very old browsers
const elements = [];
const allElements = document.getElementsByTagName('*');
for (let i = 0; i < allElements.length; i++) {
// Simple class selector fallback
if (selector.startsWith('.')) {
const className = selector.slice(1);
if (allElements[i].className.indexOf(className) !== -1) {
elements.push(allElements[i]);
}
}
}
return elements;
}
}
getCompatibleStyles(element) {
if (window.getComputedStyle) {
return window.getComputedStyle(element);
} else if (element.currentStyle) {
return element.currentStyle;
}
return {};
}
}
// Initialize compatibility layer
new CompatibilityLayer();
Performance Issues with Large Tables
Problem: Browser becomes unresponsive with large datasets
Solutions:
// performance-solutions.js - Solutions for large table performance
class LargeTableOptimizer {
constructor(table, options = {}) {
this.table = table;
this.options = {
maxRowsBeforeVirtualization: 1000,
useWebWorkers: true,
enableProgressiveLoading: true,
chunkSize: 100,
...options
};
this.init();
}
init() {
this.determineOptimizationStrategy();
}
determineOptimizationStrategy() {
const rowCount = this.table.querySelectorAll('tbody tr').length;
if (rowCount > this.options.maxRowsBeforeVirtualization) {
this.implementVirtualization();
} else if (rowCount > 500) {
this.implementProgressiveLoading();
} else {
this.implementBasicOptimizations();
}
}
implementVirtualization() {
console.log('Implementing virtual scrolling for large dataset');
// Convert table to virtual scrolling
const virtualContainer = document.createElement('div');
virtualContainer.className = 'virtual-table-container';
virtualContainer.style.height = '500px';
virtualContainer.style.overflow = 'auto';
this.table.parentNode.insertBefore(virtualContainer, this.table);
virtualContainer.appendChild(this.table);
// Implement virtual scrolling logic
this.setupVirtualScrolling(virtualContainer);
}
implementProgressiveLoading() {
console.log('Implementing progressive loading');
const tbody = this.table.querySelector('tbody');
const allRows = Array.from(tbody.querySelectorAll('tr'));
// Hide all rows initially
allRows.forEach(row => row.style.display = 'none');
// Show rows progressively
let currentIndex = 0;
const showNextChunk = () => {
const endIndex = Math.min(currentIndex + this.options.chunkSize, allRows.length);
for (let i = currentIndex; i < endIndex; i++) {
allRows[i].style.display = '';
}
currentIndex = endIndex;
if (currentIndex < allRows.length) {
requestAnimationFrame(showNextChunk);
}
};
showNextChunk();
}
implementBasicOptimizations() {
console.log('Implementing basic optimizations');
// Use CSS transforms for better performance
this.table.style.willChange = 'transform';
this.table.style.contain = 'layout style paint';
// Optimize reflows
this.table.style.tableLayout = 'fixed';
// Enable hardware acceleration
this.table.style.transform = 'translateZ(0)';
}
setupVirtualScrolling(container) {
const tbody = this.table.querySelector('tbody');
const allRows = Array.from(tbody.querySelectorAll('tr'));
const rowHeight = 40; // Estimated
let visibleStart = 0;
let visibleEnd = Math.min(50, allRows.length);
const renderVisibleRows = () => {
// Hide all rows
allRows.forEach(row => row.style.display = 'none');
// Show only visible rows
for (let i = visibleStart; i < visibleEnd; i++) {
if (allRows[i]) {
allRows[i].style.display = '';
allRows[i].style.transform = `translateY(${i * rowHeight}px)`;
}
}
// Set container height
tbody.style.height = `${allRows.length * rowHeight}px`;
tbody.style.position = 'relative';
};
container.addEventListener('scroll', () => {
const scrollTop = container.scrollTop;
const containerHeight = container.clientHeight;
visibleStart = Math.floor(scrollTop / rowHeight);
visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / rowHeight) + 10,
allRows.length
);
renderVisibleRows();
});
renderVisibleRows();
}
// Memory leak prevention
cleanup() {
// Remove event listeners
// Clear references
this.table = null;
}
}
// Automatic optimization based on table size
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.interactive-table').forEach(table => {
new LargeTableOptimizer(table);
});
});
Conclusion
Interactive Markdown table filtering and sorting transforms static documentation into dynamic, user-centric experiences that enhance content discovery and data exploration capabilities. By implementing comprehensive client-side filtering algorithms, server-side integration patterns, and performance optimization strategies, technical teams can create sophisticated documentation systems that scale efficiently while providing engaging user interactions that improve information accessibility and comprehension.
The key to successful interactive table implementation lies in balancing functionality with performance, choosing appropriate optimization strategies based on dataset size, and maintaining accessibility standards that ensure inclusive user experiences. Whether you’re building documentation platforms, data visualization tools, or content management systems, the techniques covered in this guide provide the foundation for creating professional interactive table experiences that meet modern user expectations.
Remember to test your implementations across different browsers and devices, implement proper error handling and loading states, and optimize performance based on your specific use case and data characteristics. With proper interactive filtering and sorting implementation, your Markdown-based content systems can deliver rich, dynamic experiences that engage users while maintaining the simplicity and portability that makes Markdown such an effective format for structured content presentation.