@zeyos/zx 2.2.1 → 3.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 +197 -0
- package/DESIGN-SYSTEM.md +136 -0
- package/MIGRATION.md +155 -4
- package/README.md +51 -14
- package/dist/types/compat/map/data.d.ts +3 -2
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-rail/app-rail.d.ts +250 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/chart/chart.d.ts +321 -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 +300 -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 +74 -9
- 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 +36 -1
- 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/tabbox/tabbox.d.ts +12 -3
- package/dist/types/components/table/table.d.ts +219 -12
- 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 +114 -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 +12 -2
- package/dist/zx-compat.esm.js +943 -311
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +7893 -2252
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +826 -114
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +1720 -243
- package/dist/zx.esm.js +8227 -2599
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8227 -2599
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +2597 -255
- package/docs/llms.md +475 -37
- package/docs/llms.txt +21 -9
- package/package.json +4 -2
- package/src/compat/globals.js +3 -1
- package/src/compat/map/data.js +31 -9
- 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-rail/app-rail.js +668 -0
- package/src/components/app-sidebar/app-sidebar.css +68 -0
- package/src/components/app-sidebar/app-sidebar.js +388 -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/chart/chart.css +61 -0
- package/src/components/chart/chart.js +518 -0
- package/src/components/date-picker/date-range-picker.css +7 -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/fieldset/fieldset.css +7 -0
- package/src/components/form/form.css +7 -0
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +213 -0
- package/src/components/launcher/launcher.css +100 -0
- package/src/components/launcher/launcher.js +557 -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.css +99 -15
- package/src/components/multi-value-editor/multi-value-editor.js +237 -19
- package/src/components/navigation-bar/navigation-bar.css +7 -0
- 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/search/search.css +10 -2
- package/src/components/select/select.css +6 -1
- package/src/components/select/select.js +99 -11
- 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/stepper/stepper.css +12 -2
- package/src/components/tabbox/tabbox.css +13 -3
- package/src/components/tabbox/tabbox.js +34 -10
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +523 -45
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +302 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +19 -2
- package/styles/tokens/dark.css +15 -13
- package/styles/tokens/global.css +17 -5
- package/styles/tokens/semantic.css +14 -8
- package/styles/tokens/themes.css +72 -0
- package/styles/zx.css +11 -1
- package/dist/types/components/permission/permission.d.ts +0 -93
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -198
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { breakpoints, onBreakpoint } from '../../core/breakpoint.js';
|
|
3
3
|
import { h } 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
|
/**
|
|
@@ -239,10 +287,19 @@ export class Table extends Component {
|
|
|
239
287
|
this._rovingKey = null;
|
|
240
288
|
this._rovingCell = null;
|
|
241
289
|
this._rovingMatched = false;
|
|
290
|
+
this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
|
|
291
|
+
this._expanded = initialExpanded(this._hierarchy?.expanded);
|
|
242
292
|
|
|
243
293
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
294
|
+
if (this._hierarchy?.expanded === true) {
|
|
295
|
+
for (const id of hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId)) {
|
|
296
|
+
this._expanded.add(id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this._reconcileExpanded();
|
|
244
300
|
|
|
245
301
|
this._table = h('table', { class: 'zx-table__table' });
|
|
302
|
+
if (this._hierarchy) this._table.setAttribute('role', 'treegrid');
|
|
246
303
|
this._colgroup = h('colgroup', { class: 'zx-table__columns' });
|
|
247
304
|
this._thead = h('thead', { class: 'zx-table__head' });
|
|
248
305
|
this._tbody = h('tbody', { class: 'zx-table__body' });
|
|
@@ -316,6 +373,7 @@ export class Table extends Component {
|
|
|
316
373
|
assertRows(rows);
|
|
317
374
|
this._data = [...rows];
|
|
318
375
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
376
|
+
this._reconcileExpanded();
|
|
319
377
|
const selectionChanged = this._pruneSelection();
|
|
320
378
|
this._renderBody();
|
|
321
379
|
if (selectionChanged) this._emitSelectionChange();
|
|
@@ -334,6 +392,7 @@ export class Table extends Component {
|
|
|
334
392
|
assertRows(rows);
|
|
335
393
|
this._data.push(...rows);
|
|
336
394
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
395
|
+
this._reconcileExpanded();
|
|
337
396
|
this._renderBody();
|
|
338
397
|
this._emitDataChange();
|
|
339
398
|
return this;
|
|
@@ -354,6 +413,7 @@ export class Table extends Component {
|
|
|
354
413
|
this._data[index] = row;
|
|
355
414
|
if (wasSelected) this._selected.add(this._idFor(row));
|
|
356
415
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
416
|
+
this._reconcileExpanded();
|
|
357
417
|
this._renderBody();
|
|
358
418
|
if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
|
|
359
419
|
this._emitDataChange();
|
|
@@ -372,6 +432,7 @@ export class Table extends Component {
|
|
|
372
432
|
const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
|
|
373
433
|
if (index < 0) return this;
|
|
374
434
|
this._data.splice(index, 1);
|
|
435
|
+
this._reconcileExpanded();
|
|
375
436
|
const selectionChanged = this._selected.delete(id);
|
|
376
437
|
if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
|
|
377
438
|
this._renderBody();
|
|
@@ -389,7 +450,10 @@ export class Table extends Component {
|
|
|
389
450
|
return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
|
|
390
451
|
}
|
|
391
452
|
|
|
392
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* Returns a shallow copy of the current rows in display order.
|
|
455
|
+
* @returns {TableRow[]} Current table rows.
|
|
456
|
+
*/
|
|
393
457
|
getData() {
|
|
394
458
|
return [...this._data];
|
|
395
459
|
}
|
|
@@ -404,6 +468,7 @@ export class Table extends Component {
|
|
|
404
468
|
empty() {
|
|
405
469
|
const selectionChanged = this._selected.size > 0;
|
|
406
470
|
this._data = [];
|
|
471
|
+
this._expanded.clear();
|
|
407
472
|
this._selected.clear();
|
|
408
473
|
this._selectionAnchorId = null;
|
|
409
474
|
this._renderBody();
|
|
@@ -442,7 +507,10 @@ export class Table extends Component {
|
|
|
442
507
|
return this;
|
|
443
508
|
}
|
|
444
509
|
|
|
445
|
-
/**
|
|
510
|
+
/**
|
|
511
|
+
* Returns selected rows in current display order.
|
|
512
|
+
* @returns {TableRow[]} Selected rows.
|
|
513
|
+
*/
|
|
446
514
|
getSelection() {
|
|
447
515
|
return this._data.filter((row) => this._selected.has(this._idFor(row)));
|
|
448
516
|
}
|
|
@@ -484,6 +552,81 @@ export class Table extends Component {
|
|
|
484
552
|
return this;
|
|
485
553
|
}
|
|
486
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Toggles one hierarchy branch. A leaf or an id absent from the table is a no-op.
|
|
557
|
+
* @param {unknown} id Row id.
|
|
558
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
559
|
+
* @returns {this}
|
|
560
|
+
* @fires Table#rowtoggle
|
|
561
|
+
*/
|
|
562
|
+
toggleRow(id, options = {}) {
|
|
563
|
+
return this._expanded.has(id) ? this.collapseRow(id, options) : this.expandRow(id, options);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Expands one hierarchy branch.
|
|
568
|
+
* @param {unknown} id Row id.
|
|
569
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
570
|
+
* @returns {this}
|
|
571
|
+
* @fires Table#rowtoggle
|
|
572
|
+
*/
|
|
573
|
+
expandRow(id, options = {}) {
|
|
574
|
+
return this._setRowExpanded(id, true, options);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Collapses one hierarchy branch without discarding its descendants or their expansion state.
|
|
579
|
+
* @param {unknown} id Row id.
|
|
580
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
581
|
+
* @returns {this}
|
|
582
|
+
* @fires Table#rowtoggle
|
|
583
|
+
*/
|
|
584
|
+
collapseRow(id, options = {}) {
|
|
585
|
+
return this._setRowExpanded(id, false, options);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Expands every row that currently has children.
|
|
590
|
+
* @param {{silent?: boolean}} [options={}] Event behavior. When not silent, one `rowtoggle`
|
|
591
|
+
* event is emitted per branch whose state changed.
|
|
592
|
+
* @returns {this}
|
|
593
|
+
*/
|
|
594
|
+
expandAll(options = {}) {
|
|
595
|
+
if (!this._hierarchy) return this;
|
|
596
|
+
const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
597
|
+
const changed = branchIds.filter((id) => !this._expanded.has(id));
|
|
598
|
+
for (const id of changed) this._expanded.add(id);
|
|
599
|
+
if (changed.length) this._renderBody();
|
|
600
|
+
if (!options.silent) {
|
|
601
|
+
for (const id of changed) this._emitRowToggle(id, true);
|
|
602
|
+
}
|
|
603
|
+
return this;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Collapses every hierarchy branch.
|
|
608
|
+
* @param {{silent?: boolean}} [options={}] Event behavior.
|
|
609
|
+
* @returns {this}
|
|
610
|
+
*/
|
|
611
|
+
collapseAll(options = {}) {
|
|
612
|
+
if (!this._hierarchy || this._expanded.size === 0) return this;
|
|
613
|
+
const changed = [...this._expanded];
|
|
614
|
+
this._expanded.clear();
|
|
615
|
+
this._renderBody();
|
|
616
|
+
if (!options.silent) {
|
|
617
|
+
for (const id of changed) this._emitRowToggle(id, false);
|
|
618
|
+
}
|
|
619
|
+
return this;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Returns expanded branch ids in current flat-data order.
|
|
624
|
+
* @returns {unknown[]} Expanded branch ids.
|
|
625
|
+
*/
|
|
626
|
+
getExpanded() {
|
|
627
|
+
return this._data.map((row) => this._idFor(row)).filter((id) => this._expanded.has(id));
|
|
628
|
+
}
|
|
629
|
+
|
|
487
630
|
/**
|
|
488
631
|
* Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
|
|
489
632
|
* resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
|
|
@@ -607,12 +750,18 @@ export class Table extends Component {
|
|
|
607
750
|
return this;
|
|
608
751
|
}
|
|
609
752
|
|
|
610
|
-
/**
|
|
753
|
+
/**
|
|
754
|
+
* Reports whether a cell or row is currently open for editing.
|
|
755
|
+
* @returns {boolean} Whether an edit is open.
|
|
756
|
+
*/
|
|
611
757
|
isEditing() {
|
|
612
758
|
return this._edit !== null;
|
|
613
759
|
}
|
|
614
760
|
|
|
615
|
-
/**
|
|
761
|
+
/**
|
|
762
|
+
* Returns the row id and focused column id for the open edit.
|
|
763
|
+
* @returns {{id: unknown, columnId: string}|null} Editing location, or null.
|
|
764
|
+
*/
|
|
616
765
|
getEditing() {
|
|
617
766
|
return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
|
|
618
767
|
}
|
|
@@ -622,7 +771,25 @@ export class Table extends Component {
|
|
|
622
771
|
* @returns {TableRow[]}
|
|
623
772
|
*/
|
|
624
773
|
_visibleData() {
|
|
625
|
-
return this.
|
|
774
|
+
return this._visibleEntries().map((entry) => entry.row);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** @returns {TableHierarchyEntry[]} Rows visible after hierarchy expansion, before growing. */
|
|
778
|
+
_projectedData() {
|
|
779
|
+
if (!this._hierarchy) {
|
|
780
|
+
return this._data.map((row, index) => ({
|
|
781
|
+
row, id: this._idFor(row), index, depth: 0, hasChildren: false, parentId: null
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
return projectHierarchyRows(
|
|
785
|
+
this._data, this.options.rowId, this._hierarchy.parentId, this._expanded
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** @returns {TableHierarchyEntry[]} Rows that currently have DOM entries. */
|
|
790
|
+
_visibleEntries() {
|
|
791
|
+
const projected = this._projectedData();
|
|
792
|
+
return this._rendered === Infinity ? projected : projected.slice(0, this._rendered);
|
|
626
793
|
}
|
|
627
794
|
|
|
628
795
|
/**
|
|
@@ -635,10 +802,11 @@ export class Table extends Component {
|
|
|
635
802
|
if (!this._growStep) return this;
|
|
636
803
|
const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
|
|
637
804
|
const before = this._rendered;
|
|
638
|
-
|
|
805
|
+
const total = this._projectedData().length;
|
|
806
|
+
this._rendered = Math.min(total, this._rendered + step);
|
|
639
807
|
if (this._rendered === before) return this;
|
|
640
808
|
this._renderBody();
|
|
641
|
-
this.emit('grow', { rendered: this._rendered, total
|
|
809
|
+
this.emit('grow', { rendered: this._rendered, total });
|
|
642
810
|
return this;
|
|
643
811
|
}
|
|
644
812
|
|
|
@@ -648,7 +816,7 @@ export class Table extends Component {
|
|
|
648
816
|
* @fires Table#grow
|
|
649
817
|
*/
|
|
650
818
|
showAll() {
|
|
651
|
-
return this.growBy(this.
|
|
819
|
+
return this.growBy(this._projectedData().length);
|
|
652
820
|
}
|
|
653
821
|
|
|
654
822
|
/**
|
|
@@ -656,7 +824,7 @@ export class Table extends Component {
|
|
|
656
824
|
* @returns {number}
|
|
657
825
|
*/
|
|
658
826
|
getRenderedCount() {
|
|
659
|
-
return Math.min(this._rendered, this.
|
|
827
|
+
return Math.min(this._rendered, this._projectedData().length);
|
|
660
828
|
}
|
|
661
829
|
|
|
662
830
|
/**
|
|
@@ -669,7 +837,7 @@ export class Table extends Component {
|
|
|
669
837
|
*/
|
|
670
838
|
_syncGrowing() {
|
|
671
839
|
if (!this._growStep) return;
|
|
672
|
-
const remaining = this.
|
|
840
|
+
const remaining = this._projectedData().length - this.getRenderedCount();
|
|
673
841
|
if (remaining <= 0) {
|
|
674
842
|
this._more?.remove();
|
|
675
843
|
this._more = null;
|
|
@@ -746,10 +914,11 @@ export class Table extends Component {
|
|
|
746
914
|
*/
|
|
747
915
|
_applyStackedRoles(stacked) {
|
|
748
916
|
/** @type {Array<[Element|null, string]>} */
|
|
749
|
-
const parts = [[this._table, 'table'], [this._thead, 'rowgroup'], [this._tbody, 'rowgroup']];
|
|
917
|
+
const parts = [[this._table, this._hierarchy ? 'treegrid' : 'table'], [this._thead, 'rowgroup'], [this._tbody, 'rowgroup']];
|
|
750
918
|
for (const [element, role] of parts) {
|
|
751
919
|
if (!element) continue;
|
|
752
920
|
if (stacked) element.setAttribute('role', role);
|
|
921
|
+
else if (element === this._table && this._hierarchy) element.setAttribute('role', 'treegrid');
|
|
753
922
|
else element.removeAttribute('role');
|
|
754
923
|
}
|
|
755
924
|
for (const [selector, role] of [['tr', 'row'], ['td', 'cell'], ['th', 'columnheader']]) {
|
|
@@ -836,7 +1005,8 @@ export class Table extends Component {
|
|
|
836
1005
|
this._headers = new Map();
|
|
837
1006
|
for (const column of this._columns) {
|
|
838
1007
|
const th = h('th', { scope: 'col' });
|
|
839
|
-
|
|
1008
|
+
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1009
|
+
if (align) th.dataset.align = align;
|
|
840
1010
|
if (column.headerTitle) th.title = column.headerTitle;
|
|
841
1011
|
if (column.sortable) {
|
|
842
1012
|
const button = h('button', {
|
|
@@ -868,6 +1038,8 @@ export class Table extends Component {
|
|
|
868
1038
|
this._rowElements = new Map();
|
|
869
1039
|
this._rovingCell = null;
|
|
870
1040
|
this._rovingMatched = false;
|
|
1041
|
+
const projected = this._projectedData();
|
|
1042
|
+
this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
|
|
871
1043
|
if (this._data.length === 0) {
|
|
872
1044
|
fragment.append(h('tr', { class: 'zx-table__empty-row' },
|
|
873
1045
|
h('td', {
|
|
@@ -877,14 +1049,16 @@ export class Table extends Component {
|
|
|
877
1049
|
));
|
|
878
1050
|
} else {
|
|
879
1051
|
// Indices stay absolute: a row's index is its place in the data, not in what is on screen.
|
|
880
|
-
this.
|
|
1052
|
+
this._visibleEntries().forEach((entry, visibleIndex) => {
|
|
1053
|
+
fragment.append(this._createRow(entry.row, entry.index, { ...entry, visibleIndex }));
|
|
1054
|
+
});
|
|
881
1055
|
}
|
|
882
1056
|
this._tbody.replaceChildren(fragment);
|
|
883
1057
|
/*
|
|
884
1058
|
* With `growing` on, the DOM holds a prefix of the data — so without this a screen reader
|
|
885
1059
|
* announces "row 3 of 10" for a table of ten thousand. The header counts as a row.
|
|
886
1060
|
*/
|
|
887
|
-
if (this._growStep) this._table.setAttribute('aria-rowcount', String(
|
|
1061
|
+
if (this._growStep) this._table.setAttribute('aria-rowcount', String(projected.length + 1));
|
|
888
1062
|
this._syncSelectAll();
|
|
889
1063
|
this._syncGrowing();
|
|
890
1064
|
if (this._stacked) this._applyStackedRoles(true);
|
|
@@ -905,16 +1079,25 @@ export class Table extends Component {
|
|
|
905
1079
|
return typeof empty === 'function' ? empty() : empty;
|
|
906
1080
|
}
|
|
907
1081
|
|
|
908
|
-
/**
|
|
909
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* @param {TableRow} row
|
|
1084
|
+
* @param {number} index
|
|
1085
|
+
* @param {TableHierarchyEntry & {visibleIndex: number}} tree
|
|
1086
|
+
* @returns {HTMLTableRowElement}
|
|
1087
|
+
*/
|
|
1088
|
+
_createRow(row, index, tree) {
|
|
910
1089
|
const id = this._idFor(row);
|
|
911
1090
|
const tr = /** @type {HTMLTableRowElement} */ (h('tr', { dataset: { row: '' } }));
|
|
912
|
-
// Only a prefix
|
|
913
|
-
if (this._growStep) tr.setAttribute('aria-rowindex', String(
|
|
1091
|
+
// Only a prefix is in the DOM while growing, so each visible row states where it really is.
|
|
1092
|
+
if (this._growStep) tr.setAttribute('aria-rowindex', String(tree.visibleIndex + 2));
|
|
914
1093
|
const rowClass = typeof this.options.rowClass === 'function' ? this.options.rowClass(row) : '';
|
|
915
1094
|
if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
|
|
916
1095
|
if (this.options.selectable !== false) tr.setAttribute('aria-selected', String(this._selected.has(id)));
|
|
917
|
-
this.
|
|
1096
|
+
if (this._hierarchy) {
|
|
1097
|
+
tr.setAttribute('aria-level', String(tree.depth + 1));
|
|
1098
|
+
if (tree.hasChildren) tr.setAttribute('aria-expanded', String(this._expanded.has(id)));
|
|
1099
|
+
}
|
|
1100
|
+
this._rowMeta.set(tr, { row, id, index, visibleIndex: tree.visibleIndex });
|
|
918
1101
|
const elements = this._rowElements.get(id) ?? [];
|
|
919
1102
|
elements.push(tr);
|
|
920
1103
|
this._rowElements.set(id, elements);
|
|
@@ -931,7 +1114,8 @@ export class Table extends Component {
|
|
|
931
1114
|
|
|
932
1115
|
this._columns.forEach((column) => {
|
|
933
1116
|
const cell = /** @type {HTMLTableCellElement} */ (h('td'));
|
|
934
|
-
|
|
1117
|
+
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1118
|
+
if (align) cell.dataset.align = align;
|
|
935
1119
|
// Written unconditionally: `responsive` can be switched on after the rows exist, and a
|
|
936
1120
|
// stacked cell has no column header above it to say what its value means.
|
|
937
1121
|
cell.dataset.label = column.label;
|
|
@@ -944,7 +1128,7 @@ export class Table extends Component {
|
|
|
944
1128
|
this._offerRovingCell(cell, id, column.id);
|
|
945
1129
|
}
|
|
946
1130
|
}
|
|
947
|
-
this._renderCellContent(cell, column, row, index);
|
|
1131
|
+
this._renderCellContent(cell, column, row, index, tree);
|
|
948
1132
|
tr.append(cell);
|
|
949
1133
|
});
|
|
950
1134
|
return tr;
|
|
@@ -956,13 +1140,35 @@ export class Table extends Component {
|
|
|
956
1140
|
* @param {TableColumn} column Column definition.
|
|
957
1141
|
* @param {TableRow} row Row data.
|
|
958
1142
|
* @param {number} index Row index.
|
|
1143
|
+
* @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
|
|
959
1144
|
* @returns {void}
|
|
960
1145
|
*/
|
|
961
|
-
_renderCellContent(cell, column, row, index) {
|
|
962
|
-
const value =
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
else
|
|
1146
|
+
_renderCellContent(cell, column, row, index, tree = this._hierarchyMeta?.get(this._idFor(row))) {
|
|
1147
|
+
const value = formatTableCell(column, row, index);
|
|
1148
|
+
const content = h('span', { class: 'zx-table__cell-content' });
|
|
1149
|
+
if (value && typeof value === 'object' && typeof value.nodeType === 'number') content.append(value);
|
|
1150
|
+
else if (value != null) content.append(document.createTextNode(String(value)));
|
|
1151
|
+
|
|
1152
|
+
if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
|
|
1153
|
+
cell.replaceChildren(...content.childNodes);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const branch = h('span', { class: 'zx-table__tree-cell' });
|
|
1158
|
+
branch.style.setProperty('--zx-table-depth', String(tree.depth));
|
|
1159
|
+
if (tree.hasChildren) {
|
|
1160
|
+
const expanded = this._expanded.has(tree.id);
|
|
1161
|
+
branch.append(h('button', {
|
|
1162
|
+
class: 'zx-table__tree-toggle',
|
|
1163
|
+
type: 'button',
|
|
1164
|
+
ariaExpanded: String(expanded),
|
|
1165
|
+
ariaLabel: `${expanded ? 'Collapse' : 'Expand'} row ${index + 1}`
|
|
1166
|
+
}, icon(expanded ? 'chevron-down' : 'chevron-right', { size: 14 })));
|
|
1167
|
+
} else {
|
|
1168
|
+
branch.append(h('span', { class: 'zx-table__tree-spacer', ariaHidden: 'true' }));
|
|
1169
|
+
}
|
|
1170
|
+
branch.append(content);
|
|
1171
|
+
cell.replaceChildren(branch);
|
|
966
1172
|
}
|
|
967
1173
|
|
|
968
1174
|
/** @param {MouseEvent} event @returns {void} */
|
|
@@ -975,7 +1181,17 @@ export class Table extends Component {
|
|
|
975
1181
|
const meta = this._rowMeta.get(rowElement);
|
|
976
1182
|
if (!meta) return;
|
|
977
1183
|
// Read the row metadata first: committing re-renders the body and detaches `rowElement`.
|
|
978
|
-
if (this._edit)
|
|
1184
|
+
if (this._edit) {
|
|
1185
|
+
this.commitEdit();
|
|
1186
|
+
// Validation or a preventDefault() persistence guard keeps the editor authoritative.
|
|
1187
|
+
if (this._edit) return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const disclosure = target.closest('.zx-table__tree-toggle');
|
|
1191
|
+
if (disclosure) {
|
|
1192
|
+
this.toggleRow(meta.id);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
979
1195
|
|
|
980
1196
|
const checkbox = target.closest('.zx-table__row-checkbox');
|
|
981
1197
|
if (checkbox && this.options.selectable === 'multi') {
|
|
@@ -1075,7 +1291,19 @@ export class Table extends Component {
|
|
|
1075
1291
|
return entry;
|
|
1076
1292
|
}
|
|
1077
1293
|
if (type === 'number') {
|
|
1078
|
-
const
|
|
1294
|
+
const locale = resolveColumnOption(column.locale, row);
|
|
1295
|
+
const decimals = resolveColumnDecimals(column, row);
|
|
1296
|
+
const suffix = column.type === 'currency'
|
|
1297
|
+
? resolveColumnOption(column.currency, row)
|
|
1298
|
+
: column.type === 'unit' ? resolveColumnOption(column.unit, row) : null;
|
|
1299
|
+
const inferred = {
|
|
1300
|
+
...(locale != null && !Object.prototype.hasOwnProperty.call(props, 'locale') ? { locale } : {}),
|
|
1301
|
+
...(decimals != null && !Object.prototype.hasOwnProperty.call(props, 'precision')
|
|
1302
|
+
? { precision: decimals }
|
|
1303
|
+
: {}),
|
|
1304
|
+
...(suffix != null && !Object.prototype.hasOwnProperty.call(props, 'unit') ? { unit: String(suffix) } : {})
|
|
1305
|
+
};
|
|
1306
|
+
const field = new NumberField(null, { ...inferred, ...props, value: value ?? null });
|
|
1079
1307
|
host.append(field.el);
|
|
1080
1308
|
entry.component = field;
|
|
1081
1309
|
entry.control = field.getInput();
|
|
@@ -1316,9 +1544,12 @@ export class Table extends Component {
|
|
|
1316
1544
|
_nextEditableCell(rowIndex, columnId, direction) {
|
|
1317
1545
|
const columnCount = this._columns.length;
|
|
1318
1546
|
if (rowIndex < 0 || columnCount === 0) return null;
|
|
1319
|
-
|
|
1547
|
+
const visible = this._visibleData();
|
|
1548
|
+
const startingId = this._idFor(this._data[rowIndex]);
|
|
1549
|
+
let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
|
|
1550
|
+
if (row < 0) return null;
|
|
1320
1551
|
let column = this._columns.findIndex((candidate) => candidate.id === columnId);
|
|
1321
|
-
const limit =
|
|
1552
|
+
const limit = visible.length * columnCount + columnCount;
|
|
1322
1553
|
for (let step = 0; step < limit; step += 1) {
|
|
1323
1554
|
column += direction;
|
|
1324
1555
|
if (column >= columnCount) {
|
|
@@ -1328,11 +1559,15 @@ export class Table extends Component {
|
|
|
1328
1559
|
row -= 1;
|
|
1329
1560
|
column = columnCount - 1;
|
|
1330
1561
|
}
|
|
1331
|
-
if (row < 0 || row >=
|
|
1332
|
-
const candidateRow =
|
|
1562
|
+
if (row < 0 || row >= visible.length) return null;
|
|
1563
|
+
const candidateRow = visible[row];
|
|
1333
1564
|
const candidateColumn = this._columns[column];
|
|
1334
1565
|
if (resolveEditable(candidateColumn, candidateRow) !== false) {
|
|
1335
|
-
return {
|
|
1566
|
+
return {
|
|
1567
|
+
id: this._idFor(candidateRow),
|
|
1568
|
+
columnId: candidateColumn.id,
|
|
1569
|
+
index: this._data.indexOf(candidateRow)
|
|
1570
|
+
};
|
|
1336
1571
|
}
|
|
1337
1572
|
}
|
|
1338
1573
|
return null;
|
|
@@ -1397,14 +1632,16 @@ export class Table extends Component {
|
|
|
1397
1632
|
return message === key ? fallback : message;
|
|
1398
1633
|
}
|
|
1399
1634
|
|
|
1400
|
-
/** @param {{row: TableRow, id: unknown, index: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
|
|
1635
|
+
/** @param {{row: TableRow, id: unknown, index: number, visibleIndex?: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
|
|
1401
1636
|
_toggleMultiSelection(meta, checked, shiftKey) {
|
|
1402
|
-
const
|
|
1637
|
+
const visible = this._visibleData();
|
|
1638
|
+
const anchorIndex = visible.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
|
|
1403
1639
|
if (shiftKey && anchorIndex >= 0) {
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
1640
|
+
const currentIndex = meta.visibleIndex ?? visible.findIndex((row) => Object.is(this._idFor(row), meta.id));
|
|
1641
|
+
const start = Math.min(anchorIndex, currentIndex);
|
|
1642
|
+
const end = Math.max(anchorIndex, currentIndex);
|
|
1406
1643
|
for (let index = start; index <= end; index += 1) {
|
|
1407
|
-
const id = this._idFor(
|
|
1644
|
+
const id = this._idFor(visible[index]);
|
|
1408
1645
|
if (checked) this._selected.add(id);
|
|
1409
1646
|
else this._selected.delete(id);
|
|
1410
1647
|
}
|
|
@@ -1460,7 +1697,39 @@ export class Table extends Component {
|
|
|
1460
1697
|
const column = this._columns.find((candidate) => candidate.id === this._sort.id);
|
|
1461
1698
|
if (!column) return;
|
|
1462
1699
|
const getValue = typeof column.sortValue === 'function' ? column.sortValue : (row) => row?.[column.id];
|
|
1463
|
-
this._data =
|
|
1700
|
+
this._data = this._hierarchy
|
|
1701
|
+
? sortHierarchyRows(this._data, this.options.rowId, this._hierarchy.parentId, getValue, this._sort.dir)
|
|
1702
|
+
: sortRows(this._data, getValue, this._sort.dir);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** @param {unknown} id @param {boolean} expanded @param {{silent?: boolean}} options @returns {this} */
|
|
1706
|
+
_setRowExpanded(id, expanded, options) {
|
|
1707
|
+
if (!this._hierarchy) return this;
|
|
1708
|
+
const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
1709
|
+
if (!branchIds.some((candidate) => Object.is(candidate, id))) return this;
|
|
1710
|
+
if (this._expanded.has(id) === expanded) return this;
|
|
1711
|
+
if (expanded) this._expanded.add(id);
|
|
1712
|
+
else this._expanded.delete(id);
|
|
1713
|
+
this._renderBody();
|
|
1714
|
+
if (!options.silent) this._emitRowToggle(id, expanded);
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/** @param {unknown} id @param {boolean} expanded @returns {void} */
|
|
1719
|
+
_emitRowToggle(id, expanded) {
|
|
1720
|
+
const row = this.getRow(id);
|
|
1721
|
+
if (row) this.emit('rowtoggle', { row, id, expanded });
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/** Keeps the reader's current expansion state limited to branches still present in the data. */
|
|
1725
|
+
_reconcileExpanded() {
|
|
1726
|
+
if (!this._hierarchy) {
|
|
1727
|
+
this._expanded.clear();
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const branches = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
|
|
1731
|
+
const valid = new Set(branches);
|
|
1732
|
+
for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
|
|
1464
1733
|
}
|
|
1465
1734
|
|
|
1466
1735
|
/** @returns {boolean} Whether selection changed. */
|
|
@@ -1504,6 +1773,9 @@ export class Table extends Component {
|
|
|
1504
1773
|
/** @event Table#sort @type {CustomEvent<TableSortDetail>} */
|
|
1505
1774
|
/** @event Table#selectionchange @type {CustomEvent<TableSelectionDetail>} */
|
|
1506
1775
|
/** @event Table#datachange @type {CustomEvent<TableDataDetail>} */
|
|
1776
|
+
/** @event Table#rowtoggle @type {CustomEvent<TableRowToggleDetail>} */
|
|
1777
|
+
/** @event Table#stackedchange @type {CustomEvent<{stacked: boolean}>} */
|
|
1778
|
+
/** @event Table#grow @type {CustomEvent<{rendered: number, total: number}>} */
|
|
1507
1779
|
|
|
1508
1780
|
/**
|
|
1509
1781
|
* Editor opened for a cell (or, in row mode, for the row).
|
|
@@ -1531,12 +1803,104 @@ export class Table extends Component {
|
|
|
1531
1803
|
* @type {CustomEvent<TableEditInvalidDetail>}
|
|
1532
1804
|
*/
|
|
1533
1805
|
|
|
1806
|
+
/**
|
|
1807
|
+
* Applies a column's built-in display type. A custom renderer always wins and may still return a
|
|
1808
|
+
* Node; all built-in results are strings so `_renderCellContent` can insert them as text.
|
|
1809
|
+
* @param {TableColumn} column Column definition.
|
|
1810
|
+
* @param {TableRow} row Source row.
|
|
1811
|
+
* @param {number} [index=0] Flat row index.
|
|
1812
|
+
* @returns {Node|string|number|null|undefined} Rendered cell value.
|
|
1813
|
+
*/
|
|
1814
|
+
export function formatTableCell(column, row, index = 0) {
|
|
1815
|
+
if (typeof column?.render === 'function') return column.render(row, index);
|
|
1816
|
+
const value = row?.[column?.id];
|
|
1817
|
+
const type = column?.type ?? 'text';
|
|
1818
|
+
if (type === 'text') return value;
|
|
1819
|
+
|
|
1820
|
+
const locale = resolveColumnOption(column.locale, row);
|
|
1821
|
+
const decimals = resolveColumnDecimals(column, row);
|
|
1822
|
+
const options = {
|
|
1823
|
+
...(locale != null ? { locale: String(locale) } : {}),
|
|
1824
|
+
...(decimals != null ? { decimals } : {})
|
|
1825
|
+
};
|
|
1826
|
+
if (type === 'number') return formatNumber(value, options);
|
|
1827
|
+
if (type === 'currency') {
|
|
1828
|
+
const currency = resolveColumnOption(column.currency, row);
|
|
1829
|
+
return formatCurrency(value, currency == null ? null : String(currency), options);
|
|
1830
|
+
}
|
|
1831
|
+
if (type === 'percent') return formatPercent(value, options);
|
|
1832
|
+
if (type === 'unit') {
|
|
1833
|
+
const number = formatNumber(value, options);
|
|
1834
|
+
const unit = resolveColumnOption(column.unit, row);
|
|
1835
|
+
return number && unit != null && String(unit) !== '' ? `${number}\u00a0${unit}` : number;
|
|
1836
|
+
}
|
|
1837
|
+
return value;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Projects flat parent references into the currently visible stable depth-first order. Orphans
|
|
1842
|
+
* become roots. A cycle is broken at its first source row, so every row is emitted once and the
|
|
1843
|
+
* traversal can never recurse forever.
|
|
1844
|
+
* @param {TableRow[]} rows Flat source rows.
|
|
1845
|
+
* @param {string|((row: TableRow) => unknown)} rowId Row id accessor.
|
|
1846
|
+
* @param {string|((row: TableRow) => unknown)} parentId Parent id accessor.
|
|
1847
|
+
* @param {Set<unknown>|unknown[]} [expanded=[]] Expanded branch ids.
|
|
1848
|
+
* @returns {TableHierarchyEntry[]} Visible hierarchy entries.
|
|
1849
|
+
*/
|
|
1850
|
+
export function projectHierarchyRows(rows, rowId, parentId, expanded = []) {
|
|
1851
|
+
const forest = hierarchyForest(rows, rowId, parentId);
|
|
1852
|
+
const open = expanded instanceof Set ? expanded : new Set(expanded);
|
|
1853
|
+
/** @type {TableHierarchyEntry[]} */
|
|
1854
|
+
const result = [];
|
|
1855
|
+
const stack = forest.roots.slice().reverse().map((node) => ({ node, depth: 0 }));
|
|
1856
|
+
while (stack.length) {
|
|
1857
|
+
const { node, depth } = stack.pop();
|
|
1858
|
+
result.push({
|
|
1859
|
+
row: node.row,
|
|
1860
|
+
id: node.id,
|
|
1861
|
+
index: node.index,
|
|
1862
|
+
depth,
|
|
1863
|
+
hasChildren: node.children.length > 0,
|
|
1864
|
+
parentId: node.parent?.id ?? null
|
|
1865
|
+
});
|
|
1866
|
+
if (!open.has(node.id)) continue;
|
|
1867
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
1868
|
+
stack.push({ node: node.children[index], depth: depth + 1 });
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return result;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
/**
|
|
1875
|
+
* Sorts roots and each sibling set independently, returning all rows in depth-first order. Parent
|
|
1876
|
+
* and child rows never cross levels merely because their cell values compare differently.
|
|
1877
|
+
* @param {TableRow[]} rows Flat source rows.
|
|
1878
|
+
* @param {string|((row: TableRow) => unknown)} rowId Row id accessor.
|
|
1879
|
+
* @param {string|((row: TableRow) => unknown)} parentId Parent id accessor.
|
|
1880
|
+
* @param {(row: TableRow) => unknown} getValue Sort accessor.
|
|
1881
|
+
* @param {TableSortDirection} direction Sort direction.
|
|
1882
|
+
* @returns {TableRow[]} Sorted flat rows.
|
|
1883
|
+
*/
|
|
1884
|
+
export function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
|
|
1885
|
+
const forest = hierarchyForest(rows, rowId, parentId);
|
|
1886
|
+
const sorted = (nodes) => sortRows(nodes, (node) => getValue(node.row), direction);
|
|
1887
|
+
const result = [];
|
|
1888
|
+
const stack = sorted(forest.roots).reverse();
|
|
1889
|
+
while (stack.length) {
|
|
1890
|
+
const node = stack.pop();
|
|
1891
|
+
result.push(node.row);
|
|
1892
|
+
const children = sorted(node.children);
|
|
1893
|
+
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
1894
|
+
}
|
|
1895
|
+
return result;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1534
1898
|
/**
|
|
1535
1899
|
* Resolves the editor type of a column for one row.
|
|
1536
1900
|
*
|
|
1537
1901
|
* `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.
|
|
1902
|
+
* `false` still marks the cell read-only. `editable: true` infers `'number'` for number, currency,
|
|
1903
|
+
* percent, and unit columns and `'text'` otherwise; unknown type names are treated as not editable.
|
|
1540
1904
|
* @param {TableColumn} column Column definition.
|
|
1541
1905
|
* @param {TableRow} row Row the cell belongs to.
|
|
1542
1906
|
* @returns {TableEditorType|false} Editor type, or false when the cell is read-only.
|
|
@@ -1548,7 +1912,7 @@ export function resolveEditable(column, row) {
|
|
|
1548
1912
|
return 'custom';
|
|
1549
1913
|
}
|
|
1550
1914
|
const declared = typeof column.editable === 'function' ? column.editable(row) : column.editable;
|
|
1551
|
-
if (declared === true) return 'text';
|
|
1915
|
+
if (declared === true) return isNumericColumn(column) ? 'number' : 'text';
|
|
1552
1916
|
if (typeof declared === 'string' && EDITOR_TYPES.has(declared)) return /** @type {TableEditorType} */ (declared);
|
|
1553
1917
|
return false;
|
|
1554
1918
|
}
|
|
@@ -1647,6 +2011,120 @@ export function firstValidationError(values, columns, row, fallbackMessage = 'In
|
|
|
1647
2011
|
return null;
|
|
1648
2012
|
}
|
|
1649
2013
|
|
|
2014
|
+
/** @param {unknown} option @param {TableRow} row @returns {unknown} */
|
|
2015
|
+
function resolveColumnOption(option, row) {
|
|
2016
|
+
return typeof option === 'function' ? option(row) : option;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Normalizes an explicit fraction-digit value while preserving a nullish callback result as
|
|
2021
|
+
* "unspecified" so Intl and NumberField can use their normal currency/value defaults.
|
|
2022
|
+
* @param {TableColumn} column
|
|
2023
|
+
* @param {TableRow} row
|
|
2024
|
+
* @returns {number|null}
|
|
2025
|
+
*/
|
|
2026
|
+
function resolveColumnDecimals(column, row) {
|
|
2027
|
+
const value = resolveColumnOption(column?.decimals, row);
|
|
2028
|
+
if (value == null) return null;
|
|
2029
|
+
const number = Number(value);
|
|
2030
|
+
return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : null;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** @param {TableColumn} column @returns {boolean} */
|
|
2034
|
+
function isNumericColumn(column) {
|
|
2035
|
+
return ['number', 'currency', 'percent', 'unit'].includes(column?.type);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Builds a canonical forest without mutating source rows. The first row encountered in a cycle is
|
|
2040
|
+
* promoted to a root; source order therefore makes malformed data deterministic.
|
|
2041
|
+
* @param {TableRow[]} rows
|
|
2042
|
+
* @param {string|((row: TableRow) => unknown)} rowId
|
|
2043
|
+
* @param {string|((row: TableRow) => unknown)} parentId
|
|
2044
|
+
* @returns {{roots: TableHierarchyNode[], nodes: TableHierarchyNode[]}}
|
|
2045
|
+
*/
|
|
2046
|
+
function hierarchyForest(rows, rowId, parentId) {
|
|
2047
|
+
/** @type {TableHierarchyNode[]} */
|
|
2048
|
+
const nodes = (Array.isArray(rows) ? rows : []).map((row, index) => ({
|
|
2049
|
+
row,
|
|
2050
|
+
id: readRowValue(rowId, row),
|
|
2051
|
+
index,
|
|
2052
|
+
parent: null,
|
|
2053
|
+
children: []
|
|
2054
|
+
}));
|
|
2055
|
+
const byId = new Map();
|
|
2056
|
+
for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
|
|
2057
|
+
|
|
2058
|
+
/** @type {Map<TableHierarchyNode, TableHierarchyNode|null>} */
|
|
2059
|
+
const parentOf = new Map();
|
|
2060
|
+
for (const node of nodes) {
|
|
2061
|
+
const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
|
|
2062
|
+
parentOf.set(node, parent === node ? null : parent);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Parent references form a functional graph. Breaking the repeated node's edge turns every
|
|
2066
|
+
// connected component into a finite tree while keeping all non-cyclic parent links intact.
|
|
2067
|
+
for (const node of nodes) {
|
|
2068
|
+
const seen = new Set([node]);
|
|
2069
|
+
let cursor = parentOf.get(node) ?? null;
|
|
2070
|
+
while (cursor) {
|
|
2071
|
+
if (seen.has(cursor)) {
|
|
2072
|
+
parentOf.set(cursor, null);
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
seen.add(cursor);
|
|
2076
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const roots = [];
|
|
2081
|
+
for (const node of nodes) {
|
|
2082
|
+
node.parent = parentOf.get(node) ?? null;
|
|
2083
|
+
if (node.parent) node.parent.children.push(node);
|
|
2084
|
+
else roots.push(node);
|
|
2085
|
+
}
|
|
2086
|
+
return { roots, nodes };
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* @param {TableRow[]} rows
|
|
2091
|
+
* @param {string|((row: TableRow) => unknown)} rowId
|
|
2092
|
+
* @param {string|((row: TableRow) => unknown)} parentId
|
|
2093
|
+
* @returns {unknown[]}
|
|
2094
|
+
*/
|
|
2095
|
+
function hierarchyBranchIds(rows, rowId, parentId) {
|
|
2096
|
+
return hierarchyForest(rows, rowId, parentId).nodes
|
|
2097
|
+
.filter((node) => node.children.length > 0)
|
|
2098
|
+
.map((node) => node.id);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/** @param {string|((row: TableRow) => unknown)} accessor @param {TableRow} row @returns {unknown} */
|
|
2102
|
+
function readRowValue(accessor, row) {
|
|
2103
|
+
return typeof accessor === 'function' ? accessor(row) : row?.[accessor];
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* @param {unknown} option
|
|
2108
|
+
* @param {TableColumn[]} columns
|
|
2109
|
+
* @returns {(TableHierarchyOptions & {column: string})|null}
|
|
2110
|
+
*/
|
|
2111
|
+
function normalizeHierarchy(option, columns) {
|
|
2112
|
+
if (!option || typeof option !== 'object') return null;
|
|
2113
|
+
const parentId = option.parentId;
|
|
2114
|
+
if (typeof parentId !== 'string' && typeof parentId !== 'function') return null;
|
|
2115
|
+
const requested = typeof option.column === 'string' ? option.column : null;
|
|
2116
|
+
const column = columns.some((candidate) => candidate.id === requested)
|
|
2117
|
+
? requested
|
|
2118
|
+
: columns[0]?.id;
|
|
2119
|
+
if (!column) return null;
|
|
2120
|
+
return { ...option, parentId, column };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/** @param {unknown} option @returns {Set<unknown>} */
|
|
2124
|
+
function initialExpanded(option) {
|
|
2125
|
+
return new Set(Array.isArray(option) ? option : []);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1650
2128
|
/** @param {unknown} mode @returns {TableEditMode} */
|
|
1651
2129
|
function normalizeEditMode(mode) {
|
|
1652
2130
|
if (mode === 'cell' || mode === 'row') return mode;
|