@zeyos/zx 4.3.4 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +438 -0
- package/README.md +49 -10
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +217 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +23 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +11 -3
- package/dist/types/components/card-view/record-card.d.ts +7 -1
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +149 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +75 -6
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/field/field.d.ts +24 -2
- package/dist/types/components/fieldset/fieldset.d.ts +19 -1
- package/dist/types/components/file-item/file-item.d.ts +262 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
- package/dist/types/components/launcher/launcher.d.ts +278 -26
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +37 -42
- package/dist/types/components/message/message.d.ts +126 -17
- package/dist/types/components/modal/modal.d.ts +61 -2
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +15 -3
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
- package/dist/types/components/tabbox/tabbox.d.ts +16 -0
- package/dist/types/components/table/table.d.ts +190 -1
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/icons.d.ts +37 -22
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +15 -1
- package/dist/types/index.d.ts +16 -3
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +1 -1
- package/dist/types/zeyos/query.d.ts +79 -0
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +100 -14
- package/dist/types/zeyos/view.d.ts +10 -0
- package/dist/zx-zeyos.esm.js +2206 -489
- package/dist/zx-zeyos.esm.min.js +56 -1
- package/dist/zx.css +3322 -724
- package/dist/zx.esm.js +12244 -5809
- package/dist/zx.esm.min.js +58 -3
- package/dist/zx.global.js +12246 -5811
- package/dist/zx.global.min.js +58 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1683 -146
- package/docs/llms.md +777 -110
- package/docs/llms.txt +14 -8
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +602 -0
- package/src/components/activity-list/activity-list.css +99 -0
- package/src/components/activity-list/activity-list.js +409 -0
- package/src/components/app-icon/app-icon.css +1 -1
- package/src/components/app-rail/app-rail.css +191 -22
- package/src/components/app-sidebar/app-sidebar.css +349 -24
- package/src/components/app-sidebar/app-sidebar.js +86 -11
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/button/button.css +74 -8
- package/src/components/button/button.js +44 -1
- package/src/components/calendar/calendar.css +46 -46
- package/src/components/calendar/calendar.js +33 -3
- package/src/components/card/card.css +2 -2
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +10 -7
- package/src/components/card-view/card-view.js +25 -9
- package/src/components/card-view/record-card.js +7 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +68 -0
- package/src/components/code-editor/code-editor.js +306 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +87 -5
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +8 -3
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.css +1 -1
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +174 -70
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +7 -5
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +68 -26
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +28 -5
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +505 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +205 -47
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/finder/finder.css +15 -13
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +247 -0
- package/src/components/kanban-view/kanban-view.css +226 -8
- package/src/components/kanban-view/kanban-view.js +255 -12
- package/src/components/launcher/launcher.css +107 -25
- package/src/components/launcher/launcher.js +557 -63
- package/src/components/list-toolbar/list-toolbar.css +194 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/menu-button/menu-button.css +87 -5
- package/src/components/menu-button/menu-button.js +86 -46
- package/src/components/message/message.css +38 -5
- package/src/components/message/message.js +230 -17
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +159 -3
- package/src/components/navigation-bar/navigation-bar.css +95 -11
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/number-field/field-adapter.js +1 -0
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +8 -6
- package/src/components/panel/panel.css +1 -1
- package/src/components/panel/panel.js +13 -2
- package/src/components/rating/rating.css +7 -0
- package/src/components/search/search.js +17 -5
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +6 -1
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +86 -47
- package/src/components/sheet/sheet.css +11 -8
- package/src/components/slider/slider.css +16 -2
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +20 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +208 -0
- package/src/components/stat-tile/stat-tile.js +601 -0
- package/src/components/tabbox/tabbox.css +32 -27
- package/src/components/tabbox/tabbox.js +17 -2
- package/src/components/table/table.css +155 -31
- package/src/components/table/table.js +386 -33
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +86 -6
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +10 -3
- package/src/components/tree/tree.css +25 -16
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/breakpoint.js +1 -1
- package/src/core/icons.js +259 -57
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +21 -2
- package/src/index.js +30 -3
- package/src/internal/app-rail.js +18 -1
- package/src/internal/menu-items.js +142 -0
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +67 -27
- package/src/zeyos/index.js +1 -1
- package/src/zeyos/query.js +153 -0
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +68 -5
- package/src/zeyos/view.js +6 -0
- package/styles/base.css +56 -0
- package/styles/icons.css +11 -1
- package/styles/print.css +192 -0
- package/styles/tokens/dark.css +113 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +100 -31
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +11 -1
- package/styles/tokens/density.css +0 -9
|
@@ -15,6 +15,7 @@ import { sortRows } from './sort.js';
|
|
|
15
15
|
/** @typedef {'asc'|'desc'} TableSortDirection */
|
|
16
16
|
/** @typedef {false|'single'|'multi'} TableSelectionMode */
|
|
17
17
|
/** @typedef {false|'cell'|'row'} TableEditMode */
|
|
18
|
+
/** @typedef {'default'|'plain'} TableVariant */
|
|
18
19
|
/** @typedef {'text'|'number'|'select'|'date'|'checkbox'|'textarea'|'custom'} TableEditorType */
|
|
19
20
|
/** @typedef {'text'|'number'|'currency'|'percent'|'unit'} TableColumnType */
|
|
20
21
|
/** @typedef {{value: unknown, label: string}} TableEditorOption */
|
|
@@ -28,6 +29,24 @@ import { sortRows } from './sort.js';
|
|
|
28
29
|
* @property {boolean|unknown[]} [expanded=false] Initially expanded branches: all, none, or ids.
|
|
29
30
|
*/
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* How one resolved row kind is presented. `rowKinds` is keyed by the kind coerced to a string, so
|
|
34
|
+
* `{ 1: … }` and `{ '1': … }` name the same entry.
|
|
35
|
+
* @typedef {Object} TableRowKind
|
|
36
|
+
* @property {boolean} [span=false] Draw the row as a single cell across every rendered column — a
|
|
37
|
+
* section heading rather than a record. The cell is a `<th scope="colgroup">`, not a `<td>`,
|
|
38
|
+
* because it labels the columns of the rows beneath it and a screen reader walking the table
|
|
39
|
+
* should meet it as a heading rather than as a value. A spanning row carries no selection
|
|
40
|
+
* checkbox, is never editable, and is skipped by `rowReorder`; it still emits `rowclick` and still
|
|
41
|
+
* participates in `getData()` and `updateRow()`.
|
|
42
|
+
* @property {(row: TableRow, index: number) => Node|string|number|null|undefined} [render] Content
|
|
43
|
+
* of the spanning cell. Omitted, the cell falls back to the row's raw value for the first visible
|
|
44
|
+
* column, which is what a host otherwise writes by hand.
|
|
45
|
+
* @property {string} [class] Space-separated classes added to the row, on top of `rowClass`. Zx
|
|
46
|
+
* ships no section look: a billing section is not a kanban swimlane, so the tint, the weight and
|
|
47
|
+
* the rule above it are the host's to decide.
|
|
48
|
+
*/
|
|
49
|
+
|
|
31
50
|
/**
|
|
32
51
|
* Editing handle handed to a custom `column.editor`.
|
|
33
52
|
* @typedef {Object} TableEditorApi
|
|
@@ -41,7 +60,11 @@ import { sortRows } from './sort.js';
|
|
|
41
60
|
/**
|
|
42
61
|
* @typedef {Object} TableColumn
|
|
43
62
|
* @property {string} id Row property and stable column identifier.
|
|
44
|
-
* @property {string} label Visible header label.
|
|
63
|
+
* @property {string} label Visible header label. An empty label leaves the header blank — an
|
|
64
|
+
* icon or actions column — and the header is then named by `ariaLabel`, or by the id.
|
|
65
|
+
* @property {string} [ariaLabel] Accessible name of the column header, and the name the column
|
|
66
|
+
* chooser and the cell editors use. Needed when `label` is empty or terse (`'#'`); include the
|
|
67
|
+
* visible label's words in it when there is one.
|
|
45
68
|
* @property {boolean} [sortable=false] Whether the header changes the active sort.
|
|
46
69
|
* @property {string} [width='auto'] CSS width, `auto`, or an `fr` proportion. `fr` values are
|
|
47
70
|
* converted into proportional percentages on `<col>` elements and enable `table-layout: fixed`;
|
|
@@ -100,7 +123,19 @@ import { sortRows } from './sort.js';
|
|
|
100
123
|
* placed as given, and a function is called on each render, which is what to use when the
|
|
101
124
|
* placeholder holds live controls.
|
|
102
125
|
* @property {((row: TableRow) => string)|null} [rowClass=null] Additional row class callback.
|
|
126
|
+
* @property {string|((row: TableRow) => unknown)|null} [rowKind=null] Row property or callback
|
|
127
|
+
* naming each row's kind — the `type` on a ZeyOS transaction line, where one value is a position
|
|
128
|
+
* carrying quantities and amounts and another is a section heading that groups the positions under
|
|
129
|
+
* it. `null`, the default, renders every row exactly as before.
|
|
130
|
+
* @property {Record<string, TableRowKind>|null} [rowKinds=null] Presentation per kind, keyed by the
|
|
131
|
+
* resolved kind coerced to a string. A kind with no entry here renders as an ordinary row. While
|
|
132
|
+
* any row in the current data resolves to a `span` kind the header sort controls are withheld and
|
|
133
|
+
* `setSort()` is inert: sorting a grouped table scatters the positions out of their sections, and
|
|
134
|
+
* doing that silently is worse than not offering it. `sortMode: 'server'` still owns its own order.
|
|
103
135
|
* @property {boolean} [zebra=true] Whether alternate rows use the zebra background.
|
|
136
|
+
* @property {TableVariant} [variant='default'] Frame treatment. `'default'` draws the table's own
|
|
137
|
+
* bordered box; `'plain'` is for a table inside a card or panel that already draws one — no outer
|
|
138
|
+
* frame, no header band, no zebra, hairline row dividers. Reflected as `data-variant` on the root.
|
|
104
139
|
* @property {TableEditMode} [editMode=false] Inline editing mode. `'cell'` edits one cell, `'row'`
|
|
105
140
|
* opens every editable cell of the row together and commits them as a unit. Editing is completely
|
|
106
141
|
* inert while this is `false`.
|
|
@@ -258,7 +293,10 @@ export class Table extends Component {
|
|
|
258
293
|
height: null,
|
|
259
294
|
emptyText: null,
|
|
260
295
|
rowClass: null,
|
|
296
|
+
rowKind: null,
|
|
297
|
+
rowKinds: null,
|
|
261
298
|
zebra: true,
|
|
299
|
+
variant: 'default',
|
|
262
300
|
editMode: false,
|
|
263
301
|
editTrigger: 'double',
|
|
264
302
|
hierarchy: false,
|
|
@@ -288,6 +326,12 @@ export class Table extends Component {
|
|
|
288
326
|
this._original = created ? null : snapshotTarget(root);
|
|
289
327
|
this._columns = normalizeTableColumns(this.options.columns);
|
|
290
328
|
this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
|
|
329
|
+
// Row kinds resolve before the first sort: `_sortData()` refuses to run while the data holds a
|
|
330
|
+
// spanning row, and the initial `sort` option reaches it before anything is rendered.
|
|
331
|
+
this._rowKind = normalizeRowKind(this.options.rowKind);
|
|
332
|
+
this._rowKinds = normalizeRowKinds(this.options.rowKinds);
|
|
333
|
+
this._spanning = this._hasSpanningRow();
|
|
334
|
+
this._sortWarned = false;
|
|
291
335
|
this._selected = new Set();
|
|
292
336
|
this._selectionAnchorId = null;
|
|
293
337
|
this._sort = normalizeSort(this.options.sort, this._columns);
|
|
@@ -326,6 +370,8 @@ export class Table extends Component {
|
|
|
326
370
|
}
|
|
327
371
|
this._reconcileExpanded();
|
|
328
372
|
|
|
373
|
+
if (normalizeTableVariant(this.options.variant) === 'plain') root.dataset.variant = 'plain';
|
|
374
|
+
|
|
329
375
|
this._table = h('table', { class: 'zx-table__table' });
|
|
330
376
|
if (this._hierarchy) this._table.setAttribute('role', 'treegrid');
|
|
331
377
|
this._colgroup = h('colgroup', { class: 'zx-table__columns' });
|
|
@@ -603,7 +649,7 @@ export class Table extends Component {
|
|
|
603
649
|
const column = this._columns.find((candidate) => candidate.id === String(id));
|
|
604
650
|
const index = this.getColumnOrder().indexOf(String(id));
|
|
605
651
|
if (column && index >= 0) {
|
|
606
|
-
this._columnStatus.textContent = `Moved ${column
|
|
652
|
+
this._columnStatus.textContent = `Moved ${tableColumnName(column)} to position ${index + 1} of ${this._columns.length}.`;
|
|
607
653
|
}
|
|
608
654
|
}
|
|
609
655
|
return this;
|
|
@@ -666,6 +712,8 @@ export class Table extends Component {
|
|
|
666
712
|
const row = this.getRow(id);
|
|
667
713
|
const target = this.getRow(targetId);
|
|
668
714
|
if (!row || !target || Object.is(id, targetId)) return this;
|
|
715
|
+
// A section heading has no handle to grab; refuse the programmatic route for the same reason.
|
|
716
|
+
if (this._isSpanningRow(row)) return this;
|
|
669
717
|
const next = reorderTableRows(this._data, this.options.rowId, id, targetId, position, this._hierarchy?.parentId);
|
|
670
718
|
if (!next) return this;
|
|
671
719
|
const event = this.emit('rowmove', { row, id, target, targetId, position });
|
|
@@ -704,6 +752,10 @@ export class Table extends Component {
|
|
|
704
752
|
/**
|
|
705
753
|
* Activates a column sort. Local mode stably reorders data; server mode only updates the
|
|
706
754
|
* header state and emits the request event.
|
|
755
|
+
*
|
|
756
|
+
* Inert while the data holds a spanning row: a sort would scatter the positions out of the
|
|
757
|
+
* sections that group them, in either sort mode. The header withholds its controls for the same
|
|
758
|
+
* reason, so nothing on screen invites the call.
|
|
707
759
|
* @param {string} id Column id.
|
|
708
760
|
* @param {TableSortDirection} [dir] Direction; omitted toggles the current column.
|
|
709
761
|
* @param {{silent?: boolean}} [options={}] Event options.
|
|
@@ -712,6 +764,14 @@ export class Table extends Component {
|
|
|
712
764
|
* @fires Table#editcancel
|
|
713
765
|
*/
|
|
714
766
|
setSort(id, dir, options = {}) {
|
|
767
|
+
if (this._spanning) {
|
|
768
|
+
if (!this._sortWarned) {
|
|
769
|
+
this._sortWarned = true;
|
|
770
|
+
console.warn('[zx.Table] setSort() is inert while the data contains spanning rowKinds rows: '
|
|
771
|
+
+ 'sorting would scatter the rows out of the sections that group them.');
|
|
772
|
+
}
|
|
773
|
+
return this;
|
|
774
|
+
}
|
|
715
775
|
// Server mode never re-renders the body, so the open editor is dropped here rather than in
|
|
716
776
|
// `_renderBody()`.
|
|
717
777
|
this._abortEdit({ focus: false });
|
|
@@ -769,7 +829,7 @@ export class Table extends Component {
|
|
|
769
829
|
*/
|
|
770
830
|
setSelection(ids) {
|
|
771
831
|
if (!Array.isArray(ids)) throw new TypeError('Table selection must be an array of row ids');
|
|
772
|
-
const valid =
|
|
832
|
+
const valid = this._selectableIds();
|
|
773
833
|
const next = new Set();
|
|
774
834
|
if (this.options.selectable !== false) {
|
|
775
835
|
for (const id of ids) {
|
|
@@ -889,6 +949,8 @@ export class Table extends Component {
|
|
|
889
949
|
if (!requested) return this;
|
|
890
950
|
const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
|
|
891
951
|
if (current === undefined) return this;
|
|
952
|
+
// A spanning row renders no column cells at all, so there is nothing to open an editor over.
|
|
953
|
+
if (this._isSpanningRow(current)) return this;
|
|
892
954
|
const column = this._editMode === 'row' && this._hiddenColumns.has(requested.id)
|
|
893
955
|
? this._visibleColumns().find((candidate) => resolveEditable(candidate, current) !== false) ?? null
|
|
894
956
|
: requested;
|
|
@@ -1205,6 +1267,45 @@ export class Table extends Component {
|
|
|
1205
1267
|
return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
|
|
1206
1268
|
}
|
|
1207
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* Whether a row draws as one cell across the table instead of as a record.
|
|
1272
|
+
* @param {TableRow} row Row to classify.
|
|
1273
|
+
* @returns {boolean}
|
|
1274
|
+
*/
|
|
1275
|
+
_isSpanningRow(row) {
|
|
1276
|
+
if (this._rowKind === null || this._rowKinds === null) return false;
|
|
1277
|
+
return isSpanningRow(row, this._rowKind, this._rowKinds);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Whether any row in the current data — not only the rendered prefix — spans. This is what
|
|
1282
|
+
* withholds the sort controls, and the reader must not be offered a control that the next
|
|
1283
|
+
* `growBy()` would have to take away again.
|
|
1284
|
+
* @returns {boolean}
|
|
1285
|
+
*/
|
|
1286
|
+
_hasSpanningRow() {
|
|
1287
|
+
return hasSpanningRow(this._data, this._rowKind, this._rowKinds);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* How many cells one row occupies: every visible column plus the structural columns the table
|
|
1292
|
+
* renders beside them. Shared by the spanning cell and the empty-state cell so the two can never
|
|
1293
|
+
* disagree about the table's width.
|
|
1294
|
+
* @returns {number}
|
|
1295
|
+
*/
|
|
1296
|
+
_spanColspan() {
|
|
1297
|
+
return tableSpanColspan(this._visibleColumns().length, {
|
|
1298
|
+
selectable: this.options.selectable,
|
|
1299
|
+
rowReorder: this.options.rowReorder
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** @returns {TableRow[]} Rendered rows that can carry a selection; spanning rows cannot. */
|
|
1304
|
+
_selectableData() {
|
|
1305
|
+
const visible = this._visibleData();
|
|
1306
|
+
return this._rowKind === null ? visible : visible.filter((row) => !this._isSpanningRow(row));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1208
1309
|
/** @returns {HTMLElement} */
|
|
1209
1310
|
_createColumnControls() {
|
|
1210
1311
|
const list = h('div', { class: 'zx-table__column-list' });
|
|
@@ -1213,7 +1314,8 @@ export class Table extends Component {
|
|
|
1213
1314
|
class: 'zx-table__column-status', role: 'status', ariaLive: 'polite'
|
|
1214
1315
|
});
|
|
1215
1316
|
const details = h('details', { class: 'zx-table__column-controls' },
|
|
1216
|
-
h('summary', { class: 'zx-
|
|
1317
|
+
h('summary', { class: 'zx-btn', dataset: { size: 'sm' } }, icon('fields'),
|
|
1318
|
+
this._message('table.columns', 'Columns', [])),
|
|
1217
1319
|
list);
|
|
1218
1320
|
this._renderColumnControls();
|
|
1219
1321
|
return details;
|
|
@@ -1239,20 +1341,23 @@ export class Table extends Component {
|
|
|
1239
1341
|
disabled: checked && visible.length === 1,
|
|
1240
1342
|
dataset: { columnId: column.id, columnAction: 'toggle' }
|
|
1241
1343
|
}) : null;
|
|
1344
|
+
// The chooser lists every column by name, a blank-headed one included.
|
|
1345
|
+
const name = tableColumnName(column);
|
|
1242
1346
|
const label = toggle
|
|
1243
|
-
? h('label', { for: controlId }, toggle, h('span', {},
|
|
1244
|
-
: h('span', { class: 'zx-table__column-label' },
|
|
1347
|
+
? h('label', { for: controlId }, toggle, h('span', {}, name))
|
|
1348
|
+
: h('span', { class: 'zx-table__column-label' }, name);
|
|
1245
1349
|
const order = this.options.columnReorder ? h('span', {
|
|
1246
|
-
class: 'zx-table__column-order', role: 'group',
|
|
1350
|
+
class: 'zx-table__column-order', role: 'group',
|
|
1351
|
+
ariaLabel: this._message('table.moveColumn', 'Move %1', [name])
|
|
1247
1352
|
},
|
|
1248
1353
|
h('button', {
|
|
1249
1354
|
class: 'zx-table__column-move', type: 'button', disabled: index === 0,
|
|
1250
|
-
ariaLabel: `Move ${
|
|
1355
|
+
ariaLabel: `Move ${name} earlier`,
|
|
1251
1356
|
dataset: { columnId: column.id, columnAction: 'up' }
|
|
1252
1357
|
}, icon('chevron-up', { size: 11 })),
|
|
1253
1358
|
h('button', {
|
|
1254
1359
|
class: 'zx-table__column-move', type: 'button', disabled: index === this._columns.length - 1,
|
|
1255
|
-
ariaLabel: `Move ${
|
|
1360
|
+
ariaLabel: `Move ${name} later`,
|
|
1256
1361
|
dataset: { columnId: column.id, columnAction: 'down' }
|
|
1257
1362
|
}, icon('chevron-down', { size: 11 }))) : null;
|
|
1258
1363
|
return h('div', { class: 'zx-table__column-option', dataset: { columnId: column.id } }, label, order);
|
|
@@ -1494,9 +1599,9 @@ export class Table extends Component {
|
|
|
1494
1599
|
* click is usually a bulk action. `showAll()` first if you mean all of them.
|
|
1495
1600
|
*/
|
|
1496
1601
|
if (this._selectAll.checked) {
|
|
1497
|
-
for (const dataRow of this.
|
|
1602
|
+
for (const dataRow of this._selectableData()) this._selected.add(this._idFor(dataRow));
|
|
1498
1603
|
} else {
|
|
1499
|
-
for (const dataRow of this.
|
|
1604
|
+
for (const dataRow of this._selectableData()) this._selected.delete(this._idFor(dataRow));
|
|
1500
1605
|
}
|
|
1501
1606
|
this._selectionAnchorId = null;
|
|
1502
1607
|
this._syncSelection();
|
|
@@ -1507,15 +1612,26 @@ export class Table extends Component {
|
|
|
1507
1612
|
}
|
|
1508
1613
|
|
|
1509
1614
|
this._headers = new Map();
|
|
1615
|
+
// What this header was built from, so `_renderBody()` can tell whether it is still the right one.
|
|
1616
|
+
this._headerSpanning = this._spanning;
|
|
1510
1617
|
for (const column of this._visibleColumns()) {
|
|
1511
|
-
|
|
1618
|
+
// `data-column` on the header and on every body cell, in every mode, so a read-only table
|
|
1619
|
+
// can be styled per column as readily as an editable one.
|
|
1620
|
+
const th = h('th', { scope: 'col', dataset: { column: column.id } });
|
|
1512
1621
|
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1513
1622
|
if (align) th.dataset.align = align;
|
|
1514
1623
|
if (column.headerTitle) th.title = column.headerTitle;
|
|
1515
|
-
|
|
1624
|
+
// A blank header (an icon or actions column) still needs a name, or every cell beneath it
|
|
1625
|
+
// is announced with an empty column header.
|
|
1626
|
+
const name = tableHeaderName(column);
|
|
1627
|
+
if (name !== null) th.setAttribute('aria-label', name);
|
|
1628
|
+
// Withheld, not disabled: a header that invites a sort the component will refuse is worse
|
|
1629
|
+
// than a header that never offered one. The label still reads exactly the same.
|
|
1630
|
+
if (column.sortable && !this._spanning) {
|
|
1516
1631
|
const button = h('button', {
|
|
1517
1632
|
class: 'zx-table__sort-button',
|
|
1518
|
-
type: 'button'
|
|
1633
|
+
type: 'button',
|
|
1634
|
+
ariaLabel: name ?? undefined
|
|
1519
1635
|
}, h('span', { class: 'zx-table__header-label' }, column.label));
|
|
1520
1636
|
this.listen(button, 'click', () => {
|
|
1521
1637
|
const direction = this._sort?.id === column.id && this._sort.dir === 'asc' ? 'desc' : 'asc';
|
|
@@ -1537,6 +1653,18 @@ export class Table extends Component {
|
|
|
1537
1653
|
// Every data mutation funnels through here, so this is the single place that guarantees no
|
|
1538
1654
|
// editor component outlives the rows it was rendered into.
|
|
1539
1655
|
this._abortEdit({ focus: false });
|
|
1656
|
+
/*
|
|
1657
|
+
* And the single place that notices the data gaining or losing its sections. The header owns
|
|
1658
|
+
* the sort controls, so it has to be rebuilt when the answer flips — otherwise a table that was
|
|
1659
|
+
* given grouped data after construction keeps offering a sort it will now refuse, and one whose
|
|
1660
|
+
* sections were filtered away never gets its sort back.
|
|
1661
|
+
*
|
|
1662
|
+
* Compared against what the *header* was built from, not against `_spanning`: `_sortData()`
|
|
1663
|
+
* runs first on every data mutation and has already brought `_spanning` up to date, so
|
|
1664
|
+
* comparing against it would find no change and leave the stale header in place.
|
|
1665
|
+
*/
|
|
1666
|
+
this._spanning = this._hasSpanningRow();
|
|
1667
|
+
if (this._headers && this._spanning !== this._headerSpanning) this._renderHeader();
|
|
1540
1668
|
const fragment = document.createDocumentFragment();
|
|
1541
1669
|
this._rowMeta = new WeakMap();
|
|
1542
1670
|
this._rowElements = new Map();
|
|
@@ -1546,11 +1674,7 @@ export class Table extends Component {
|
|
|
1546
1674
|
this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
|
|
1547
1675
|
if (this._data.length === 0) {
|
|
1548
1676
|
fragment.append(h('tr', { class: 'zx-table__empty-row' },
|
|
1549
|
-
h('td', {
|
|
1550
|
-
class: 'zx-table__empty',
|
|
1551
|
-
colspan: this._visibleColumns().length + (this.options.selectable === 'multi' ? 1 : 0)
|
|
1552
|
-
+ (this.options.rowReorder ? 1 : 0)
|
|
1553
|
-
}, this._emptyContent())
|
|
1677
|
+
h('td', { class: 'zx-table__empty', colspan: this._spanColspan() }, this._emptyContent())
|
|
1554
1678
|
));
|
|
1555
1679
|
} else {
|
|
1556
1680
|
// Indices stay absolute: a row's index is its place in the data, not in what is on screen.
|
|
@@ -1599,7 +1723,16 @@ export class Table extends Component {
|
|
|
1599
1723
|
if (this._growStep) tr.setAttribute('aria-rowindex', String(tree.visibleIndex + 2));
|
|
1600
1724
|
const rowClass = typeof this.options.rowClass === 'function' ? this.options.rowClass(row) : '';
|
|
1601
1725
|
if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
|
|
1602
|
-
|
|
1726
|
+
const kind = this._rowKind === null ? null : resolveRowKind(row, this._rowKind);
|
|
1727
|
+
const kindSpec = kind === null ? null : resolveRowKindSpec(this._rowKinds, kind);
|
|
1728
|
+
const spanning = Boolean(kindSpec?.span);
|
|
1729
|
+
if (kind !== null) tr.dataset.rowKind = kind;
|
|
1730
|
+
if (kindSpec?.class) tr.classList.add(...String(kindSpec.class).split(/\s+/).filter(Boolean));
|
|
1731
|
+
// A spanning row is a heading, not a record: `aria-selected="false"` would announce it as one
|
|
1732
|
+
// more selectable row that simply is not selected.
|
|
1733
|
+
if (this.options.selectable !== false && !spanning) {
|
|
1734
|
+
tr.setAttribute('aria-selected', String(this._selected.has(id)));
|
|
1735
|
+
}
|
|
1603
1736
|
if (this._hierarchy) {
|
|
1604
1737
|
tr.setAttribute('aria-level', String(tree.depth + 1));
|
|
1605
1738
|
if (tree.hasChildren) tr.setAttribute('aria-expanded', String(this._expanded.has(id)));
|
|
@@ -1609,6 +1742,25 @@ export class Table extends Component {
|
|
|
1609
1742
|
elements.push(tr);
|
|
1610
1743
|
this._rowElements.set(id, elements);
|
|
1611
1744
|
|
|
1745
|
+
if (spanning) {
|
|
1746
|
+
tr.dataset.span = 'true';
|
|
1747
|
+
/*
|
|
1748
|
+
* One cell, `<th scope="colgroup">`, covering the reorder and selection columns as well as
|
|
1749
|
+
* the data ones: the row labels the columns of the records beneath it, so it belongs in the
|
|
1750
|
+
* accessibility tree as a heading and not as a value — and a second, narrower cell beside it
|
|
1751
|
+
* would put a blank column back into exactly the layout this removes.
|
|
1752
|
+
*/
|
|
1753
|
+
const cell = /** @type {HTMLTableCellElement} */ (h('th', {
|
|
1754
|
+
class: 'zx-table__section-cell', scope: 'colgroup', colspan: this._spanColspan()
|
|
1755
|
+
}));
|
|
1756
|
+
const content = spanRowContent(row, kindSpec, this._visibleColumns(), index);
|
|
1757
|
+
if (content && typeof content === 'object' && typeof content.nodeType === 'number') {
|
|
1758
|
+
cell.append(content);
|
|
1759
|
+
} else if (content != null) cell.append(document.createTextNode(String(content)));
|
|
1760
|
+
tr.append(cell);
|
|
1761
|
+
return tr;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1612
1764
|
if (this.options.rowReorder) {
|
|
1613
1765
|
const handle = h('button', {
|
|
1614
1766
|
class: 'zx-table__row-handle', type: 'button', draggable: true,
|
|
@@ -1637,9 +1789,9 @@ export class Table extends Component {
|
|
|
1637
1789
|
// Written unconditionally: `responsive` can be switched on after the rows exist, and a
|
|
1638
1790
|
// stacked cell has no column header above it to say what its value means.
|
|
1639
1791
|
cell.dataset.label = column.label;
|
|
1792
|
+
cell.dataset.column = column.id;
|
|
1640
1793
|
if (column.popin === false) cell.dataset.popin = 'false';
|
|
1641
1794
|
if (this._editMode !== false) {
|
|
1642
|
-
cell.dataset.column = column.id;
|
|
1643
1795
|
if (resolveEditable(column, row) !== false) {
|
|
1644
1796
|
cell.dataset.editable = 'true';
|
|
1645
1797
|
cell.tabIndex = -1;
|
|
@@ -1718,7 +1870,9 @@ export class Table extends Component {
|
|
|
1718
1870
|
return;
|
|
1719
1871
|
}
|
|
1720
1872
|
|
|
1721
|
-
|
|
1873
|
+
// Only an editable table holds a click on a control inside a cell back from the row: there the
|
|
1874
|
+
// click could otherwise open an editor. A read-only table activates the row as it always has.
|
|
1875
|
+
if (this._editMode !== false && isInteractiveCellContent(target)) return;
|
|
1722
1876
|
|
|
1723
1877
|
if (this._editMode !== false && this._editTrigger === 'single') {
|
|
1724
1878
|
const cell = target?.closest('td[data-editable="true"]');
|
|
@@ -1729,7 +1883,10 @@ export class Table extends Component {
|
|
|
1729
1883
|
}
|
|
1730
1884
|
|
|
1731
1885
|
this.emit('rowclick', { ...meta, event });
|
|
1732
|
-
|
|
1886
|
+
// The click still reaches the host — collapsing a section is a reasonable thing to hang off it —
|
|
1887
|
+
// but it does not select the heading, which carries no checkbox in `multi` either.
|
|
1888
|
+
if (this.options.selectable === 'single' && !this._selected.has(meta.id)
|
|
1889
|
+
&& !this._isSpanningRow(meta.row)) {
|
|
1733
1890
|
this._selected = new Set([meta.id]);
|
|
1734
1891
|
this._selectionAnchorId = meta.id;
|
|
1735
1892
|
this._syncSelection();
|
|
@@ -1746,7 +1903,7 @@ export class Table extends Component {
|
|
|
1746
1903
|
if (!rowElement || !this._tbody.contains(rowElement)) return;
|
|
1747
1904
|
const meta = this._rowMeta.get(rowElement);
|
|
1748
1905
|
if (!meta) return;
|
|
1749
|
-
if (isInteractiveCellContent(target)) return;
|
|
1906
|
+
if (this._editMode !== false && isInteractiveCellContent(target)) return;
|
|
1750
1907
|
if (this._editMode !== false && this._editTrigger === 'double') {
|
|
1751
1908
|
const cell = target?.closest('td[data-editable="true"]');
|
|
1752
1909
|
if (cell && this._tbody.contains(cell)) {
|
|
@@ -1892,7 +2049,7 @@ export class Table extends Component {
|
|
|
1892
2049
|
...props,
|
|
1893
2050
|
class: textarea ? 'zx-table__editor-input zx-table__editor-textarea' : 'zx-table__editor-input',
|
|
1894
2051
|
...(textarea ? {} : { type: 'text' }),
|
|
1895
|
-
ariaLabel: props.ariaLabel ?? `${column
|
|
2052
|
+
ariaLabel: props.ariaLabel ?? `${tableColumnName(column)} row ${index + 1}`,
|
|
1896
2053
|
value: formatCellValue(column, value, row)
|
|
1897
2054
|
}));
|
|
1898
2055
|
host.append(input);
|
|
@@ -1911,8 +2068,10 @@ export class Table extends Component {
|
|
|
1911
2068
|
const control = entry?.control;
|
|
1912
2069
|
if (!control || typeof control.focus !== 'function') return;
|
|
1913
2070
|
control.focus();
|
|
1914
|
-
// Opening a text editor selects its content, so typing replaces the old value
|
|
1915
|
-
|
|
2071
|
+
// Opening a text, number, or date editor selects its content, so typing replaces the old value
|
|
2072
|
+
// ("6" over "1.00" gives 6, not 1.006). Selected after focus: NumberField rewrites its text on
|
|
2073
|
+
// focus, from the grouped display to a plain re-parseable number.
|
|
2074
|
+
if (editorSelectsOnEntry(entry.type) && typeof control.select === 'function') control.select();
|
|
1916
2075
|
}
|
|
1917
2076
|
|
|
1918
2077
|
/**
|
|
@@ -2093,6 +2252,8 @@ export class Table extends Component {
|
|
|
2093
2252
|
if (row < 0 || row >= visible.length) return null;
|
|
2094
2253
|
const candidateRow = visible[row];
|
|
2095
2254
|
const candidateColumn = columns[column];
|
|
2255
|
+
// Tab walks past a section heading rather than stopping on a row that has no cells to edit.
|
|
2256
|
+
if (this._isSpanningRow(candidateRow)) continue;
|
|
2096
2257
|
if (resolveEditable(candidateColumn, candidateRow) !== false) {
|
|
2097
2258
|
return {
|
|
2098
2259
|
id: this._idFor(candidateRow),
|
|
@@ -2172,6 +2333,9 @@ export class Table extends Component {
|
|
|
2172
2333
|
const start = Math.min(anchorIndex, currentIndex);
|
|
2173
2334
|
const end = Math.max(anchorIndex, currentIndex);
|
|
2174
2335
|
for (let index = start; index <= end; index += 1) {
|
|
2336
|
+
// Indices stay against the rendered rows so `visibleIndex` still lines up; a section caught
|
|
2337
|
+
// inside the range is stepped over rather than selected.
|
|
2338
|
+
if (this._isSpanningRow(visible[index])) continue;
|
|
2175
2339
|
const id = this._idFor(visible[index]);
|
|
2176
2340
|
if (checked) this._selected.add(id);
|
|
2177
2341
|
else this._selected.delete(id);
|
|
@@ -2191,6 +2355,9 @@ export class Table extends Component {
|
|
|
2191
2355
|
for (const [id, rows] of this._rowElements) {
|
|
2192
2356
|
const selected = this._selected.has(id);
|
|
2193
2357
|
for (const row of rows) {
|
|
2358
|
+
// A spanning row never claimed `aria-selected`; writing it here would tell a screen reader
|
|
2359
|
+
// the heading is a selectable record that happens to be unselected.
|
|
2360
|
+
if (row.hasAttribute('data-span')) continue;
|
|
2194
2361
|
row.setAttribute('aria-selected', String(selected));
|
|
2195
2362
|
const checkbox = row.querySelector('.zx-table__row-checkbox');
|
|
2196
2363
|
if (checkbox) checkbox.checked = selected;
|
|
@@ -2202,8 +2369,9 @@ export class Table extends Component {
|
|
|
2202
2369
|
/** @returns {void} */
|
|
2203
2370
|
_syncSelectAll() {
|
|
2204
2371
|
if (!this._selectAll) return;
|
|
2205
|
-
// Measured against what is rendered, so the box reflects the rows the reader can actually see
|
|
2206
|
-
|
|
2372
|
+
// Measured against what is rendered, so the box reflects the rows the reader can actually see —
|
|
2373
|
+
// and only the ones that can be ticked, so a section heading never holds the box indeterminate.
|
|
2374
|
+
const visible = this._selectableData();
|
|
2207
2375
|
const selectedRows = visible.reduce(
|
|
2208
2376
|
(count, row) => count + Number(this._selected.has(this._idFor(row))),
|
|
2209
2377
|
0
|
|
@@ -2217,14 +2385,20 @@ export class Table extends Component {
|
|
|
2217
2385
|
_syncSortHeader() {
|
|
2218
2386
|
if (!this._headers) return;
|
|
2219
2387
|
for (const [id, header] of this._headers) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2388
|
+
// A withheld control must not leave `aria-sort` behind: with the sort inert the attribute
|
|
2389
|
+
// would announce an order the rows are not actually in.
|
|
2390
|
+
if (!this._spanning && this._sort?.id === id) {
|
|
2391
|
+
header.setAttribute('aria-sort', this._sort.dir === 'asc' ? 'ascending' : 'descending');
|
|
2392
|
+
} else header.removeAttribute('aria-sort');
|
|
2222
2393
|
}
|
|
2223
2394
|
}
|
|
2224
2395
|
|
|
2225
2396
|
/** @returns {void} */
|
|
2226
2397
|
_sortData() {
|
|
2227
|
-
|
|
2398
|
+
// Recomputed here and not only in `_renderBody()`: every data mutation sorts before it renders,
|
|
2399
|
+
// so data that has just gained a section must be seen as grouped before the sort would run.
|
|
2400
|
+
this._spanning = this._hasSpanningRow();
|
|
2401
|
+
if (!this._sort || this._spanning) return;
|
|
2228
2402
|
const column = this._columns.find((candidate) => candidate.id === this._sort.id);
|
|
2229
2403
|
if (!column) return;
|
|
2230
2404
|
const getValue = typeof column.sortValue === 'function' ? column.sortValue : (row) => row?.[column.id];
|
|
@@ -2263,9 +2437,18 @@ export class Table extends Component {
|
|
|
2263
2437
|
for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
|
|
2264
2438
|
}
|
|
2265
2439
|
|
|
2440
|
+
/** @returns {Set<unknown>} Ids of every row in the data that may hold a selection. */
|
|
2441
|
+
_selectableIds() {
|
|
2442
|
+
const ids = new Set();
|
|
2443
|
+
for (const row of this._data) {
|
|
2444
|
+
if (!this._isSpanningRow(row)) ids.add(this._idFor(row));
|
|
2445
|
+
}
|
|
2446
|
+
return ids;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2266
2449
|
/** @returns {boolean} Whether selection changed. */
|
|
2267
2450
|
_pruneSelection() {
|
|
2268
|
-
const valid =
|
|
2451
|
+
const valid = this._selectableIds();
|
|
2269
2452
|
const previousSize = this._selected.size;
|
|
2270
2453
|
for (const id of this._selected) if (!valid.has(id)) this._selected.delete(id);
|
|
2271
2454
|
if (!valid.has(this._selectionAnchorId)) this._selectionAnchorId = null;
|
|
@@ -2337,6 +2520,104 @@ export class Table extends Component {
|
|
|
2337
2520
|
* @type {CustomEvent<TableEditInvalidDetail>}
|
|
2338
2521
|
*/
|
|
2339
2522
|
|
|
2523
|
+
/**
|
|
2524
|
+
* Resolves a row's kind through the `rowKind` accessor and coerces it to the string `rowKinds` is
|
|
2525
|
+
* keyed by.
|
|
2526
|
+
*
|
|
2527
|
+
* Only `null` and `undefined` mean "no kind". `0` and `''` are kinds like any other: a ZeyOS
|
|
2528
|
+
* transaction line uses `0` for a position and `1` for a section, so a falsy test here would render
|
|
2529
|
+
* every position as though the table had never been told about kinds at all.
|
|
2530
|
+
* @param {TableRow} row Row to classify.
|
|
2531
|
+
* @param {string|((row: TableRow) => unknown)|null|undefined} rowKind Field name or accessor.
|
|
2532
|
+
* @returns {string|null} The kind as a string, or null when there is none.
|
|
2533
|
+
*/
|
|
2534
|
+
export function resolveRowKind(row, rowKind) {
|
|
2535
|
+
if (rowKind == null || rowKind === '') return null;
|
|
2536
|
+
const raw = typeof rowKind === 'function' ? rowKind(row) : row?.[rowKind];
|
|
2537
|
+
return raw == null ? null : String(raw);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/**
|
|
2541
|
+
* Looks one resolved kind up in the `rowKinds` map.
|
|
2542
|
+
*
|
|
2543
|
+
* Own properties only: a kind read out of server data could just as easily be `toString` or
|
|
2544
|
+
* `constructor`, and inherited members of `Object.prototype` are not configuration.
|
|
2545
|
+
* @param {Record<string, TableRowKind>|null|undefined} rowKinds Kind map.
|
|
2546
|
+
* @param {string|null} kind Resolved kind.
|
|
2547
|
+
* @returns {TableRowKind|null} Matching entry, or null.
|
|
2548
|
+
*/
|
|
2549
|
+
export function resolveRowKindSpec(rowKinds, kind) {
|
|
2550
|
+
if (kind === null || rowKinds == null || typeof rowKinds !== 'object') return null;
|
|
2551
|
+
if (!Object.prototype.hasOwnProperty.call(rowKinds, kind)) return null;
|
|
2552
|
+
const spec = rowKinds[kind];
|
|
2553
|
+
return spec && typeof spec === 'object' ? spec : null;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/**
|
|
2557
|
+
* Whether a row draws as one spanning cell under the given configuration.
|
|
2558
|
+
* @param {TableRow} row Row to classify.
|
|
2559
|
+
* @param {string|((row: TableRow) => unknown)|null|undefined} rowKind Field name or accessor.
|
|
2560
|
+
* @param {Record<string, TableRowKind>|null|undefined} rowKinds Kind map.
|
|
2561
|
+
* @returns {boolean}
|
|
2562
|
+
*/
|
|
2563
|
+
export function isSpanningRow(row, rowKind, rowKinds) {
|
|
2564
|
+
return Boolean(resolveRowKindSpec(rowKinds, resolveRowKind(row, rowKind))?.span);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Whether a data set holds a row that spans — the question that decides whether `Table` offers its
|
|
2569
|
+
* sort controls at all.
|
|
2570
|
+
*
|
|
2571
|
+
* Asked of the whole data rather than of the rendered prefix: with `growing` on, a sort control
|
|
2572
|
+
* that disappeared the moment the reader pressed "show 20 more" would be worse than one that was
|
|
2573
|
+
* never there. Asked again on every data change, so a filter that removes the last section gives
|
|
2574
|
+
* the sort back.
|
|
2575
|
+
* @param {TableRow[]} rows Rows to scan.
|
|
2576
|
+
* @param {string|((row: TableRow) => unknown)|null|undefined} rowKind Field name or accessor.
|
|
2577
|
+
* @param {Record<string, TableRowKind>|null|undefined} rowKinds Kind map.
|
|
2578
|
+
* @returns {boolean}
|
|
2579
|
+
*/
|
|
2580
|
+
export function hasSpanningRow(rows, rowKind, rowKinds) {
|
|
2581
|
+
if (!Array.isArray(rows) || rowKind == null || rowKinds == null) return false;
|
|
2582
|
+
return rows.some((row) => isSpanningRow(row, rowKind, rowKinds));
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/**
|
|
2586
|
+
* How many cells one table row occupies.
|
|
2587
|
+
*
|
|
2588
|
+
* `selectable: 'multi'` and `rowReorder` each render a `<col>` and a leading cell of their own, and
|
|
2589
|
+
* a spanning cell that ignored them would leave the row one or two columns short — the table would
|
|
2590
|
+
* still lay out, silently and wrongly. `selectable: 'single'` adds no column: the whole row is the
|
|
2591
|
+
* control there.
|
|
2592
|
+
* @param {number} columnCount Number of visible data columns.
|
|
2593
|
+
* @param {{selectable?: TableSelectionMode, rowReorder?: boolean}} [structure={}] Structural columns.
|
|
2594
|
+
* @returns {number} Colspan covering every rendered cell, never below 1.
|
|
2595
|
+
*/
|
|
2596
|
+
export function tableSpanColspan(columnCount, structure = {}) {
|
|
2597
|
+
const columns = Number.isFinite(Number(columnCount)) ? Math.max(0, Math.trunc(Number(columnCount))) : 0;
|
|
2598
|
+
const extras = (structure.selectable === 'multi' ? 1 : 0) + (structure.rowReorder ? 1 : 0);
|
|
2599
|
+
return Math.max(1, columns + extras);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* Content for a spanning row's single cell.
|
|
2604
|
+
*
|
|
2605
|
+
* Without a `render` the cell shows the row's raw value for the first visible column — the same
|
|
2606
|
+
* thing a host writes by hand today. Deliberately raw rather than routed through the column's own
|
|
2607
|
+
* renderer or display type: that renderer was written for a record, and running it against a
|
|
2608
|
+
* heading that carries no quantities or amounts is how a section row ends up showing `0,00 €`.
|
|
2609
|
+
* @param {TableRow} row Row being rendered.
|
|
2610
|
+
* @param {TableRowKind|null|undefined} spec Resolved kind presentation.
|
|
2611
|
+
* @param {TableColumn[]} visibleColumns Columns currently rendered, in display order.
|
|
2612
|
+
* @param {number} [index=0] Flat row index.
|
|
2613
|
+
* @returns {Node|string|number|null|undefined} Cell content.
|
|
2614
|
+
*/
|
|
2615
|
+
export function spanRowContent(row, spec, visibleColumns, index = 0) {
|
|
2616
|
+
if (typeof spec?.render === 'function') return spec.render(row, index);
|
|
2617
|
+
const first = visibleColumns?.[0];
|
|
2618
|
+
return first ? row?.[first.id] : null;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2340
2621
|
/**
|
|
2341
2622
|
* Applies a column's built-in display type. A custom renderer always wins and may still return a
|
|
2342
2623
|
* Node; all built-in results are strings so `_renderCellContent` can insert them as text.
|
|
@@ -2724,11 +3005,42 @@ function normalizeHierarchy(option, columns) {
|
|
|
2724
3005
|
return { ...option, parentId, column };
|
|
2725
3006
|
}
|
|
2726
3007
|
|
|
3008
|
+
/**
|
|
3009
|
+
* Normalizes the `rowKind` accessor. Anything that is not a non-empty field name or a callback is
|
|
3010
|
+
* `null`, which is the switch that keeps every existing table on exactly its old rendering path.
|
|
3011
|
+
* @param {unknown} option Configured accessor.
|
|
3012
|
+
* @returns {string|((row: TableRow) => unknown)|null}
|
|
3013
|
+
*/
|
|
3014
|
+
function normalizeRowKind(option) {
|
|
3015
|
+
if (typeof option === 'function') return /** @type {(row: TableRow) => unknown} */ (option);
|
|
3016
|
+
return typeof option === 'string' && option !== '' ? option : null;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* Normalizes the `rowKinds` map. An array, a function or a primitive is not a keyed map and is
|
|
3021
|
+
* discarded rather than half-read.
|
|
3022
|
+
* @param {unknown} option Configured kind map.
|
|
3023
|
+
* @returns {Record<string, TableRowKind>|null}
|
|
3024
|
+
*/
|
|
3025
|
+
function normalizeRowKinds(option) {
|
|
3026
|
+
if (!option || typeof option !== 'object' || Array.isArray(option)) return null;
|
|
3027
|
+
return /** @type {Record<string, TableRowKind>} */ (option);
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2727
3030
|
/** @param {unknown} option @returns {Set<unknown>} */
|
|
2728
3031
|
function initialExpanded(option) {
|
|
2729
3032
|
return new Set(Array.isArray(option) ? option : []);
|
|
2730
3033
|
}
|
|
2731
3034
|
|
|
3035
|
+
/**
|
|
3036
|
+
* Resolves the `variant` option to one the stylesheet knows; anything else is the default frame.
|
|
3037
|
+
* @param {unknown} value Requested variant.
|
|
3038
|
+
* @returns {TableVariant}
|
|
3039
|
+
*/
|
|
3040
|
+
export function normalizeTableVariant(value) {
|
|
3041
|
+
return value === 'plain' ? 'plain' : 'default';
|
|
3042
|
+
}
|
|
3043
|
+
|
|
2732
3044
|
/** @param {unknown} mode @returns {TableEditMode} */
|
|
2733
3045
|
function normalizeEditMode(mode) {
|
|
2734
3046
|
if (mode === 'cell' || mode === 'row') return mode;
|
|
@@ -2780,6 +3092,47 @@ export function normalizeTableColumns(columns) {
|
|
|
2780
3092
|
});
|
|
2781
3093
|
}
|
|
2782
3094
|
|
|
3095
|
+
/**
|
|
3096
|
+
* The name a column goes by wherever it is named rather than shown: its `ariaLabel`, else its
|
|
3097
|
+
* visible label, else its id. The column chooser lists columns by it and cell editors are labelled
|
|
3098
|
+
* with it, so a column with a blank header still has a name in both.
|
|
3099
|
+
* @param {Partial<TableColumn>|null|undefined} column Column descriptor.
|
|
3100
|
+
* @returns {string} Non-empty name, unless the column has no id either.
|
|
3101
|
+
*/
|
|
3102
|
+
export function tableColumnName(column) {
|
|
3103
|
+
for (const candidate of [column?.ariaLabel, column?.label]) {
|
|
3104
|
+
const text = candidate == null ? '' : String(candidate).trim();
|
|
3105
|
+
if (text) return text;
|
|
3106
|
+
}
|
|
3107
|
+
return String(column?.id ?? '');
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/**
|
|
3111
|
+
* The accessible name a column header needs on top of its content, or null when its visible label
|
|
3112
|
+
* already names it. That is the `ariaLabel` when one is given, and the column id when the label is
|
|
3113
|
+
* blank — an icon or actions column — so no header is announced as an empty column.
|
|
3114
|
+
* @param {Partial<TableColumn>|null|undefined} column Column descriptor.
|
|
3115
|
+
* @returns {string|null} Name to set as `aria-label`, or null.
|
|
3116
|
+
*/
|
|
3117
|
+
export function tableHeaderName(column) {
|
|
3118
|
+
const label = column?.label == null ? '' : String(column.label).trim();
|
|
3119
|
+
const explicit = column?.ariaLabel == null ? '' : String(column.ariaLabel).trim();
|
|
3120
|
+
if (explicit) return explicit;
|
|
3121
|
+
return label ? null : tableColumnName(column) || null;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
/**
|
|
3125
|
+
* Whether opening an editor of this type selects its content, so the first keystroke replaces the
|
|
3126
|
+
* value instead of being appended to it. True for the single-line text, number, and date editors;
|
|
3127
|
+
* a textarea keeps its caret, and a select, checkbox, or custom editor keeps its own focus
|
|
3128
|
+
* behaviour.
|
|
3129
|
+
* @param {TableEditorType|string} type Resolved editor type.
|
|
3130
|
+
* @returns {boolean}
|
|
3131
|
+
*/
|
|
3132
|
+
export function editorSelectsOnEntry(type) {
|
|
3133
|
+
return type === 'text' || type === 'number' || type === 'date';
|
|
3134
|
+
}
|
|
3135
|
+
|
|
2783
3136
|
/**
|
|
2784
3137
|
* Reconciles saved or partial column IDs with every current column.
|
|
2785
3138
|
* @param {TableColumn[]} columns Current columns.
|