UNPKG

gentelella

Version:

Gentelella v4 — free admin template. 60 pages, 20 chart variants, fully interactive inbox & kanban, live theme generator, component playground, PWA-ready. Vite 8, vanilla JS, no Bootstrap, no jQuery.

169 lines (151 loc) 5.96 kB
// Gentelella 2026 v4 — DataTables integration // Dynamic-imports DataTables only when a [data-datatable] table is present. import { showToast } from './toast.js'; /** * Initialize DataTables on every `<table data-datatable>` on the page. * Reads `data-page-length` (default 10) on the table and `data-orderable="false"` * on individual `<th>` cells to disable sorting per column. * * Opt-in extras (just add the attribute to the `<table>`): * - `data-selectable` — wire row checkboxes (header checkbox = select all visible). * - `data-export="filename"` — show a CSV export button in the table card header. * * Lazily imports `datatables.net`; the import never fires on pages without a * matching table. * @returns {Promise<void>} */ export async function initTables() { const tables = document.querySelectorAll('table[data-datatable]'); if (!tables.length) {return;} const { default: DataTable } = await import('datatables.net'); tables.forEach((table) => { const columnDefs = []; table.querySelectorAll('thead th').forEach((th, i) => { if (th.dataset.orderable === 'false') { columnDefs.push({ targets: i, orderable: false }); } }); const dt = new DataTable(table, { pageLength: parseInt(table.dataset.pageLength || '10', 10), lengthChange: false, order: [], columnDefs, language: { search: '', searchPlaceholder: 'Search…', info: 'Showing _START_–_END_ of _TOTAL_', infoEmpty: 'No matching records', infoFiltered: '(of _MAX_ total)', zeroRecords: 'No matches found', paginate: { previous: '←', next: '→' } } }); if (table.hasAttribute('data-selectable') || hasRowCheckboxes(table)) { wireRowSelection(table); } if (table.hasAttribute('data-export')) { wireCsvExport(table, dt); } // DataTables 2 emits its search input without an accessible name. const searchInput = table.closest('.dt-container')?.querySelector('.dt-search input'); if (searchInput && !searchInput.hasAttribute('aria-label')) { searchInput.setAttribute('aria-label', 'Search table'); } }); } function hasRowCheckboxes(table) { return !!table.querySelector('thead input[type="checkbox"]'); } function wireRowSelection(table) { const headerCb = table.querySelector('thead input[type="checkbox"]'); const updateHeader = () => { if (!headerCb) {return;} const rowCbs = table.querySelectorAll('tbody input[type="checkbox"]'); const checked = [...rowCbs].filter((c) => c.checked).length; headerCb.checked = checked > 0 && checked === rowCbs.length; headerCb.indeterminate = checked > 0 && checked < rowCbs.length; table.classList.toggle('has-selection', checked > 0); const counter = table.closest('.card')?.querySelector('.bulk-selection-count'); if (counter) {counter.textContent = checked ? `${checked} selected` : '';} }; if (headerCb) { headerCb.addEventListener('change', () => { const rowCbs = table.querySelectorAll('tbody input[type="checkbox"]'); rowCbs.forEach((c) => { c.checked = headerCb.checked; }); updateHeader(); }); } table.addEventListener('change', (e) => { const cb = e.target.closest('tbody input[type="checkbox"]'); if (!cb) {return;} updateHeader(); }); } function wireCsvExport(table, dt) { const filename = (table.dataset.export || 'export') + '.csv'; // Find the export button. If a sibling already exists with [data-export-btn], // use it; otherwise inject a small button into the card header (next to the // existing actions, if any). let btn = table.closest('.card')?.querySelector('[data-export-btn]'); if (!btn) { const header = table.closest('.card')?.querySelector('.card-header'); if (!header) {return;} btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-outline btn-sm'; btn.setAttribute('data-export-btn', ''); btn.textContent = 'Export CSV'; let actions = header.querySelector('.card-actions'); if (!actions) { actions = document.createElement('div'); actions.className = 'card-actions'; actions.style.marginLeft = 'auto'; header.appendChild(actions); } actions.appendChild(btn); } btn.addEventListener('click', () => { const rows = []; const headers = []; dt.columns().every(function () { const th = this.header(); if (th && th.dataset.orderable === 'false' && !th.textContent.trim()) { headers.push(null); // skip checkbox/action columns } else { headers.push(th ? th.textContent.trim() : ''); } }); rows.push(headers.filter((h) => h !== null).map(csvEscape).join(',')); // Iterate filtered + sorted rows in display order. `indexes()` gives DT // indexes; `.row(i).node()` returns the underlying <tr>. const indexes = dt.rows({ search: 'applied', order: 'applied' }).indexes(); for (let n = 0; n < indexes.length; n += 1) { const rowEl = dt.row(indexes[n]).node(); if (!rowEl) {continue;} const cells = []; [...rowEl.cells].forEach((td, idx) => { if (headers[idx] === null) {return;} cells.push(csvEscape(td.textContent.trim().replace(/\s+/g, ' '))); }); rows.push(cells.join(',')); } downloadFile(filename, rows.join('\n'), 'text/csv;charset=utf-8;'); showToast(`Exported ${filename}`, { variant: 'success' }); }); } function csvEscape(v) { const s = String(v ?? ''); if (/[",\n]/.test(s)) {return `"${s.replace(/"/g, '""')}"`;} return s; } function downloadFile(filename, content, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 0); }