ZDDC/tables/js/render.js
2026-06-11 13:32:31 -05:00

107 lines
4.6 KiB
JavaScript

(function (app) {
'use strict';
function renderHeader(theadEl, columns, sortState, filterMap, onHeaderClick, onFilterChange) {
const util = app.modules.util;
const filters = app.modules.filters;
const sort = app.modules.sort;
theadEl.innerHTML = '';
// Required fields come from the row schema (form.yaml schema.required).
const ctx = app.context || {};
const reqList = (ctx.rowSchema && Array.isArray(ctx.rowSchema.required)) ? ctx.rowSchema.required : [];
const requiredSet = {};
for (let r = 0; r < reqList.length; r++) requiredSet[reqList[r]] = true;
const titleRow = util.h('tr', { className: 'zddc-table__title-row' });
const filterRow = util.h('tr', { className: 'zddc-table__filter-row' });
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
const indicator = sort.indicator(sortState, col.field);
const isReq = !!requiredSet[col.field];
const th = util.h('th', {
className: 'zddc-table__th' + (isReq ? ' zddc-table__th--required' : ''),
'data-field': col.field,
title: isReq ? 'Required' : null,
style: col.width ? 'width:' + col.width : null,
onClick: function (ev) { onHeaderClick(col.field, ev.shiftKey); }
}, col.title || col.field, indicator);
// Mandatory marker — a red asterisk after the column title.
if (isReq) th.insertBefore(util.h('span', { className: 'zddc-table__req', 'aria-hidden': 'true' }, ' *'), th.childNodes[1] || null);
titleRow.appendChild(th);
const td = util.h('td', { className: 'zddc-table__filter-cell' });
// Every column gets the same text-contains filter input, even
// enum columns — keeps the filter row visually uniform and
// doesn't constrain users to picking from the enum (a
// case-insensitive substring match works for both free-text
// and enum data).
const f = filterMap[col.field] || filters.defaultFilterFor(col);
const input = util.h('input', {
type: 'text',
className: 'zddc-table__filter-text',
placeholder: 'filter…',
'aria-label': 'Filter ' + (col.title || col.field),
value: typeof f.value === 'string' ? f.value : '',
onInput: function (ev) {
onFilterChange(col.field, { kind: 'contains', value: ev.target.value });
}
});
td.appendChild(input);
filterRow.appendChild(td);
}
theadEl.appendChild(titleRow);
theadEl.appendChild(filterRow);
}
function renderBody(tbodyEl, rows, columns) {
const util = app.modules.util;
const editor = app.modules.editor;
tbodyEl.innerHTML = '';
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const tr = util.h('tr', {
className: 'zddc-table__row' + (row.editable ? ' zddc-table__row--editable' : ' zddc-table__row--readonly'),
'data-url': row.url,
'data-editable': row.editable ? '1' : '0'
});
const rowId = editor ? editor.rowKey(row) : (row.url || '');
if (editor) {
editor.attachToRow(tr, rowId);
}
for (let c = 0; c < columns.length; c++) {
const col = columns[c];
// Editor's draft buffer overrides the row's stored value
// until Phase 3 commits it. Falls back to row.data when
// no draft is present.
const value = editor
? editor.effectiveCellValue(row, col)
: util.resolveField(row.data, col.field);
const text = util.formatCell(value, col.format);
const td = util.h('td', { className: 'zddc-table__cell' }, text);
if (editor) {
editor.attachToCell(td, i, c);
}
tr.appendChild(td);
}
tbodyEl.appendChild(tr);
}
}
function renderRowCount(el, displayed, total) {
if (!el) return;
if (displayed === total) {
el.textContent = total + (total === 1 ? ' row' : ' rows');
} else {
el.textContent = displayed + ' of ' + total + ' rows';
}
}
app.modules.render = {
header: renderHeader,
body: renderBody,
rowCount: renderRowCount
};
})(window.tablesApp);