2026-01-16 22:53:04 +01:00

836 lines
28 KiB
JavaScript

// public/assets/table.js
// Shared table controller for pagination/infinite mode
class TableController {
constructor(opts) {
this.table = opts.table;
this.tbody = this.table.querySelector('tbody');
this.wrap = this.table.closest('.table-wrap');
this.footer = this.wrap?.querySelector('[data-table-footer]');
this.metaEl = this.wrap?.querySelector('[data-table-meta]');
this.pageEl = this.wrap?.querySelector('[data-table-page]');
this.prevBtn = this.wrap?.querySelector('[data-table-prev]');
this.nextBtn = this.wrap?.querySelector('[data-table-next]');
this.renderRow = opts.renderRow;
this.fetchPage = opts.fetchPage;
this.mode = opts.mode || 'pagination';
this.pageSize = Number(opts.pageSize || 50);
this.page = 1;
this.total = 0;
this.sort = opts.sort || '';
this.dir = opts.dir || 'asc';
this.onSortChange = opts.onSortChange || null;
this.params = opts.params || {};
this.filters = Array.isArray(opts.filters) ? opts.filters : [];
this.loading = false;
this.attachSortHandlers();
this.attachPagerHandlers();
this.initFilters();
this.initViewEditor();
this.defaultColumns = this.buildColumnsFromHeaders([]);
const tableId = this.getTableId();
if (tableId && window.UserPrefs?.getTable) {
const prefs = window.UserPrefs.getTable(tableId);
if (prefs && prefs.columns) {
this.applyColumnPrefs(prefs);
}
}
}
setMode(mode) {
this.mode = mode === 'infinite' ? 'infinite' : 'pagination';
}
setPageSize(size) {
this.pageSize = Math.max(1, Number(size || 50));
}
setParams(params) {
this.params = params || {};
}
setFilters(filters) {
this.filters = Array.isArray(filters) ? filters : [];
this.syncFilterInputs();
}
setSort(sort, dir) {
this.sort = sort || '';
this.dir = dir === 'desc' ? 'desc' : 'asc';
}
attachSortHandlers() {
const headers = this.table.querySelectorAll('thead th[data-sort]');
headers.forEach(th => {
th.classList.add('sortable');
th.addEventListener('click', () => {
const key = th.dataset.sort || '';
if (!key) return;
if (this.sort === key) {
this.dir = (this.dir === 'asc') ? 'desc' : 'asc';
} else {
this.sort = key;
this.dir = 'asc';
}
if (typeof this.onSortChange === 'function') {
this.onSortChange(this.sort, this.dir);
}
this.load(1, false);
});
});
}
attachPagerHandlers() {
if (this.prevBtn) {
this.prevBtn.addEventListener('click', () => {
if (this.page > 1) this.load(this.page - 1, false);
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', () => {
const totalPages = this.totalPages();
if (this.page < totalPages) this.load(this.page + 1, false);
});
}
const handleScroll = () => {
if (this.mode !== 'infinite') return;
if (this.loading) return;
if (this.page >= this.totalPages()) return;
if (this.isWrapScrollable()) {
const nearBottom = this.wrap.scrollTop + this.wrap.clientHeight >= this.wrap.scrollHeight - 80;
if (nearBottom) this.load(this.page + 1, true);
} else {
const doc = document.documentElement;
const nearBottom = window.scrollY + window.innerHeight >= doc.scrollHeight - 120;
if (nearBottom) this.load(this.page + 1, true);
}
};
if (this.wrap) {
this.wrap.addEventListener('scroll', handleScroll);
}
window.addEventListener('scroll', handleScroll);
}
initFilters() {
const headRow = this.table?.querySelector('thead tr');
if (!headRow) return;
const filtersEnabled = this.table?.dataset?.filtersEnabled;
if (filtersEnabled === '0') {
const tools = this.wrap?.querySelector('[data-table-tools]');
if (tools) tools.classList.add('is-hidden');
return;
}
const ths = Array.from(headRow.querySelectorAll('th'));
const hasFilters = ths.some(th => th.dataset.filter);
const tools = this.wrap?.querySelector('[data-table-tools]');
const toggleBtn = this.wrap?.querySelector('[data-table-filters-toggle]');
const summaryEl = this.wrap?.querySelector('[data-table-filters-summary]');
if (!hasFilters) {
if (tools) tools.classList.add('is-hidden');
return;
}
if (tools) tools.classList.remove('is-hidden');
const ensureIndicator = (th) => {
if (!th.classList.contains('has-filter')) {
th.classList.add('has-filter');
}
if (!th.querySelector('.filter-indicator')) {
const dot = document.createElement('span');
dot.className = 'filter-indicator';
dot.setAttribute('aria-hidden', 'true');
th.appendChild(dot);
}
};
ths.forEach((th) => {
if (th.dataset.filter) {
ensureIndicator(th);
}
});
let row = this.table.querySelector('thead tr[data-filter-row]');
if (!row) {
row = document.createElement('tr');
row.className = 'table-filter-row';
row.dataset.filterRow = '1';
headRow.insertAdjacentElement('afterend', row);
ths.forEach((th) => {
const td = document.createElement('td');
const key = th.dataset.key || '';
if (key) td.dataset.key = key;
row.appendChild(td);
});
}
this.filterRow = row;
this.filterThs = ths;
this.filterSummaryEl = summaryEl;
const parseFilter = (value) => {
if (!value) return null;
try {
const obj = JSON.parse(value);
if (obj && typeof obj === 'object') return obj;
} catch (e) {
return null;
}
return null;
};
const defaultOp = (meta, type) => {
if (Array.isArray(meta?.ops) && meta.ops.length > 0) return meta.ops[0];
if (type === 'text') return 'like';
return 'eq';
};
const buildInput = (meta, key) => {
const type = meta?.type || 'text';
const control = meta?.control || '';
const isSelect = type === 'select' || control === 'select' || Array.isArray(meta?.options);
const ops = Array.isArray(meta?.ops) ? meta.ops : [];
const op = defaultOp(meta, type);
if (isSelect) {
const dict = window.I18N || {};
const t = (i18nKey, fallback) => {
if (i18nKey && Object.prototype.hasOwnProperty.call(dict, i18nKey)) {
const v = dict[i18nKey];
if (typeof v === 'string' && v.length) return v;
}
return fallback;
};
const select = document.createElement('select');
select.dataset.filterKey = key;
select.dataset.filterOp = op;
const optAny = document.createElement('option');
optAny.value = '';
optAny.textContent = t('filters.all', 'All');
select.appendChild(optAny);
const options = Array.isArray(meta?.options) ? meta.options : [];
options.forEach((value) => {
const opt = document.createElement('option');
if (value && typeof value === 'object') {
opt.value = String(value.value ?? '');
opt.textContent = t(value.i18n || '', String(value.label ?? value.value ?? ''));
} else {
opt.value = String(value);
opt.textContent = String(value);
}
select.appendChild(opt);
});
return select;
}
if (type === 'boolean') {
const select = document.createElement('select');
select.dataset.filterKey = key;
select.dataset.filterOp = op;
const optAny = document.createElement('option');
optAny.value = '';
optAny.textContent = 'All';
select.appendChild(optAny);
const optTrue = document.createElement('option');
optTrue.value = 'true';
optTrue.textContent = 'Yes';
select.appendChild(optTrue);
const optFalse = document.createElement('option');
optFalse.value = 'false';
optFalse.textContent = 'No';
select.appendChild(optFalse);
return select;
}
if ((type === 'number' || type === 'date') && ops.includes('between')) {
const wrap = document.createElement('div');
wrap.className = 'table-filter-range';
const inputFrom = document.createElement('input');
const inputTo = document.createElement('input');
inputFrom.type = type === 'date' ? 'date' : 'number';
inputTo.type = type === 'date' ? 'date' : 'number';
inputFrom.dataset.filterKey = key;
inputFrom.dataset.filterOp = 'between';
inputFrom.dataset.filterRange = 'from';
inputTo.dataset.filterKey = key;
inputTo.dataset.filterOp = 'between';
inputTo.dataset.filterRange = 'to';
wrap.appendChild(inputFrom);
wrap.appendChild(inputTo);
return wrap;
}
const input = document.createElement('input');
input.type = type === 'number' ? 'number' : (type === 'date' ? 'date' : 'text');
input.dataset.filterKey = key;
input.dataset.filterOp = op;
return input;
};
const cells = Array.from(row.children);
ths.forEach((th, idx) => {
const meta = parseFilter(th.dataset.filter || '');
const key = th.dataset.key || '';
const cell = cells[idx];
if (!cell) return;
cell.innerHTML = '';
if (!meta || key === '') return;
const control = buildInput(meta, key);
cell.appendChild(control);
});
let debounceId = null;
const readFilters = () => {
const filters = [];
const inputs = row.querySelectorAll('[data-filter-key]');
const rangeMap = new Map();
inputs.forEach((el) => {
const key = el.dataset.filterKey || '';
const op = el.dataset.filterOp || 'eq';
if (!key) return;
if (el.dataset.filterRange) {
const entry = rangeMap.get(key) || { from: '', to: '', op };
if (el.dataset.filterRange === 'from') entry.from = el.value || '';
if (el.dataset.filterRange === 'to') entry.to = el.value || '';
rangeMap.set(key, entry);
return;
}
const value = el.value;
if (value === '' || value === null || value === undefined) return;
filters.push({ key, op, value });
});
rangeMap.forEach((entry, key) => {
if (entry.from && entry.to) {
filters.push({ key, op: 'between', value: [entry.from, entry.to] });
}
});
return filters;
};
const updateIndicators = (filters) => {
const active = new Set(filters.map(f => f.key));
ths.forEach((th) => {
if (!th.dataset.filter) return;
const key = th.dataset.key || '';
th.classList.toggle('filter-active', key !== '' && active.has(key));
});
if (summaryEl) {
summaryEl.textContent = filters.length ? `Filters: ${filters.length}` : '';
}
};
const applyFilters = () => {
const filters = readFilters();
this.setFilters(filters);
updateIndicators(filters);
this.load(1, false);
};
const handleInput = (e) => {
const target = e.target;
if (!target || !target.dataset.filterKey) return;
if (target.tagName === 'INPUT' && target.type === 'text') {
if (debounceId) clearTimeout(debounceId);
debounceId = setTimeout(applyFilters, 350);
return;
}
applyFilters();
};
row.addEventListener('input', handleInput);
row.addEventListener('change', handleInput);
if (toggleBtn && !toggleBtn.dataset.filtersInit) {
toggleBtn.dataset.filtersInit = '1';
toggleBtn.addEventListener('click', () => {
if (!this.wrap) return;
this.wrap.classList.toggle('filters-open');
});
}
updateIndicators(this.filters);
}
initViewEditor() {
const dialog = document.getElementById('dlg-table-view');
const tools = this.wrap?.querySelector('[data-table-tools]');
const viewBtn = this.wrap?.querySelector('[data-table-view-toggle]');
if (!dialog || !viewBtn) return;
const hasConfig = this.table?.dataset?.viewConfig === '1';
if (!hasConfig) {
if (viewBtn) viewBtn.classList.add('is-hidden');
if (tools && !this.table?.querySelector('thead th[data-filter]')) {
tools.classList.add('is-hidden');
}
return;
}
viewBtn.classList.remove('is-hidden');
if (viewBtn.dataset.viewInit === '1') return;
viewBtn.dataset.viewInit = '1';
viewBtn.addEventListener('click', () => {
this.openViewEditor(dialog);
});
}
openViewEditor(dialog) {
if (!dialog) return;
const body = dialog.querySelector('[data-table-view-body]');
if (!body) return;
const tableId = this.getTableId();
const prefs = window.UserPrefs?.getTable?.(tableId) || {};
const columns = this.buildColumnsFromHeaders(prefs.columns || []);
body.innerHTML = '';
columns.forEach((col, idx) => {
const tr = document.createElement('tr');
tr.className = 'table-view-row';
tr.dataset.key = col.key;
const tdOn = document.createElement('td');
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = col.visible !== false;
tdOn.appendChild(chk);
const tdLabel = document.createElement('td');
tdLabel.textContent = col.label;
const tdWidth = document.createElement('td');
const sel = document.createElement('select');
const optAuto = document.createElement('option');
optAuto.value = 'auto';
optAuto.textContent = 'Auto';
const optManual = document.createElement('option');
optManual.value = 'manual';
optManual.textContent = 'Manual';
sel.appendChild(optAuto);
sel.appendChild(optManual);
const widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.min = '40';
widthInput.max = '800';
widthInput.value = col.width ? String(col.width) : '';
const widthMode = col.width ? 'manual' : 'auto';
sel.value = widthMode;
widthInput.disabled = widthMode !== 'manual';
sel.addEventListener('change', () => {
widthInput.disabled = sel.value !== 'manual';
});
tdWidth.appendChild(sel);
tdWidth.appendChild(widthInput);
const tdHeadAlign = document.createElement('td');
const headAlign = document.createElement('select');
['left', 'center', 'right'].forEach((val) => {
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
headAlign.appendChild(opt);
});
headAlign.value = col.headAlign || 'left';
tdHeadAlign.appendChild(headAlign);
const tdCellAlign = document.createElement('td');
const cellAlign = document.createElement('select');
['left', 'center', 'right'].forEach((val) => {
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
cellAlign.appendChild(opt);
});
cellAlign.value = col.cellAlign || 'left';
tdCellAlign.appendChild(cellAlign);
const tdOrder = document.createElement('td');
const upBtn = document.createElement('button');
upBtn.type = 'button';
upBtn.className = 'btn';
upBtn.textContent = '↑';
const downBtn = document.createElement('button');
downBtn.type = 'button';
downBtn.className = 'btn';
downBtn.textContent = '↓';
upBtn.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) prev.before(tr);
});
downBtn.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) next.after(tr);
});
tdOrder.appendChild(upBtn);
tdOrder.appendChild(downBtn);
tr.appendChild(tdOn);
tr.appendChild(tdLabel);
tr.appendChild(tdWidth);
tr.appendChild(tdHeadAlign);
tr.appendChild(tdCellAlign);
tr.appendChild(tdOrder);
body.appendChild(tr);
});
const applyBtn = dialog.querySelector('[data-table-view-apply]');
const resetBtn = dialog.querySelector('[data-table-view-reset]');
if (applyBtn && applyBtn.dataset.bound !== '1') {
applyBtn.dataset.bound = '1';
applyBtn.addEventListener('click', () => {
const rows = Array.from(body.querySelectorAll('tr.table-view-row'));
const cols = rows.map((row) => {
const key = row.dataset.key || '';
const inputs = row.querySelectorAll('input, select');
const chk = inputs[0];
const mode = inputs[1];
const widthInput = inputs[2];
const headAlign = inputs[3];
const cellAlign = inputs[4];
const width = mode && mode.value === 'manual' ? Number(widthInput.value || 0) : 0;
return {
key,
visible: chk?.checked !== false,
width: width > 0 ? width : 0,
headAlign: headAlign?.value || 'left',
cellAlign: cellAlign?.value || 'left',
};
}).filter(c => c.key !== '');
window.UserPrefs?.setTable?.(tableId, { columns: cols });
this.applyColumnPrefs({ columns: cols });
dialog.close();
});
}
if (resetBtn && resetBtn.dataset.bound !== '1') {
resetBtn.dataset.bound = '1';
resetBtn.addEventListener('click', () => {
window.UserPrefs?.setTable?.(tableId, { columns: [] });
this.applyColumnPrefs({ columns: [] });
dialog.close();
});
}
dialog.showModal();
}
buildColumnsFromHeaders(saved) {
const headRow = this.table?.querySelector('thead tr');
if (!headRow) return [];
const ths = Array.from(headRow.querySelectorAll('th'));
const base = ths.map((th) => ({
key: th.dataset.key || '',
label: th.textContent?.trim() || '',
visible: true,
width: 0,
headAlign: th.classList.contains('align-right') ? 'right' : (th.classList.contains('align-center') ? 'center' : 'left'),
cellAlign: th.classList.contains('align-right') ? 'right' : (th.classList.contains('align-center') ? 'center' : 'left'),
})).filter(c => c.key !== '');
if (!Array.isArray(saved) || saved.length === 0) return base;
const map = new Map(saved.map(c => [c.key, c]));
const ordered = [];
saved.forEach((c) => {
const baseCol = base.find(b => b.key === c.key);
if (!baseCol) return;
ordered.push({
...baseCol,
visible: c.visible !== false,
width: Number(c.width || 0),
headAlign: c.headAlign || baseCol.headAlign || 'left',
cellAlign: c.cellAlign || baseCol.cellAlign || 'left',
});
});
base.forEach((c) => {
if (!map.has(c.key)) ordered.push(c);
});
return ordered;
}
getTableId() {
return this.table?.dataset?.tableId || this.table?.id || '';
}
applyColumnPrefs(prefs) {
let columns = Array.isArray(prefs?.columns) ? prefs.columns : [];
if (columns.length === 0 && Array.isArray(this.defaultColumns)) {
columns = this.defaultColumns.map(c => ({ ...c }));
}
if (columns.length === 0) return;
this.columnPrefs = columns;
const headRow = this.table?.querySelector('thead tr');
if (!headRow) return;
const ths = Array.from(headRow.querySelectorAll('th'));
const keyToIndex = new Map();
ths.forEach((th, idx) => {
const key = th.dataset.key || '';
if (key) keyToIndex.set(key, idx);
});
const order = columns.map(c => c.key).filter(k => keyToIndex.has(k));
const used = new Set(order);
ths.forEach((th) => {
const key = th.dataset.key || '';
if (key && !used.has(key)) order.push(key);
});
this.columnOrder = order;
this.reorderColumns(order, ths.length, keyToIndex);
this.applyColumnStyles();
}
reorderColumns(order, count, keyToIndex) {
if (!this.table) return;
const rows = Array.from(this.table.querySelectorAll('tr'));
rows.forEach((row) => {
const cells = Array.from(row.children);
if (cells.length !== count) return;
const cellByKey = new Map();
let hasKeys = false;
cells.forEach((cell) => {
const key = cell.dataset?.key || '';
if (key) {
cellByKey.set(key, cell);
hasKeys = true;
}
});
if (hasKeys) {
const newCells = [];
const used = new Set();
order.forEach((key) => {
const cell = cellByKey.get(key);
if (cell) {
newCells.push(cell);
used.add(cell);
}
});
cells.forEach((cell) => {
if (!used.has(cell)) newCells.push(cell);
});
if (newCells.length !== cells.length) return;
newCells.forEach((cell) => row.appendChild(cell));
return;
}
const newCells = [];
order.forEach((key) => {
const idx = keyToIndex.get(key);
if (idx >= 0 && cells[idx]) newCells.push(cells[idx]);
});
if (newCells.length !== count) return;
newCells.forEach((cell) => row.appendChild(cell));
});
}
applyColumnVisibility(columns, order) {
if (!this.table) return;
const visibility = new Map(columns.map(c => [c.key, c.visible !== false]));
const rows = Array.from(this.table.querySelectorAll('tr'));
rows.forEach((row) => {
const cells = Array.from(row.children);
if (cells.length !== order.length) return;
order.forEach((key, idx) => {
const visible = visibility.get(key);
if (visible === undefined) return;
cells[idx].classList.toggle('is-hidden', !visible);
});
});
}
applyFilterVisibility(columns, order) {
const row = this.filterRow;
if (!row) return;
const visibility = new Map(columns.map(c => [c.key, c.visible !== false]));
const visibleKeys = new Set(columns.filter(c => c.visible !== false).map(c => c.key));
const cells = Array.from(row.children);
cells.forEach((cell, idx) => {
const key = cell.dataset?.key || order[idx] || '';
if (!key) return;
const visible = visibility.get(key);
if (visible === undefined) return;
cell.classList.toggle('is-hidden', !visible);
const inputs = cell.querySelectorAll('input, select, textarea, button');
inputs.forEach((el) => {
el.disabled = !visible;
if (!visible && 'value' in el) el.value = '';
});
});
const before = Array.isArray(this.filters) ? this.filters : [];
const next = before.filter(f => visibleKeys.has(f.key));
if (next.length !== before.length) {
this.filters = next;
this.syncFilterInputs();
}
}
applyColumnWidths(columns, order) {
if (!this.table) return;
const widths = new Map(columns.map(c => [c.key, Number(c.width || 0)]));
const rows = Array.from(this.table.querySelectorAll('tr'));
rows.forEach((row) => {
const cells = Array.from(row.children);
if (cells.length !== order.length) return;
order.forEach((key, idx) => {
const width = widths.get(key) || 0;
if (width > 0) {
cells[idx].style.width = `${width}px`;
} else {
cells[idx].style.width = '';
}
});
});
this.table.style.tableLayout = 'fixed';
}
applyColumnStyles() {
const columns = Array.isArray(this.columnPrefs) ? this.columnPrefs : [];
const order = Array.isArray(this.columnOrder) ? this.columnOrder : [];
if (columns.length === 0 || order.length === 0) return;
const headRow = this.table?.querySelector('thead tr');
if (headRow) {
const ths = Array.from(headRow.querySelectorAll('th'));
const keyToIndex = new Map();
ths.forEach((th, idx) => {
const key = th.dataset.key || '';
if (key) keyToIndex.set(key, idx);
});
this.reorderColumns(order, ths.length, keyToIndex);
}
this.applyColumnVisibility(columns, order);
this.applyFilterVisibility(columns, order);
this.applyColumnWidths(columns, order);
this.applyColumnAlign(columns, order);
}
applyColumnAlign(columns, order) {
if (!this.table) return;
const headAlign = new Map(columns.map(c => [c.key, c.headAlign || 'left']));
const cellAlign = new Map(columns.map(c => [c.key, c.cellAlign || 'left']));
const rows = Array.from(this.table.querySelectorAll('tr'));
rows.forEach((row) => {
const isHead = row.parentElement?.tagName === 'THEAD';
const cells = Array.from(row.children);
if (cells.length !== order.length) return;
order.forEach((key, idx) => {
const align = (isHead ? headAlign.get(key) : cellAlign.get(key)) || 'left';
const cell = cells[idx];
if (!cell) return;
cell.classList.remove('align-left', 'align-center', 'align-right');
cell.classList.add(`align-${align}`);
});
});
}
syncFilterInputs() {
const row = this.filterRow;
if (!row) return;
const inputs = row.querySelectorAll('[data-filter-key]');
const rangeMap = new Map();
inputs.forEach((el) => {
if (el.dataset.filterRange) {
const key = el.dataset.filterKey || '';
if (!key) return;
const entry = rangeMap.get(key) || { fromEl: null, toEl: null };
if (el.dataset.filterRange === 'from') entry.fromEl = el;
if (el.dataset.filterRange === 'to') entry.toEl = el;
rangeMap.set(key, entry);
return;
}
el.value = '';
});
rangeMap.forEach((entry) => {
if (entry.fromEl) entry.fromEl.value = '';
if (entry.toEl) entry.toEl.value = '';
});
const filters = Array.isArray(this.filters) ? this.filters : [];
filters.forEach((f) => {
const key = f?.key || '';
if (!key) return;
if (f.op === 'between' && Array.isArray(f.value)) {
const entry = rangeMap.get(key);
if (entry?.fromEl) entry.fromEl.value = String(f.value[0] ?? '');
if (entry?.toEl) entry.toEl.value = String(f.value[1] ?? '');
return;
}
inputs.forEach((el) => {
if (el.dataset.filterKey !== key) return;
if (el.dataset.filterRange) return;
el.value = f.value ?? '';
});
});
if (this.filterThs) {
const active = new Set(filters.map(f => f.key));
this.filterThs.forEach((th) => {
if (!th.dataset.filter) return;
const key = th.dataset.key || '';
th.classList.toggle('filter-active', key !== '' && active.has(key));
});
}
if (this.filterSummaryEl) {
this.filterSummaryEl.textContent = filters.length ? `Filters: ${filters.length}` : '';
}
}
isWrapScrollable() {
if (!this.wrap) return false;
return this.wrap.scrollHeight > this.wrap.clientHeight + 4;
}
totalPages() {
return Math.max(1, Math.ceil((this.total || 0) / this.pageSize));
}
async load(page = 1, append = false) {
if (!this.fetchPage || !this.renderRow || !this.tbody) return;
if (this.loading) return;
this.loading = true;
const payload = {
page,
per_page: this.pageSize,
sort: this.sort,
dir: this.dir,
filters: this.filters,
params: this.params,
};
try {
const res = await this.fetchPage(payload);
const items = res.items || [];
this.total = Number(res.total || 0);
this.page = Number(res.page || page);
if (!append) {
this.tbody.innerHTML = '';
}
items.forEach(item => this.renderRow(this.tbody, item));
this.applyColumnStyles();
this.updateFooter();
} finally {
this.loading = false;
}
}
reload() {
return this.load(this.page, false);
}
updateFooter() {
if (!this.footer) return;
if (this.mode === 'infinite') {
if (this.prevBtn) this.prevBtn.disabled = true;
if (this.nextBtn) this.nextBtn.disabled = true;
} else {
const totalPages = this.totalPages();
if (this.prevBtn) this.prevBtn.disabled = this.page <= 1;
if (this.nextBtn) this.nextBtn.disabled = this.page >= totalPages;
}
if (this.pageEl) {
const totalPages = this.totalPages();
this.pageEl.textContent = `${this.page} / ${totalPages}`;
}
if (this.metaEl) {
const start = (this.page - 1) * this.pageSize + 1;
const end = Math.min(this.total, this.page * this.pageSize);
if (this.total === 0) {
this.metaEl.textContent = '';
} else {
this.metaEl.textContent = `${start}-${end} / ${this.total}`;
}
}
}
}
window.TableController = TableController;