@zeyos/zx 2.3.0 → 4.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 +138 -0
- package/DESIGN-SYSTEM.md +96 -0
- package/README.md +29 -39
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +322 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +359 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +51 -0
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/table/table.d.ts +223 -15
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +122 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +13 -2
- package/dist/types/internal/app-rail.d.ts +265 -0
- package/dist/types/zeyos/icons.d.ts +5 -0
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +39 -0
- package/dist/zx-zeyos.esm.js +1870 -133
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2006 -267
- package/dist/zx.esm.js +8987 -2757
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8987 -2757
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +4396 -2344
- package/docs/llms.md +463 -86
- package/docs/llms.txt +32 -19
- package/package.json +10 -17
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-sidebar/app-sidebar.css +80 -0
- package/src/components/app-sidebar/app-sidebar.js +588 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +523 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +220 -0
- package/src/components/launcher/launcher.css +210 -0
- package/src/components/launcher/launcher.js +765 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.js +2 -1
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +144 -11
- package/src/components/select/select.js +259 -27
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +531 -52
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +322 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +20 -2
- package/src/internal/app-rail.js +718 -0
- package/src/zeyos/icons.js +8 -3
- package/src/zeyos/index.js +7 -1
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +115 -1
- package/styles/tokens/dark.css +2 -3
- package/styles/tokens/global.css +6 -6
- package/styles/tokens/modules.css +1 -0
- package/styles/tokens/semantic.css +5 -3
- package/styles/tokens/themes.css +1 -1
- package/styles/zx.css +11 -1
- package/MIGRATION.md +0 -598
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -102
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/types/components/permission/permission.d.ts +0 -96
- package/dist/zx-compat.esm.js +0 -17484
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -23635
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -165
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -232
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -201
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { breakpoints, onBreakpoint } from '../../core/breakpoint.js';
|
|
3
|
-
import { h } from '../../core/dom.js';
|
|
3
|
+
import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
4
|
+
import { formatCurrency, formatNumber, formatPercent } from '../../core/format.js';
|
|
5
|
+
import { icon } from '../../core/icons.js';
|
|
4
6
|
import { printf } from '../../core/i18n.js';
|
|
5
7
|
import { uid } from '../../core/util.js';
|
|
6
8
|
import { Datebox } from '../datebox/datebox.js';
|
|
@@ -14,8 +16,18 @@ import { sortRows } from './sort.js';
|
|
|
14
16
|
/** @typedef {false|'single'|'multi'} TableSelectionMode */
|
|
15
17
|
/** @typedef {false|'cell'|'row'} TableEditMode */
|
|
16
18
|
/** @typedef {'text'|'number'|'select'|'date'|'checkbox'|'textarea'|'custom'} TableEditorType */
|
|
19
|
+
/** @typedef {'text'|'number'|'currency'|'percent'|'unit'} TableColumnType */
|
|
17
20
|
/** @typedef {{value: unknown, label: string}} TableEditorOption */
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} TableHierarchyOptions
|
|
24
|
+
* @property {string|((row: TableRow) => unknown)} parentId Row property or callback returning its
|
|
25
|
+
* parent row id. Null and undefined identify a root row.
|
|
26
|
+
* @property {string} [column] Column that receives indentation and the disclosure control; the
|
|
27
|
+
* first column is used when omitted.
|
|
28
|
+
* @property {boolean|unknown[]} [expanded=false] Initially expanded branches: all, none, or ids.
|
|
29
|
+
*/
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* Editing handle handed to a custom `column.editor`.
|
|
21
33
|
* @typedef {Object} TableEditorApi
|
|
@@ -39,11 +51,19 @@ import { sortRows } from './sort.js';
|
|
|
39
51
|
* Cell renderer. Non-Node results are inserted as text.
|
|
40
52
|
* @property {(row: TableRow) => unknown} [sortValue] Sort-value accessor.
|
|
41
53
|
* @property {string} [headerTitle] Header tooltip.
|
|
54
|
+
* @property {TableColumnType} [type='text'] Built-in display formatter used when `render` is absent.
|
|
55
|
+
* @property {string|((row: TableRow) => string|null|undefined)} [locale] BCP 47 formatting locale.
|
|
56
|
+
* @property {number|((row: TableRow) => number|null|undefined)} [decimals] Fraction digits.
|
|
57
|
+
* @property {string|((row: TableRow) => string|null|undefined)} [currency] ISO 4217 code used by a
|
|
58
|
+
* `currency` column.
|
|
59
|
+
* @property {string|((row: TableRow) => string|null|undefined)} [unit] Suffix used by a `unit`
|
|
60
|
+
* column. Row callbacks support mixed units in one transaction table.
|
|
42
61
|
* @property {boolean} [popin=true] While stacked, whether this column becomes a labelled line.
|
|
43
62
|
* `false` keeps it in the card's headline — the identifying columns usually want that.
|
|
44
63
|
* @property {boolean|TableEditorType|((row: TableRow) => boolean|TableEditorType)} [editable=false]
|
|
45
|
-
* Editor for this column while the table runs with `editMode`. `true`
|
|
46
|
-
* evaluated per row so individual rows stay
|
|
64
|
+
* Editor for this column while the table runs with `editMode`. `true` infers `'number'` for numeric
|
|
65
|
+
* display types and `'text'` otherwise; a function is evaluated per row so individual rows stay
|
|
66
|
+
* read-only.
|
|
47
67
|
* @property {TableEditorOption[]|Record<string, string>|((row: TableRow) => TableEditorOption[]|Record<string, string>)} [options]
|
|
48
68
|
* Choices for an `'select'` editor: `[{value, label}]`, a `{value: label}` map, or a row callback.
|
|
49
69
|
* @property {Record<string, unknown>} [editorProps] Options forwarded to the underlying editor
|
|
@@ -84,6 +104,8 @@ import { sortRows } from './sort.js';
|
|
|
84
104
|
* @property {TableEditMode} [editMode=false] Inline editing mode. `'cell'` edits one cell, `'row'`
|
|
85
105
|
* opens every editable cell of the row together and commits them as a unit. Editing is completely
|
|
86
106
|
* inert while this is `false`.
|
|
107
|
+
* @property {false|TableHierarchyOptions} [hierarchy=false] Flat parent references projected as an
|
|
108
|
+
* expandable treegrid. Row data and mutation APIs remain flat.
|
|
87
109
|
* @property {(event: CustomEvent<TableRowClickDetail>) => void} [onrowclick]
|
|
88
110
|
* @property {(event: CustomEvent<TableRowClickDetail>) => void} [onrowdblclick]
|
|
89
111
|
* @property {(event: CustomEvent<TableSortDetail>) => void} [onsort]
|
|
@@ -97,6 +119,7 @@ import { sortRows } from './sort.js';
|
|
|
97
119
|
* @property {(event: CustomEvent<TableEditCommitDetail>) => void} [oneditcommit]
|
|
98
120
|
* @property {(event: CustomEvent<TableEditCancelDetail>) => void} [oneditcancel]
|
|
99
121
|
* @property {(event: CustomEvent<TableEditInvalidDetail>) => void} [oneditinvalid]
|
|
122
|
+
* @property {(event: CustomEvent<TableRowToggleDetail>) => void} [onrowtoggle]
|
|
100
123
|
*/
|
|
101
124
|
|
|
102
125
|
/**
|
|
@@ -110,6 +133,27 @@ import { sortRows } from './sort.js';
|
|
|
110
133
|
/** @typedef {{id: string, dir: TableSortDirection}} TableSortDetail */
|
|
111
134
|
/** @typedef {{rows: TableRow[], ids: unknown[]}} TableSelectionDetail */
|
|
112
135
|
/** @typedef {{rows: TableRow[]}} TableDataDetail */
|
|
136
|
+
/** @typedef {{row: TableRow, id: unknown, expanded: boolean}} TableRowToggleDetail */
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Visible hierarchy metadata returned by `projectHierarchyRows`.
|
|
140
|
+
* @typedef {Object} TableHierarchyEntry
|
|
141
|
+
* @property {TableRow} row
|
|
142
|
+
* @property {unknown} id
|
|
143
|
+
* @property {number} index Index in the flat input data.
|
|
144
|
+
* @property {number} depth Zero-based visual depth.
|
|
145
|
+
* @property {boolean} hasChildren
|
|
146
|
+
* @property {unknown} parentId Canonical parent id, or null for a root.
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @typedef {Object} TableHierarchyNode
|
|
151
|
+
* @property {TableRow} row
|
|
152
|
+
* @property {unknown} id
|
|
153
|
+
* @property {number} index
|
|
154
|
+
* @property {TableHierarchyNode|null} parent
|
|
155
|
+
* @property {TableHierarchyNode[]} children
|
|
156
|
+
*/
|
|
113
157
|
|
|
114
158
|
/**
|
|
115
159
|
* @typedef {Object} TableEditStartDetail
|
|
@@ -179,6 +223,9 @@ const FOCUSABLE = 'input, textarea, select, button, [tabindex]:not([tabindex="-1
|
|
|
179
223
|
* @fires Table#editcommit
|
|
180
224
|
* @fires Table#editcancel
|
|
181
225
|
* @fires Table#editinvalid
|
|
226
|
+
* @fires Table#rowtoggle
|
|
227
|
+
* @fires Table#stackedchange
|
|
228
|
+
* @fires Table#grow
|
|
182
229
|
* @extends {Component<TableOptions>}
|
|
183
230
|
*/
|
|
184
231
|
export class Table extends Component {
|
|
@@ -199,7 +246,8 @@ export class Table extends Component {
|
|
|
199
246
|
emptyText: null,
|
|
200
247
|
rowClass: null,
|
|
201
248
|
zebra: true,
|
|
202
|
-
editMode: false
|
|
249
|
+
editMode: false,
|
|
250
|
+
hierarchy: false
|
|
203
251
|
};
|
|
204
252
|
|
|
205
253
|
/**
|
|
@@ -213,11 +261,13 @@ export class Table extends Component {
|
|
|
213
261
|
|
|
214
262
|
/** @returns {HTMLElement} Table root. */
|
|
215
263
|
render() {
|
|
264
|
+
const created = this.el === null;
|
|
216
265
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
217
266
|
// Claimed here, not left to the base constructor: `_watchWidth()` below observes `this.el`,
|
|
218
267
|
// and until this assignment it is null for a component creating its own root.
|
|
219
268
|
this.el = root;
|
|
220
|
-
this.
|
|
269
|
+
this._createdRoot = created;
|
|
270
|
+
this._original = created ? null : snapshotTarget(root);
|
|
221
271
|
this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
|
|
222
272
|
this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
|
|
223
273
|
this._selected = new Set();
|
|
@@ -239,10 +289,19 @@ export class Table extends Component {
|
|
|
239
289
|
this._rovingKey = null;
|
|
240
290
|
this._rovingCell = null;
|
|
241
291
|
this._rovingMatched = false;
|
|
292
|
+
this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
|
|
293
|
+
this._expanded = initialExpanded(this._hierarchy?.expanded);
|
|
242
294
|
|
|
243
295
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
296
|
+
if (this._hierarchy?.expanded === true) {
|
|
297
|
+
for (const id of hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId)) {
|
|
298
|
+
this._expanded.add(id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this._reconcileExpanded();
|
|
244
302
|
|
|
245
303
|
this._table = h('table', { class: 'zx-table__table' });
|
|
304
|
+
if (this._hierarchy) this._table.setAttribute('role', 'treegrid');
|
|
246
305
|
this._colgroup = h('colgroup', { class: 'zx-table__columns' });
|
|
247
306
|
this._thead = h('thead', { class: 'zx-table__head' });
|
|
248
307
|
this._tbody = h('tbody', { class: 'zx-table__body' });
|
|
@@ -290,8 +349,8 @@ export class Table extends Component {
|
|
|
290
349
|
|
|
291
350
|
/**
|
|
292
351
|
* Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
|
|
293
|
-
* shown, and `aria-busy` is set — a lightweight
|
|
294
|
-
*
|
|
352
|
+
* shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
|
|
353
|
+
* Call `setLoading(false)` (or `setData`) when done.
|
|
295
354
|
* @param {boolean} [loading=true] Whether the table is loading.
|
|
296
355
|
* @returns {this}
|
|
297
356
|
*/
|
|
@@ -316,6 +375,7 @@ export class Table extends Component {
|
|
|
316
375
|
assertRows(rows);
|
|
317
376
|
this._data = [...rows];
|
|
318
377
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
378
|
+
this._reconcileExpanded();
|
|
319
379
|
const selectionChanged = this._pruneSelection();
|
|
320
380
|
this._renderBody();
|
|
321
381
|
if (selectionChanged) this._emitSelectionChange();
|
|
@@ -334,6 +394,7 @@ export class Table extends Component {
|
|
|
334
394
|
assertRows(rows);
|
|
335
395
|
this._data.push(...rows);
|
|
336
396
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
397
|
+
this._reconcileExpanded();
|
|
337
398
|
this._renderBody();
|
|
338
399
|
this._emitDataChange();
|
|
339
400
|
return this;
|
|
@@ -354,6 +415,7 @@ export class Table extends Component {
|
|
|
354
415
|
this._data[index] = row;
|
|
355
416
|
if (wasSelected) this._selected.add(this._idFor(row));
|
|
356
417
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
418
|
+
this._reconcileExpanded();
|
|
357
419
|
this._renderBody();
|
|
358
420
|
if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
|
|
359
421
|
this._emitDataChange();
|
|
@@ -372,6 +434,7 @@ export class Table extends Component {
|
|
|
372
434
|
const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
|
|
373
435
|
if (index < 0) return this;
|
|
374
436
|
this._data.splice(index, 1);
|
|
437
|
+
this._reconcileExpanded();
|
|
375
438
|
const selectionChanged = this._selected.delete(id);
|
|
376
439
|
if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
|
|
377
440
|
this._renderBody();
|
|
@@ -389,7 +452,10 @@ export class Table extends Component {
|
|
|
389
452
|
return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
|
|
390
453
|
}
|
|
391
454
|
|
|
392
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* Returns a shallow copy of the current rows in display order.
|
|
457
|
+
* @returns {TableRow[]} Current table rows.
|
|
458
|
+
*/
|
|
393
459
|
getData() {
|
|
394
460
|
return [...this._data];
|
|
395
461
|
}
|
|
@@ -404,6 +470,7 @@ export class Table extends Component {
|
|
|
404
470
|
empty() {
|
|
405
471
|
const selectionChanged = this._selected.size > 0;
|
|
406
472
|
this._data = [];
|
|
473
|
+
this._expanded.clear();
|
|
407
474
|
this._selected.clear();
|
|
408
475
|
this._selectionAnchorId = null;
|
|
409
476
|
this._renderBody();
|
|
@@ -442,7 +509,10 @@ export class Table extends Component {
|
|
|
442
509
|
return this;
|
|
443
510
|
}
|
|
444
511
|
|
|
445
|
-
/**
|
|
512
|
+
/**
|
|
513
|
+
* Returns selected rows in current display order.
|
|
514
|
+
* @returns {TableRow[]} Selected rows.
|
|
515
|
+
*/
|
|
446
516
|
getSelection() {
|
|
447
517
|
return this._data.filter((row) => this._selected.has(this._idFor(row)));
|
|
448
518
|
}
|
|
@@ -484,6 +554,81 @@ export class Table extends Component {
|
|
|
484
554
|
return this;
|
|
485
555
|
}
|
|
486
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Toggles one hierarchy branch. A leaf or an id absent from the table is a no-op.
|
|
559
|
+
* @param {unknown} id Row id.
|
|
560
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
561
|
+
* @returns {this}
|
|
562
|
+
* @fires Table#rowtoggle
|
|
563
|
+
*/
|
|
564
|
+
toggleRow(id, options = {}) {
|
|
565
|
+
return this._expanded.has(id) ? this.collapseRow(id, options) : this.expandRow(id, options);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Expands one hierarchy branch.
|
|
570
|
+
* @param {unknown} id Row id.
|
|
571
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
572
|
+
* @returns {this}
|
|
573
|
+
* @fires Table#rowtoggle
|
|
574
|
+
*/
|
|
575
|
+
expandRow(id, options = {}) {
|
|
576
|
+
return this._setRowExpanded(id, true, options);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Collapses one hierarchy branch without discarding its descendants or their expansion state.
|
|
581
|
+
* @param {unknown} id Row id.
|
|
582
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
583
|
+
* @returns {this}
|
|
584
|
+
* @fires Table#rowtoggle
|
|
585
|
+
*/
|
|
586
|
+
collapseRow(id, options = {}) {
|
|
587
|
+
return this._setRowExpanded(id, false, options);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Expands every row that currently has children.
|
|
592
|
+
* @param {{silent?: boolean}} [options={}] Event behavior. When not silent, one `rowtoggle`
|
|
593
|
+
* event is emitted per branch whose state changed.
|
|
594
|
+
* @returns {this}
|
|
595
|
+
*/
|
|
596
|
+
expandAll(options = {}) {
|
|
597
|
+
if (!this._hierarchy) return this;
|
|
598
|
+
const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
599
|
+
const changed = branchIds.filter((id) => !this._expanded.has(id));
|
|
600
|
+
for (const id of changed) this._expanded.add(id);
|
|
601
|
+
if (changed.length) this._renderBody();
|
|
602
|
+
if (!options.silent) {
|
|
603
|
+
for (const id of changed) this._emitRowToggle(id, true);
|
|
604
|
+
}
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Collapses every hierarchy branch.
|
|
610
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
611
|
+
* @returns {this}
|
|
612
|
+
*/
|
|
613
|
+
collapseAll(options = {}) {
|
|
614
|
+
if (!this._hierarchy || this._expanded.size === 0) return this;
|
|
615
|
+
const changed = [...this._expanded];
|
|
616
|
+
this._expanded.clear();
|
|
617
|
+
this._renderBody();
|
|
618
|
+
if (!options.silent) {
|
|
619
|
+
for (const id of changed) this._emitRowToggle(id, false);
|
|
620
|
+
}
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Returns expanded branch ids in current flat-data order.
|
|
626
|
+
* @returns {unknown[]} Expanded branch ids.
|
|
627
|
+
*/
|
|
628
|
+
getExpanded() {
|
|
629
|
+
return this._data.map((row) => this._idFor(row)).filter((id) => this._expanded.has(id));
|
|
630
|
+
}
|
|
631
|
+
|
|
487
632
|
/**
|
|
488
633
|
* Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
|
|
489
634
|
* resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
|
|
@@ -607,12 +752,18 @@ export class Table extends Component {
|
|
|
607
752
|
return this;
|
|
608
753
|
}
|
|
609
754
|
|
|
610
|
-
/**
|
|
755
|
+
/**
|
|
756
|
+
* Reports whether a cell or row is currently open for editing.
|
|
757
|
+
* @returns {boolean} Whether an edit is open.
|
|
758
|
+
*/
|
|
611
759
|
isEditing() {
|
|
612
760
|
return this._edit !== null;
|
|
613
761
|
}
|
|
614
762
|
|
|
615
|
-
/**
|
|
763
|
+
/**
|
|
764
|
+
* Returns the row id and focused column id for the open edit.
|
|
765
|
+
* @returns {{id: unknown, columnId: string}|null} Editing location, or null.
|
|
766
|
+
*/
|
|
616
767
|
getEditing() {
|
|
617
768
|
return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
|
|
618
769
|
}
|
|
@@ -622,7 +773,25 @@ export class Table extends Component {
|
|
|
622
773
|
* @returns {TableRow[]}
|
|
623
774
|
*/
|
|
624
775
|
_visibleData() {
|
|
625
|
-
return this.
|
|
776
|
+
return this._visibleEntries().map((entry) => entry.row);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** @returns {TableHierarchyEntry[]} Rows visible after hierarchy expansion, before growing. */
|
|
780
|
+
_projectedData() {
|
|
781
|
+
if (!this._hierarchy) {
|
|
782
|
+
return this._data.map((row, index) => ({
|
|
783
|
+
row, id: this._idFor(row), index, depth: 0, hasChildren: false, parentId: null
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
786
|
+
return projectHierarchyRows(
|
|
787
|
+
this._data, this.options.rowId, this._hierarchy.parentId, this._expanded
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** @returns {TableHierarchyEntry[]} Rows that currently have DOM entries. */
|
|
792
|
+
_visibleEntries() {
|
|
793
|
+
const projected = this._projectedData();
|
|
794
|
+
return this._rendered === Infinity ? projected : projected.slice(0, this._rendered);
|
|
626
795
|
}
|
|
627
796
|
|
|
628
797
|
/**
|
|
@@ -635,10 +804,11 @@ export class Table extends Component {
|
|
|
635
804
|
if (!this._growStep) return this;
|
|
636
805
|
const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
|
|
637
806
|
const before = this._rendered;
|
|
638
|
-
|
|
807
|
+
const total = this._projectedData().length;
|
|
808
|
+
this._rendered = Math.min(total, this._rendered + step);
|
|
639
809
|
if (this._rendered === before) return this;
|
|
640
810
|
this._renderBody();
|
|
641
|
-
this.emit('grow', { rendered: this._rendered, total
|
|
811
|
+
this.emit('grow', { rendered: this._rendered, total });
|
|
642
812
|
return this;
|
|
643
813
|
}
|
|
644
814
|
|
|
@@ -648,7 +818,7 @@ export class Table extends Component {
|
|
|
648
818
|
* @fires Table#grow
|
|
649
819
|
*/
|
|
650
820
|
showAll() {
|
|
651
|
-
return this.growBy(this.
|
|
821
|
+
return this.growBy(this._projectedData().length);
|
|
652
822
|
}
|
|
653
823
|
|
|
654
824
|
/**
|
|
@@ -656,7 +826,7 @@ export class Table extends Component {
|
|
|
656
826
|
* @returns {number}
|
|
657
827
|
*/
|
|
658
828
|
getRenderedCount() {
|
|
659
|
-
return Math.min(this._rendered, this.
|
|
829
|
+
return Math.min(this._rendered, this._projectedData().length);
|
|
660
830
|
}
|
|
661
831
|
|
|
662
832
|
/**
|
|
@@ -669,7 +839,7 @@ export class Table extends Component {
|
|
|
669
839
|
*/
|
|
670
840
|
_syncGrowing() {
|
|
671
841
|
if (!this._growStep) return;
|
|
672
|
-
const remaining = this.
|
|
842
|
+
const remaining = this._projectedData().length - this.getRenderedCount();
|
|
673
843
|
if (remaining <= 0) {
|
|
674
844
|
this._more?.remove();
|
|
675
845
|
this._more = null;
|
|
@@ -746,10 +916,11 @@ export class Table extends Component {
|
|
|
746
916
|
*/
|
|
747
917
|
_applyStackedRoles(stacked) {
|
|
748
918
|
/** @type {Array<[Element|null, string]>} */
|
|
749
|
-
const parts = [[this._table, 'table'], [this._thead, 'rowgroup'], [this._tbody, 'rowgroup']];
|
|
919
|
+
const parts = [[this._table, this._hierarchy ? 'treegrid' : 'table'], [this._thead, 'rowgroup'], [this._tbody, 'rowgroup']];
|
|
750
920
|
for (const [element, role] of parts) {
|
|
751
921
|
if (!element) continue;
|
|
752
922
|
if (stacked) element.setAttribute('role', role);
|
|
923
|
+
else if (element === this._table && this._hierarchy) element.setAttribute('role', 'treegrid');
|
|
753
924
|
else element.removeAttribute('role');
|
|
754
925
|
}
|
|
755
926
|
for (const [selector, role] of [['tr', 'row'], ['td', 'cell'], ['th', 'columnheader']]) {
|
|
@@ -773,9 +944,8 @@ export class Table extends Component {
|
|
|
773
944
|
this._teardownEdit();
|
|
774
945
|
this._width?.destroy();
|
|
775
946
|
this._width = null;
|
|
776
|
-
if (this.
|
|
777
|
-
|
|
778
|
-
this.el.replaceChildren(...this._originalChildren);
|
|
947
|
+
if (!this._restored && !this._createdRoot) {
|
|
948
|
+
restoreTarget(this.el, this._original);
|
|
779
949
|
this._restored = true;
|
|
780
950
|
}
|
|
781
951
|
super.destroy();
|
|
@@ -836,7 +1006,8 @@ export class Table extends Component {
|
|
|
836
1006
|
this._headers = new Map();
|
|
837
1007
|
for (const column of this._columns) {
|
|
838
1008
|
const th = h('th', { scope: 'col' });
|
|
839
|
-
|
|
1009
|
+
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1010
|
+
if (align) th.dataset.align = align;
|
|
840
1011
|
if (column.headerTitle) th.title = column.headerTitle;
|
|
841
1012
|
if (column.sortable) {
|
|
842
1013
|
const button = h('button', {
|
|
@@ -868,6 +1039,8 @@ export class Table extends Component {
|
|
|
868
1039
|
this._rowElements = new Map();
|
|
869
1040
|
this._rovingCell = null;
|
|
870
1041
|
this._rovingMatched = false;
|
|
1042
|
+
const projected = this._projectedData();
|
|
1043
|
+
this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
|
|
871
1044
|
if (this._data.length === 0) {
|
|
872
1045
|
fragment.append(h('tr', { class: 'zx-table__empty-row' },
|
|
873
1046
|
h('td', {
|
|
@@ -877,14 +1050,16 @@ export class Table extends Component {
|
|
|
877
1050
|
));
|
|
878
1051
|
} else {
|
|
879
1052
|
// Indices stay absolute: a row's index is its place in the data, not in what is on screen.
|
|
880
|
-
this.
|
|
1053
|
+
this._visibleEntries().forEach((entry, visibleIndex) => {
|
|
1054
|
+
fragment.append(this._createRow(entry.row, entry.index, { ...entry, visibleIndex }));
|
|
1055
|
+
});
|
|
881
1056
|
}
|
|
882
1057
|
this._tbody.replaceChildren(fragment);
|
|
883
1058
|
/*
|
|
884
1059
|
* With `growing` on, the DOM holds a prefix of the data — so without this a screen reader
|
|
885
1060
|
* announces "row 3 of 10" for a table of ten thousand. The header counts as a row.
|
|
886
1061
|
*/
|
|
887
|
-
if (this._growStep) this._table.setAttribute('aria-rowcount', String(
|
|
1062
|
+
if (this._growStep) this._table.setAttribute('aria-rowcount', String(projected.length + 1));
|
|
888
1063
|
this._syncSelectAll();
|
|
889
1064
|
this._syncGrowing();
|
|
890
1065
|
if (this._stacked) this._applyStackedRoles(true);
|
|
@@ -905,16 +1080,25 @@ export class Table extends Component {
|
|
|
905
1080
|
return typeof empty === 'function' ? empty() : empty;
|
|
906
1081
|
}
|
|
907
1082
|
|
|
908
|
-
/**
|
|
909
|
-
|
|
1083
|
+
/**
|
|
1084
|
+
* @param {TableRow} row
|
|
1085
|
+
* @param {number} index
|
|
1086
|
+
* @param {TableHierarchyEntry & {visibleIndex: number}} tree
|
|
1087
|
+
* @returns {HTMLTableRowElement}
|
|
1088
|
+
*/
|
|
1089
|
+
_createRow(row, index, tree) {
|
|
910
1090
|
const id = this._idFor(row);
|
|
911
1091
|
const tr = /** @type {HTMLTableRowElement} */ (h('tr', { dataset: { row: '' } }));
|
|
912
|
-
// Only a prefix
|
|
913
|
-
if (this._growStep) tr.setAttribute('aria-rowindex', String(
|
|
1092
|
+
// Only a prefix is in the DOM while growing, so each visible row states where it really is.
|
|
1093
|
+
if (this._growStep) tr.setAttribute('aria-rowindex', String(tree.visibleIndex + 2));
|
|
914
1094
|
const rowClass = typeof this.options.rowClass === 'function' ? this.options.rowClass(row) : '';
|
|
915
1095
|
if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
|
|
916
1096
|
if (this.options.selectable !== false) tr.setAttribute('aria-selected', String(this._selected.has(id)));
|
|
917
|
-
this.
|
|
1097
|
+
if (this._hierarchy) {
|
|
1098
|
+
tr.setAttribute('aria-level', String(tree.depth + 1));
|
|
1099
|
+
if (tree.hasChildren) tr.setAttribute('aria-expanded', String(this._expanded.has(id)));
|
|
1100
|
+
}
|
|
1101
|
+
this._rowMeta.set(tr, { row, id, index, visibleIndex: tree.visibleIndex });
|
|
918
1102
|
const elements = this._rowElements.get(id) ?? [];
|
|
919
1103
|
elements.push(tr);
|
|
920
1104
|
this._rowElements.set(id, elements);
|
|
@@ -931,7 +1115,8 @@ export class Table extends Component {
|
|
|
931
1115
|
|
|
932
1116
|
this._columns.forEach((column) => {
|
|
933
1117
|
const cell = /** @type {HTMLTableCellElement} */ (h('td'));
|
|
934
|
-
|
|
1118
|
+
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1119
|
+
if (align) cell.dataset.align = align;
|
|
935
1120
|
// Written unconditionally: `responsive` can be switched on after the rows exist, and a
|
|
936
1121
|
// stacked cell has no column header above it to say what its value means.
|
|
937
1122
|
cell.dataset.label = column.label;
|
|
@@ -944,7 +1129,7 @@ export class Table extends Component {
|
|
|
944
1129
|
this._offerRovingCell(cell, id, column.id);
|
|
945
1130
|
}
|
|
946
1131
|
}
|
|
947
|
-
this._renderCellContent(cell, column, row, index);
|
|
1132
|
+
this._renderCellContent(cell, column, row, index, tree);
|
|
948
1133
|
tr.append(cell);
|
|
949
1134
|
});
|
|
950
1135
|
return tr;
|
|
@@ -956,13 +1141,35 @@ export class Table extends Component {
|
|
|
956
1141
|
* @param {TableColumn} column Column definition.
|
|
957
1142
|
* @param {TableRow} row Row data.
|
|
958
1143
|
* @param {number} index Row index.
|
|
1144
|
+
* @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
|
|
959
1145
|
* @returns {void}
|
|
960
1146
|
*/
|
|
961
|
-
_renderCellContent(cell, column, row, index) {
|
|
962
|
-
const value =
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
else
|
|
1147
|
+
_renderCellContent(cell, column, row, index, tree = this._hierarchyMeta?.get(this._idFor(row))) {
|
|
1148
|
+
const value = formatTableCell(column, row, index);
|
|
1149
|
+
const content = h('span', { class: 'zx-table__cell-content' });
|
|
1150
|
+
if (value && typeof value === 'object' && typeof value.nodeType === 'number') content.append(value);
|
|
1151
|
+
else if (value != null) content.append(document.createTextNode(String(value)));
|
|
1152
|
+
|
|
1153
|
+
if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
|
|
1154
|
+
cell.replaceChildren(...content.childNodes);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const branch = h('span', { class: 'zx-table__tree-cell' });
|
|
1159
|
+
branch.style.setProperty('--zx-table-depth', String(tree.depth));
|
|
1160
|
+
if (tree.hasChildren) {
|
|
1161
|
+
const expanded = this._expanded.has(tree.id);
|
|
1162
|
+
branch.append(h('button', {
|
|
1163
|
+
class: 'zx-table__tree-toggle',
|
|
1164
|
+
type: 'button',
|
|
1165
|
+
ariaExpanded: String(expanded),
|
|
1166
|
+
ariaLabel: `${expanded ? 'Collapse' : 'Expand'} row ${index + 1}`
|
|
1167
|
+
}, icon(expanded ? 'chevron-down' : 'chevron-right', { size: 14 })));
|
|
1168
|
+
} else {
|
|
1169
|
+
branch.append(h('span', { class: 'zx-table__tree-spacer', ariaHidden: 'true' }));
|
|
1170
|
+
}
|
|
1171
|
+
branch.append(content);
|
|
1172
|
+
cell.replaceChildren(branch);
|
|
966
1173
|
}
|
|
967
1174
|
|
|
968
1175
|
/** @param {MouseEvent} event @returns {void} */
|
|
@@ -975,7 +1182,17 @@ export class Table extends Component {
|
|
|
975
1182
|
const meta = this._rowMeta.get(rowElement);
|
|
976
1183
|
if (!meta) return;
|
|
977
1184
|
// Read the row metadata first: committing re-renders the body and detaches `rowElement`.
|
|
978
|
-
if (this._edit)
|
|
1185
|
+
if (this._edit) {
|
|
1186
|
+
this.commitEdit();
|
|
1187
|
+
// Validation or a preventDefault() persistence guard keeps the editor authoritative.
|
|
1188
|
+
if (this._edit) return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const disclosure = target.closest('.zx-table__tree-toggle');
|
|
1192
|
+
if (disclosure) {
|
|
1193
|
+
this.toggleRow(meta.id);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
979
1196
|
|
|
980
1197
|
const checkbox = target.closest('.zx-table__row-checkbox');
|
|
981
1198
|
if (checkbox && this.options.selectable === 'multi') {
|
|
@@ -1075,7 +1292,19 @@ export class Table extends Component {
|
|
|
1075
1292
|
return entry;
|
|
1076
1293
|
}
|
|
1077
1294
|
if (type === 'number') {
|
|
1078
|
-
const
|
|
1295
|
+
const locale = resolveColumnOption(column.locale, row);
|
|
1296
|
+
const decimals = resolveColumnDecimals(column, row);
|
|
1297
|
+
const suffix = column.type === 'currency'
|
|
1298
|
+
? resolveColumnOption(column.currency, row)
|
|
1299
|
+
: column.type === 'unit' ? resolveColumnOption(column.unit, row) : null;
|
|
1300
|
+
const inferred = {
|
|
1301
|
+
...(locale != null && !Object.prototype.hasOwnProperty.call(props, 'locale') ? { locale } : {}),
|
|
1302
|
+
...(decimals != null && !Object.prototype.hasOwnProperty.call(props, 'precision')
|
|
1303
|
+
? { precision: decimals }
|
|
1304
|
+
: {}),
|
|
1305
|
+
...(suffix != null && !Object.prototype.hasOwnProperty.call(props, 'unit') ? { unit: String(suffix) } : {})
|
|
1306
|
+
};
|
|
1307
|
+
const field = new NumberField(null, { ...inferred, ...props, value: value ?? null });
|
|
1079
1308
|
host.append(field.el);
|
|
1080
1309
|
entry.component = field;
|
|
1081
1310
|
entry.control = field.getInput();
|
|
@@ -1316,9 +1545,12 @@ export class Table extends Component {
|
|
|
1316
1545
|
_nextEditableCell(rowIndex, columnId, direction) {
|
|
1317
1546
|
const columnCount = this._columns.length;
|
|
1318
1547
|
if (rowIndex < 0 || columnCount === 0) return null;
|
|
1319
|
-
|
|
1548
|
+
const visible = this._visibleData();
|
|
1549
|
+
const startingId = this._idFor(this._data[rowIndex]);
|
|
1550
|
+
let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
|
|
1551
|
+
if (row < 0) return null;
|
|
1320
1552
|
let column = this._columns.findIndex((candidate) => candidate.id === columnId);
|
|
1321
|
-
const limit =
|
|
1553
|
+
const limit = visible.length * columnCount + columnCount;
|
|
1322
1554
|
for (let step = 0; step < limit; step += 1) {
|
|
1323
1555
|
column += direction;
|
|
1324
1556
|
if (column >= columnCount) {
|
|
@@ -1328,11 +1560,15 @@ export class Table extends Component {
|
|
|
1328
1560
|
row -= 1;
|
|
1329
1561
|
column = columnCount - 1;
|
|
1330
1562
|
}
|
|
1331
|
-
if (row < 0 || row >=
|
|
1332
|
-
const candidateRow =
|
|
1563
|
+
if (row < 0 || row >= visible.length) return null;
|
|
1564
|
+
const candidateRow = visible[row];
|
|
1333
1565
|
const candidateColumn = this._columns[column];
|
|
1334
1566
|
if (resolveEditable(candidateColumn, candidateRow) !== false) {
|
|
1335
|
-
return {
|
|
1567
|
+
return {
|
|
1568
|
+
id: this._idFor(candidateRow),
|
|
1569
|
+
columnId: candidateColumn.id,
|
|
1570
|
+
index: this._data.indexOf(candidateRow)
|
|
1571
|
+
};
|
|
1336
1572
|
}
|
|
1337
1573
|
}
|
|
1338
1574
|
return null;
|
|
@@ -1397,14 +1633,16 @@ export class Table extends Component {
|
|
|
1397
1633
|
return message === key ? fallback : message;
|
|
1398
1634
|
}
|
|
1399
1635
|
|
|
1400
|
-
/** @param {{row: TableRow, id: unknown, index: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
|
|
1636
|
+
/** @param {{row: TableRow, id: unknown, index: number, visibleIndex?: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
|
|
1401
1637
|
_toggleMultiSelection(meta, checked, shiftKey) {
|
|
1402
|
-
const
|
|
1638
|
+
const visible = this._visibleData();
|
|
1639
|
+
const anchorIndex = visible.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
|
|
1403
1640
|
if (shiftKey && anchorIndex >= 0) {
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
1641
|
+
const currentIndex = meta.visibleIndex ?? visible.findIndex((row) => Object.is(this._idFor(row), meta.id));
|
|
1642
|
+
const start = Math.min(anchorIndex, currentIndex);
|
|
1643
|
+
const end = Math.max(anchorIndex, currentIndex);
|
|
1406
1644
|
for (let index = start; index <= end; index += 1) {
|
|
1407
|
-
const id = this._idFor(
|
|
1645
|
+
const id = this._idFor(visible[index]);
|
|
1408
1646
|
if (checked) this._selected.add(id);
|
|
1409
1647
|
else this._selected.delete(id);
|
|
1410
1648
|
}
|
|
@@ -1460,7 +1698,39 @@ export class Table extends Component {
|
|
|
1460
1698
|
const column = this._columns.find((candidate) => candidate.id === this._sort.id);
|
|
1461
1699
|
if (!column) return;
|
|
1462
1700
|
const getValue = typeof column.sortValue === 'function' ? column.sortValue : (row) => row?.[column.id];
|
|
1463
|
-
this._data =
|
|
1701
|
+
this._data = this._hierarchy
|
|
1702
|
+
? sortHierarchyRows(this._data, this.options.rowId, this._hierarchy.parentId, getValue, this._sort.dir)
|
|
1703
|
+
: sortRows(this._data, getValue, this._sort.dir);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/** @param {unknown} id @param {boolean} expanded @param {{silent?: boolean}} options @returns {this} */
|
|
1707
|
+
_setRowExpanded(id, expanded, options) {
|
|
1708
|
+
if (!this._hierarchy) return this;
|
|
1709
|
+
const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
1710
|
+
if (!branchIds.some((candidate) => Object.is(candidate, id))) return this;
|
|
1711
|
+
if (this._expanded.has(id) === expanded) return this;
|
|
1712
|
+
if (expanded) this._expanded.add(id);
|
|
1713
|
+
else this._expanded.delete(id);
|
|
1714
|
+
this._renderBody();
|
|
1715
|
+
if (!options.silent) this._emitRowToggle(id, expanded);
|
|
1716
|
+
return this;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/** @param {unknown} id @param {boolean} expanded @returns {void} */
|
|
1720
|
+
_emitRowToggle(id, expanded) {
|
|
1721
|
+
const row = this.getRow(id);
|
|
1722
|
+
if (row) this.emit('rowtoggle', { row, id, expanded });
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/** Keeps the reader's current expansion state limited to branches still present in the data. */
|
|
1726
|
+
_reconcileExpanded() {
|
|
1727
|
+
if (!this._hierarchy) {
|
|
1728
|
+
this._expanded.clear();
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const branches = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
1732
|
+
const valid = new Set(branches);
|
|
1733
|
+
for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
|
|
1464
1734
|
}
|
|
1465
1735
|
|
|
1466
1736
|
/** @returns {boolean} Whether selection changed. */
|
|
@@ -1504,6 +1774,9 @@ export class Table extends Component {
|
|
|
1504
1774
|
/** @event Table#sort @type {CustomEvent<TableSortDetail>} */
|
|
1505
1775
|
/** @event Table#selectionchange @type {CustomEvent<TableSelectionDetail>} */
|
|
1506
1776
|
/** @event Table#datachange @type {CustomEvent<TableDataDetail>} */
|
|
1777
|
+
/** @event Table#rowtoggle @type {CustomEvent<TableRowToggleDetail>} */
|
|
1778
|
+
/** @event Table#stackedchange @type {CustomEvent<{stacked: boolean}>} */
|
|
1779
|
+
/** @event Table#grow @type {CustomEvent<{rendered: number, total: number}>} */
|
|
1507
1780
|
|
|
1508
1781
|
/**
|
|
1509
1782
|
* Editor opened for a cell (or, in row mode, for the row).
|
|
@@ -1531,12 +1804,104 @@ export class Table extends Component {
|
|
|
1531
1804
|
* @type {CustomEvent<TableEditInvalidDetail>}
|
|
1532
1805
|
*/
|
|
1533
1806
|
|
|
1807
|
+
/**
|
|
1808
|
+
* Applies a column's built-in display type. A custom renderer always wins and may still return a
|
|
1809
|
+
* Node; all built-in results are strings so `_renderCellContent` can insert them as text.
|
|
1810
|
+
* @param {TableColumn} column Column definition.
|
|
1811
|
+
* @param {TableRow} row Source row.
|
|
1812
|
+
* @param {number} [index=0] Flat row index.
|
|
1813
|
+
* @returns {Node|string|number|null|undefined} Rendered cell value.
|
|
1814
|
+
*/
|
|
1815
|
+
export function formatTableCell(column, row, index = 0) {
|
|
1816
|
+
if (typeof column?.render === 'function') return column.render(row, index);
|
|
1817
|
+
const value = row?.[column?.id];
|
|
1818
|
+
const type = column?.type ?? 'text';
|
|
1819
|
+
if (type === 'text') return value;
|
|
1820
|
+
|
|
1821
|
+
const locale = resolveColumnOption(column.locale, row);
|
|
1822
|
+
const decimals = resolveColumnDecimals(column, row);
|
|
1823
|
+
const options = {
|
|
1824
|
+
...(locale != null ? { locale: String(locale) } : {}),
|
|
1825
|
+
...(decimals != null ? { decimals } : {})
|
|
1826
|
+
};
|
|
1827
|
+
if (type === 'number') return formatNumber(value, options);
|
|
1828
|
+
if (type === 'currency') {
|
|
1829
|
+
const currency = resolveColumnOption(column.currency, row);
|
|
1830
|
+
return formatCurrency(value, currency == null ? null : String(currency), options);
|
|
1831
|
+
}
|
|
1832
|
+
if (type === 'percent') return formatPercent(value, options);
|
|
1833
|
+
if (type === 'unit') {
|
|
1834
|
+
const number = formatNumber(value, options);
|
|
1835
|
+
const unit = resolveColumnOption(column.unit, row);
|
|
1836
|
+
return number && unit != null && String(unit) !== '' ? `${number}\u00a0${unit}` : number;
|
|
1837
|
+
}
|
|
1838
|
+
return value;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Projects flat parent references into the currently visible stable depth-first order. Orphans
|
|
1843
|
+
* become roots. A cycle is broken at its first source row, so every row is emitted once and the
|
|
1844
|
+
* traversal can never recurse forever.
|
|
1845
|
+
* @param {TableRow[]} rows Flat source rows.
|
|
1846
|
+
* @param {string|((row: TableRow) => unknown)} rowId Row id accessor.
|
|
1847
|
+
* @param {string|((row: TableRow) => unknown)} parentId Parent id accessor.
|
|
1848
|
+
* @param {Set<unknown>|unknown[]} [expanded=[]] Expanded branch ids.
|
|
1849
|
+
* @returns {TableHierarchyEntry[]} Visible hierarchy entries.
|
|
1850
|
+
*/
|
|
1851
|
+
export function projectHierarchyRows(rows, rowId, parentId, expanded = []) {
|
|
1852
|
+
const forest = hierarchyForest(rows, rowId, parentId);
|
|
1853
|
+
const open = expanded instanceof Set ? expanded : new Set(expanded);
|
|
1854
|
+
/** @type {TableHierarchyEntry[]} */
|
|
1855
|
+
const result = [];
|
|
1856
|
+
const stack = forest.roots.slice().reverse().map((node) => ({ node, depth: 0 }));
|
|
1857
|
+
while (stack.length) {
|
|
1858
|
+
const { node, depth } = stack.pop();
|
|
1859
|
+
result.push({
|
|
1860
|
+
row: node.row,
|
|
1861
|
+
id: node.id,
|
|
1862
|
+
index: node.index,
|
|
1863
|
+
depth,
|
|
1864
|
+
hasChildren: node.children.length > 0,
|
|
1865
|
+
parentId: node.parent?.id ?? null
|
|
1866
|
+
});
|
|
1867
|
+
if (!open.has(node.id)) continue;
|
|
1868
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
1869
|
+
stack.push({ node: node.children[index], depth: depth + 1 });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
return result;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Sorts roots and each sibling set independently, returning all rows in depth-first order. Parent
|
|
1877
|
+
* and child rows never cross levels merely because their cell values compare differently.
|
|
1878
|
+
* @param {TableRow[]} rows Flat source rows.
|
|
1879
|
+
* @param {string|((row: TableRow) => unknown)} rowId Row id accessor.
|
|
1880
|
+
* @param {string|((row: TableRow) => unknown)} parentId Parent id accessor.
|
|
1881
|
+
* @param {(row: TableRow) => unknown} getValue Sort accessor.
|
|
1882
|
+
* @param {TableSortDirection} direction Sort direction.
|
|
1883
|
+
* @returns {TableRow[]} Sorted flat rows.
|
|
1884
|
+
*/
|
|
1885
|
+
export function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
|
|
1886
|
+
const forest = hierarchyForest(rows, rowId, parentId);
|
|
1887
|
+
const sorted = (nodes) => sortRows(nodes, (node) => getValue(node.row), direction);
|
|
1888
|
+
const result = [];
|
|
1889
|
+
const stack = sorted(forest.roots).reverse();
|
|
1890
|
+
while (stack.length) {
|
|
1891
|
+
const node = stack.pop();
|
|
1892
|
+
result.push(node.row);
|
|
1893
|
+
const children = sorted(node.children);
|
|
1894
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
1895
|
+
}
|
|
1896
|
+
return result;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1534
1899
|
/**
|
|
1535
1900
|
* Resolves the editor type of a column for one row.
|
|
1536
1901
|
*
|
|
1537
1902
|
* `column.editor` wins over `column.editable`, except that a per-row `editable` function returning
|
|
1538
|
-
* `false` still marks the cell read-only. `editable: true`
|
|
1539
|
-
* treated as not editable.
|
|
1903
|
+
* `false` still marks the cell read-only. `editable: true` infers `'number'` for number, currency,
|
|
1904
|
+
* percent, and unit columns and `'text'` otherwise; unknown type names are treated as not editable.
|
|
1540
1905
|
* @param {TableColumn} column Column definition.
|
|
1541
1906
|
* @param {TableRow} row Row the cell belongs to.
|
|
1542
1907
|
* @returns {TableEditorType|false} Editor type, or false when the cell is read-only.
|
|
@@ -1548,7 +1913,7 @@ export function resolveEditable(column, row) {
|
|
|
1548
1913
|
return 'custom';
|
|
1549
1914
|
}
|
|
1550
1915
|
const declared = typeof column.editable === 'function' ? column.editable(row) : column.editable;
|
|
1551
|
-
if (declared === true) return 'text';
|
|
1916
|
+
if (declared === true) return isNumericColumn(column) ? 'number' : 'text';
|
|
1552
1917
|
if (typeof declared === 'string' && EDITOR_TYPES.has(declared)) return /** @type {TableEditorType} */ (declared);
|
|
1553
1918
|
return false;
|
|
1554
1919
|
}
|
|
@@ -1647,6 +2012,120 @@ export function firstValidationError(values, columns, row, fallbackMessage = 'In
|
|
|
1647
2012
|
return null;
|
|
1648
2013
|
}
|
|
1649
2014
|
|
|
2015
|
+
/** @param {unknown} option @param {TableRow} row @returns {unknown} */
|
|
2016
|
+
function resolveColumnOption(option, row) {
|
|
2017
|
+
return typeof option === 'function' ? option(row) : option;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Normalizes an explicit fraction-digit value while preserving a nullish callback result as
|
|
2022
|
+
* "unspecified" so Intl and NumberField can use their normal currency/value defaults.
|
|
2023
|
+
* @param {TableColumn} column
|
|
2024
|
+
* @param {TableRow} row
|
|
2025
|
+
* @returns {number|null}
|
|
2026
|
+
*/
|
|
2027
|
+
function resolveColumnDecimals(column, row) {
|
|
2028
|
+
const value = resolveColumnOption(column?.decimals, row);
|
|
2029
|
+
if (value == null) return null;
|
|
2030
|
+
const number = Number(value);
|
|
2031
|
+
return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : null;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/** @param {TableColumn} column @returns {boolean} */
|
|
2035
|
+
function isNumericColumn(column) {
|
|
2036
|
+
return ['number', 'currency', 'percent', 'unit'].includes(column?.type);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Builds a canonical forest without mutating source rows. The first row encountered in a cycle is
|
|
2041
|
+
* promoted to a root; source order therefore makes malformed data deterministic.
|
|
2042
|
+
* @param {TableRow[]} rows
|
|
2043
|
+
* @param {string|((row: TableRow) => unknown)} rowId
|
|
2044
|
+
* @param {string|((row: TableRow) => unknown)} parentId
|
|
2045
|
+
* @returns {{roots: TableHierarchyNode[], nodes: TableHierarchyNode[]}}
|
|
2046
|
+
*/
|
|
2047
|
+
function hierarchyForest(rows, rowId, parentId) {
|
|
2048
|
+
/** @type {TableHierarchyNode[]} */
|
|
2049
|
+
const nodes = (Array.isArray(rows) ? rows : []).map((row, index) => ({
|
|
2050
|
+
row,
|
|
2051
|
+
id: readRowValue(rowId, row),
|
|
2052
|
+
index,
|
|
2053
|
+
parent: null,
|
|
2054
|
+
children: []
|
|
2055
|
+
}));
|
|
2056
|
+
const byId = new Map();
|
|
2057
|
+
for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
|
|
2058
|
+
|
|
2059
|
+
/** @type {Map<TableHierarchyNode, TableHierarchyNode|null>} */
|
|
2060
|
+
const parentOf = new Map();
|
|
2061
|
+
for (const node of nodes) {
|
|
2062
|
+
const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
|
|
2063
|
+
parentOf.set(node, parent === node ? null : parent);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// Parent references form a functional graph. Breaking the repeated node's edge turns every
|
|
2067
|
+
// connected component into a finite tree while keeping all non-cyclic parent links intact.
|
|
2068
|
+
for (const node of nodes) {
|
|
2069
|
+
const seen = new Set([node]);
|
|
2070
|
+
let cursor = parentOf.get(node) ?? null;
|
|
2071
|
+
while (cursor) {
|
|
2072
|
+
if (seen.has(cursor)) {
|
|
2073
|
+
parentOf.set(cursor, null);
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
seen.add(cursor);
|
|
2077
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const roots = [];
|
|
2082
|
+
for (const node of nodes) {
|
|
2083
|
+
node.parent = parentOf.get(node) ?? null;
|
|
2084
|
+
if (node.parent) node.parent.children.push(node);
|
|
2085
|
+
else roots.push(node);
|
|
2086
|
+
}
|
|
2087
|
+
return { roots, nodes };
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* @param {TableRow[]} rows
|
|
2092
|
+
* @param {string|((row: TableRow) => unknown)} rowId
|
|
2093
|
+
* @param {string|((row: TableRow) => unknown)} parentId
|
|
2094
|
+
* @returns {unknown[]}
|
|
2095
|
+
*/
|
|
2096
|
+
function hierarchyBranchIds(rows, rowId, parentId) {
|
|
2097
|
+
return hierarchyForest(rows, rowId, parentId).nodes
|
|
2098
|
+
.filter((node) => node.children.length > 0)
|
|
2099
|
+
.map((node) => node.id);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/** @param {string|((row: TableRow) => unknown)} accessor @param {TableRow} row @returns {unknown} */
|
|
2103
|
+
function readRowValue(accessor, row) {
|
|
2104
|
+
return typeof accessor === 'function' ? accessor(row) : row?.[accessor];
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* @param {unknown} option
|
|
2109
|
+
* @param {TableColumn[]} columns
|
|
2110
|
+
* @returns {(TableHierarchyOptions & {column: string})|null}
|
|
2111
|
+
*/
|
|
2112
|
+
function normalizeHierarchy(option, columns) {
|
|
2113
|
+
if (!option || typeof option !== 'object') return null;
|
|
2114
|
+
const parentId = option.parentId;
|
|
2115
|
+
if (typeof parentId !== 'string' && typeof parentId !== 'function') return null;
|
|
2116
|
+
const requested = typeof option.column === 'string' ? option.column : null;
|
|
2117
|
+
const column = columns.some((candidate) => candidate.id === requested)
|
|
2118
|
+
? requested
|
|
2119
|
+
: columns[0]?.id;
|
|
2120
|
+
if (!column) return null;
|
|
2121
|
+
return { ...option, parentId, column };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
/** @param {unknown} option @returns {Set<unknown>} */
|
|
2125
|
+
function initialExpanded(option) {
|
|
2126
|
+
return new Set(Array.isArray(option) ? option : []);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
1650
2129
|
/** @param {unknown} mode @returns {TableEditMode} */
|
|
1651
2130
|
function normalizeEditMode(mode) {
|
|
1652
2131
|
if (mode === 'cell' || mode === 'row') return mode;
|