@zeyos/zx 3.0.0 → 4.1.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 +95 -0
- package/DESIGN-SYSTEM.md +87 -71
- package/README.md +16 -37
- package/dist/types/components/app-icon/app-icon.d.ts +88 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +1 -0
- package/dist/types/components/filter/filter-model.d.ts +87 -0
- package/dist/types/components/filter/filter.d.ts +331 -0
- package/dist/types/components/launcher/launcher.d.ts +65 -6
- package/dist/types/components/select/select.d.ts +97 -4
- package/dist/types/components/table/table.d.ts +107 -3
- package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
- package/dist/types/core/dom.d.ts +9 -1
- package/dist/types/core/util.d.ts +9 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
- package/dist/types/zeyos/icons.d.ts +24 -2
- package/dist/types/zeyos/index.d.ts +3 -2
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +41 -0
- package/dist/zx-zeyos.esm.js +2018 -113
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +815 -16
- package/dist/zx.esm.js +2661 -338
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +2661 -338
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +602 -110
- package/docs/llms.md +181 -123
- package/docs/llms.txt +24 -21
- package/package.json +8 -17
- package/src/components/app-icon/app-icon.css +105 -0
- package/src/components/app-icon/app-icon.js +134 -0
- package/src/components/app-sidebar/app-sidebar.css +12 -0
- package/src/components/app-sidebar/app-sidebar.js +310 -110
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.js +8 -3
- package/src/components/filter/filter-model.js +226 -0
- package/src/components/filter/filter.css +147 -0
- package/src/components/filter/filter.js +554 -0
- package/src/components/grid/grid.css +1 -1
- package/src/components/grid/grid.js +14 -4
- package/src/components/launcher/launcher.css +113 -3
- package/src/components/launcher/launcher.js +234 -26
- package/src/components/message/message.css +36 -3
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +161 -10
- package/src/components/select/select.js +361 -48
- package/src/components/table/table.css +85 -0
- package/src/components/table/table.js +426 -31
- package/src/components/tag-picker/tag-picker.css +19 -0
- package/src/components/tag-picker/tag-picker.js +42 -5
- package/src/core/dom.js +21 -1
- package/src/core/util.js +20 -0
- package/src/index.js +9 -2
- package/src/{components/app-rail → internal}/app-rail.js +80 -30
- package/src/zeyos/icons.js +38 -20
- package/src/zeyos/index.js +8 -2
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +116 -1
- package/styles/tokens/global.css +1 -1
- package/styles/tokens/modules.css +1 -0
- package/styles/tokens/semantic.css +9 -0
- package/styles/tokens/themes.css +2 -2
- package/styles/zx.css +3 -0
- package/MIGRATION.md +0 -749
- 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 -103
- 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/zx-compat.esm.js +0 -17848
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -29008
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -167
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -254
- 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
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
import { formatCurrency, formatNumber, formatPercent } from '../../core/format.js';
|
|
5
5
|
import { icon } from '../../core/icons.js';
|
|
6
6
|
import { printf } from '../../core/i18n.js';
|
|
@@ -104,6 +104,10 @@ import { sortRows } from './sort.js';
|
|
|
104
104
|
* @property {TableEditMode} [editMode=false] Inline editing mode. `'cell'` edits one cell, `'row'`
|
|
105
105
|
* opens every editable cell of the row together and commits them as a unit. Editing is completely
|
|
106
106
|
* inert while this is `false`.
|
|
107
|
+
* @property {'single'|'double'} [editTrigger='double'] Gesture that starts an editable cell.
|
|
108
|
+
* @property {boolean} [rowReorder=false] Whether rows expose pointer/keyboard reorder handles.
|
|
109
|
+
* @property {boolean} [columnVisibility=false] Whether a column visibility chooser is rendered.
|
|
110
|
+
* @property {string[]} [hiddenColumns=[]] Initially hidden column IDs; at least one stays visible.
|
|
107
111
|
* @property {false|TableHierarchyOptions} [hierarchy=false] Flat parent references projected as an
|
|
108
112
|
* expandable treegrid. Row data and mutation APIs remain flat.
|
|
109
113
|
* @property {(event: CustomEvent<TableRowClickDetail>) => void} [onrowclick]
|
|
@@ -120,6 +124,8 @@ import { sortRows } from './sort.js';
|
|
|
120
124
|
* @property {(event: CustomEvent<TableEditCancelDetail>) => void} [oneditcancel]
|
|
121
125
|
* @property {(event: CustomEvent<TableEditInvalidDetail>) => void} [oneditinvalid]
|
|
122
126
|
* @property {(event: CustomEvent<TableRowToggleDetail>) => void} [onrowtoggle]
|
|
127
|
+
* @property {(event: CustomEvent<TableRowMoveDetail>) => void} [onrowmove]
|
|
128
|
+
* @property {(event: CustomEvent<{visible:string[],hidden:string[]}>) => void} [oncolumnvisibilitychange]
|
|
123
129
|
*/
|
|
124
130
|
|
|
125
131
|
/**
|
|
@@ -134,6 +140,7 @@ import { sortRows } from './sort.js';
|
|
|
134
140
|
/** @typedef {{rows: TableRow[], ids: unknown[]}} TableSelectionDetail */
|
|
135
141
|
/** @typedef {{rows: TableRow[]}} TableDataDetail */
|
|
136
142
|
/** @typedef {{row: TableRow, id: unknown, expanded: boolean}} TableRowToggleDetail */
|
|
143
|
+
/** @typedef {{row:TableRow,id:unknown,target:TableRow,targetId:unknown,position:'before'|'after'}} TableRowMoveDetail */
|
|
137
144
|
|
|
138
145
|
/**
|
|
139
146
|
* Visible hierarchy metadata returned by `projectHierarchyRows`.
|
|
@@ -226,6 +233,8 @@ const FOCUSABLE = 'input, textarea, select, button, [tabindex]:not([tabindex="-1
|
|
|
226
233
|
* @fires Table#rowtoggle
|
|
227
234
|
* @fires Table#stackedchange
|
|
228
235
|
* @fires Table#grow
|
|
236
|
+
* @fires Table#rowmove
|
|
237
|
+
* @fires Table#columnvisibilitychange
|
|
229
238
|
* @extends {Component<TableOptions>}
|
|
230
239
|
*/
|
|
231
240
|
export class Table extends Component {
|
|
@@ -247,7 +256,11 @@ export class Table extends Component {
|
|
|
247
256
|
rowClass: null,
|
|
248
257
|
zebra: true,
|
|
249
258
|
editMode: false,
|
|
250
|
-
|
|
259
|
+
editTrigger: 'double',
|
|
260
|
+
hierarchy: false,
|
|
261
|
+
rowReorder: false,
|
|
262
|
+
columnVisibility: false,
|
|
263
|
+
hiddenColumns: []
|
|
251
264
|
};
|
|
252
265
|
|
|
253
266
|
/**
|
|
@@ -261,11 +274,13 @@ export class Table extends Component {
|
|
|
261
274
|
|
|
262
275
|
/** @returns {HTMLElement} Table root. */
|
|
263
276
|
render() {
|
|
277
|
+
const created = this.el === null;
|
|
264
278
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
265
279
|
// Claimed here, not left to the base constructor: `_watchWidth()` below observes `this.el`,
|
|
266
280
|
// and until this assignment it is null for a component creating its own root.
|
|
267
281
|
this.el = root;
|
|
268
|
-
this.
|
|
282
|
+
this._createdRoot = created;
|
|
283
|
+
this._original = created ? null : snapshotTarget(root);
|
|
269
284
|
this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
|
|
270
285
|
this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
|
|
271
286
|
this._selected = new Set();
|
|
@@ -277,6 +292,7 @@ export class Table extends Component {
|
|
|
277
292
|
// Editing state. `_edit` is null unless a cell or row is currently open for editing; every
|
|
278
293
|
// other member below is only ever touched while `_editMode !== false`.
|
|
279
294
|
this._editMode = normalizeEditMode(this.options.editMode);
|
|
295
|
+
this._editTrigger = this.options.editTrigger === 'single' ? 'single' : 'double';
|
|
280
296
|
this._edit = null;
|
|
281
297
|
this._editMessage = null;
|
|
282
298
|
this._stacked = false;
|
|
@@ -288,6 +304,11 @@ export class Table extends Component {
|
|
|
288
304
|
this._rovingCell = null;
|
|
289
305
|
this._rovingMatched = false;
|
|
290
306
|
this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
|
|
307
|
+
this._hiddenColumns = new Set((Array.isArray(this.options.hiddenColumns) ? this.options.hiddenColumns : [])
|
|
308
|
+
.map(String).filter((id) => this._columns.some((column) => column.id === id)));
|
|
309
|
+
if (this._hiddenColumns.size >= this._columns.length && this._columns[0]) this._hiddenColumns.delete(this._columns[0].id);
|
|
310
|
+
this._dragId = null;
|
|
311
|
+
this._grabbedId = null;
|
|
291
312
|
this._expanded = initialExpanded(this._hierarchy?.expanded);
|
|
292
313
|
|
|
293
314
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
@@ -312,12 +333,16 @@ export class Table extends Component {
|
|
|
312
333
|
zebra: String(Boolean(this.options.zebra))
|
|
313
334
|
}
|
|
314
335
|
}, this._table);
|
|
336
|
+
this._rowMoveMessage = this.options.rowReorder ? h('div', {
|
|
337
|
+
class: 'zx-table__reorder-status', role: 'status', ariaLive: 'polite'
|
|
338
|
+
}) : null;
|
|
315
339
|
if (this.options.height != null) {
|
|
316
340
|
this._scroll.style.blockSize = typeof this.options.height === 'number' ?
|
|
317
341
|
`${this.options.height}px` : String(this.options.height);
|
|
318
342
|
}
|
|
343
|
+
this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
|
|
319
344
|
if (this._editMode === false) {
|
|
320
|
-
root.replaceChildren(this._scroll);
|
|
345
|
+
root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
|
|
321
346
|
} else {
|
|
322
347
|
root.dataset.editMode = this._editMode;
|
|
323
348
|
// Always present and empty rather than hidden: a live region that is revealed together with
|
|
@@ -328,7 +353,7 @@ export class Table extends Component {
|
|
|
328
353
|
role: 'status',
|
|
329
354
|
ariaLive: 'polite'
|
|
330
355
|
});
|
|
331
|
-
root.replaceChildren(this._scroll, this._editMessage);
|
|
356
|
+
root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
|
|
332
357
|
}
|
|
333
358
|
|
|
334
359
|
this._renderColumns();
|
|
@@ -337,6 +362,19 @@ export class Table extends Component {
|
|
|
337
362
|
this._watchWidth();
|
|
338
363
|
this.listen(this._tbody, 'click', (event) => this._handleBodyClick(event));
|
|
339
364
|
this.listen(this._tbody, 'dblclick', (event) => this._handleBodyDoubleClick(event));
|
|
365
|
+
if (this.options.rowReorder) {
|
|
366
|
+
this.listen(this._tbody, 'dragstart', (event) => this._handleRowDragStart(/** @type {DragEvent} */ (event)));
|
|
367
|
+
this.listen(this._tbody, 'dragover', (event) => this._handleRowDragOver(/** @type {DragEvent} */ (event)));
|
|
368
|
+
this.listen(this._tbody, 'drop', (event) => this._handleRowDrop(/** @type {DragEvent} */ (event)));
|
|
369
|
+
this.listen(this._tbody, 'dragend', () => this._clearRowDrag());
|
|
370
|
+
this.listen(this._tbody, 'keydown', (event) => this._handleRowHandleKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
371
|
+
}
|
|
372
|
+
if (this._columnControls) {
|
|
373
|
+
this.listen(this._columnControls, 'change', (event) => {
|
|
374
|
+
const input = /** @type {HTMLInputElement|null} */ (event.target);
|
|
375
|
+
if (input?.classList.contains('zx-table__column-toggle')) this.setColumnVisible(input.value, input.checked);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
340
378
|
if (this._editMode !== false) {
|
|
341
379
|
this.listen(this._tbody, 'keydown', (event) => this._handleEditKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
342
380
|
this.listen(this._tbody, 'focusin', (event) => this._handleEditFocusIn(event));
|
|
@@ -347,8 +385,8 @@ export class Table extends Component {
|
|
|
347
385
|
|
|
348
386
|
/**
|
|
349
387
|
* Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
|
|
350
|
-
* shown, and `aria-busy` is set — a lightweight
|
|
351
|
-
*
|
|
388
|
+
* shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
|
|
389
|
+
* Call `setLoading(false)` (or `setData`) when done.
|
|
352
390
|
* @param {boolean} [loading=true] Whether the table is loading.
|
|
353
391
|
* @returns {this}
|
|
354
392
|
*/
|
|
@@ -458,6 +496,80 @@ export class Table extends Component {
|
|
|
458
496
|
return [...this._data];
|
|
459
497
|
}
|
|
460
498
|
|
|
499
|
+
/** Returns visible column IDs in caller order. @returns {string[]} */
|
|
500
|
+
getVisibleColumns() {
|
|
501
|
+
return this._visibleColumns().map((column) => column.id);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Returns hidden column IDs in caller order. @returns {string[]} */
|
|
505
|
+
getHiddenColumns() {
|
|
506
|
+
return this._columns.filter((column) => this._hiddenColumns.has(column.id)).map((column) => column.id);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Shows or hides one column. The final visible column cannot be hidden.
|
|
511
|
+
* @param {string} id Column ID.
|
|
512
|
+
* @param {boolean} [visible=true] Desired visibility.
|
|
513
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
514
|
+
* @returns {this}
|
|
515
|
+
*/
|
|
516
|
+
setColumnVisible(id, visible = true, options = {}) {
|
|
517
|
+
const column = this._columns.find((candidate) => candidate.id === id);
|
|
518
|
+
if (!column) throw new RangeError(`Unknown table column: ${id}`);
|
|
519
|
+
const active = /** @type {HTMLElement|null} */ (document.activeElement);
|
|
520
|
+
const restoreFocus = active?.classList.contains('zx-table__column-toggle')
|
|
521
|
+
&& this._columnList?.contains(active) ? /** @type {HTMLInputElement} */ (active).value : null;
|
|
522
|
+
if (!visible && !this._hiddenColumns.has(id) && this._visibleColumns().length <= 1) {
|
|
523
|
+
this._renderColumnControls(restoreFocus);
|
|
524
|
+
return this;
|
|
525
|
+
}
|
|
526
|
+
const changed = visible ? this._hiddenColumns.delete(id) : !this._hiddenColumns.has(id);
|
|
527
|
+
if (!visible && changed) this._hiddenColumns.add(id);
|
|
528
|
+
if (!changed) return this;
|
|
529
|
+
this._renderColumns();
|
|
530
|
+
this._renderHeader();
|
|
531
|
+
this._renderBody();
|
|
532
|
+
this._renderColumnControls(restoreFocus);
|
|
533
|
+
if (!options.silent) this.emit('columnvisibilitychange', {
|
|
534
|
+
visible: this.getVisibleColumns(), hidden: this.getHiddenColumns()
|
|
535
|
+
});
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Toggles one column. @param {string} id @param {{silent?:boolean}} [options={}] @returns {this} */
|
|
540
|
+
toggleColumn(id, options = {}) {
|
|
541
|
+
return this.setColumnVisible(id, this._hiddenColumns.has(id), options);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Reorders one row relative to another and emits a cancelable `rowmove` before committing.
|
|
546
|
+
* Hierarchical rows may only move among siblings; an active local sort disables manual order.
|
|
547
|
+
* @param {unknown} id Moving row ID.
|
|
548
|
+
* @param {unknown} targetId Target row ID.
|
|
549
|
+
* @param {'before'|'after'} [position='before'] Target edge.
|
|
550
|
+
* @returns {this}
|
|
551
|
+
*/
|
|
552
|
+
moveRow(id, targetId, position = 'before') {
|
|
553
|
+
if (!this.options.rowReorder || this._sort && this.options.sortMode === 'local') return this;
|
|
554
|
+
const row = this.getRow(id);
|
|
555
|
+
const target = this.getRow(targetId);
|
|
556
|
+
if (!row || !target || Object.is(id, targetId)) return this;
|
|
557
|
+
const next = reorderTableRows(this._data, this.options.rowId, id, targetId, position, this._hierarchy?.parentId);
|
|
558
|
+
if (!next) return this;
|
|
559
|
+
const event = this.emit('rowmove', { row, id, target, targetId, position });
|
|
560
|
+
if (event.defaultPrevented) return this;
|
|
561
|
+
this._data = next;
|
|
562
|
+
this._renderBody();
|
|
563
|
+
if (this._rowMoveMessage) {
|
|
564
|
+
const siblings = this._siblingIds(id);
|
|
565
|
+
const index = siblings.findIndex((candidate) => Object.is(candidate, id));
|
|
566
|
+
this._rowMoveMessage.textContent = `Moved row to position ${index + 1} of ${siblings.length}.`;
|
|
567
|
+
}
|
|
568
|
+
this._emitDataChange();
|
|
569
|
+
queueMicrotask(() => this._rowHandle(id)?.focus());
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
|
|
461
573
|
/**
|
|
462
574
|
* Removes all rows.
|
|
463
575
|
* @returns {this}
|
|
@@ -639,15 +751,20 @@ export class Table extends Component {
|
|
|
639
751
|
*/
|
|
640
752
|
startEdit(rowId, columnId) {
|
|
641
753
|
if (this._editMode === false) return this;
|
|
642
|
-
const
|
|
643
|
-
if (!
|
|
754
|
+
const requested = this._columns.find((candidate) => candidate.id === columnId);
|
|
755
|
+
if (!requested) return this;
|
|
644
756
|
const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
|
|
645
|
-
if (current === undefined
|
|
757
|
+
if (current === undefined) return this;
|
|
758
|
+
const column = this._editMode === 'row' && this._hiddenColumns.has(requested.id)
|
|
759
|
+
? this._visibleColumns().find((candidate) => resolveEditable(candidate, current) !== false) ?? null
|
|
760
|
+
: requested;
|
|
761
|
+
if (!column || resolveEditable(column, current) === false) return this;
|
|
762
|
+
const activeColumnId = column.id;
|
|
646
763
|
|
|
647
764
|
if (this._edit) {
|
|
648
|
-
if (Object.is(this._edit.id, rowId) && this._edit.cells.has(
|
|
649
|
-
this._edit.columnId =
|
|
650
|
-
this._focusEditor(
|
|
765
|
+
if (Object.is(this._edit.id, rowId) && this._edit.cells.has(activeColumnId)) {
|
|
766
|
+
this._edit.columnId = activeColumnId;
|
|
767
|
+
this._focusEditor(activeColumnId);
|
|
651
768
|
return this;
|
|
652
769
|
}
|
|
653
770
|
this.commitEdit();
|
|
@@ -662,7 +779,7 @@ export class Table extends Component {
|
|
|
662
779
|
const tr = this._rowElements.get(rowId)?.[0];
|
|
663
780
|
if (!tr) return this;
|
|
664
781
|
const targets = this._editMode === 'row'
|
|
665
|
-
? this.
|
|
782
|
+
? this._visibleColumns().filter((candidate) => resolveEditable(candidate, row) !== false)
|
|
666
783
|
: [column];
|
|
667
784
|
/** @type {Map<string, TableEditorEntry>} */
|
|
668
785
|
const cells = new Map();
|
|
@@ -674,10 +791,10 @@ export class Table extends Component {
|
|
|
674
791
|
}
|
|
675
792
|
if (cells.size === 0) return this;
|
|
676
793
|
|
|
677
|
-
this._edit = { id: rowId, columnId, row, cells, tr };
|
|
794
|
+
this._edit = { id: rowId, columnId: activeColumnId, row, cells, tr };
|
|
678
795
|
tr.dataset.editing = 'true';
|
|
679
|
-
this.emit('editstart', { row, id: rowId, column, columnId, value: row?.[
|
|
680
|
-
this._focusEditor(
|
|
796
|
+
this.emit('editstart', { row, id: rowId, column, columnId: activeColumnId, value: row?.[activeColumnId] });
|
|
797
|
+
this._focusEditor(activeColumnId);
|
|
681
798
|
return this;
|
|
682
799
|
}
|
|
683
800
|
|
|
@@ -942,24 +1059,192 @@ export class Table extends Component {
|
|
|
942
1059
|
this._teardownEdit();
|
|
943
1060
|
this._width?.destroy();
|
|
944
1061
|
this._width = null;
|
|
945
|
-
if (this.
|
|
946
|
-
|
|
947
|
-
this.el.replaceChildren(...this._originalChildren);
|
|
1062
|
+
if (!this._restored && !this._createdRoot) {
|
|
1063
|
+
restoreTarget(this.el, this._original);
|
|
948
1064
|
this._restored = true;
|
|
949
1065
|
}
|
|
950
1066
|
super.destroy();
|
|
951
1067
|
}
|
|
952
1068
|
|
|
1069
|
+
/** @returns {TableColumn[]} */
|
|
1070
|
+
_visibleColumns() {
|
|
1071
|
+
return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** @returns {HTMLElement} */
|
|
1075
|
+
_createColumnControls() {
|
|
1076
|
+
const list = h('div', { class: 'zx-table__column-list' });
|
|
1077
|
+
this._columnList = list;
|
|
1078
|
+
const details = h('details', { class: 'zx-table__column-controls' },
|
|
1079
|
+
h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }), 'Columns'),
|
|
1080
|
+
list);
|
|
1081
|
+
this._renderColumnControls();
|
|
1082
|
+
return details;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** @param {string|null} [focusColumn=null] Column checkbox that keeps focus. @returns {void} */
|
|
1086
|
+
_renderColumnControls(focusColumn = null) {
|
|
1087
|
+
if (!this._columnList) return;
|
|
1088
|
+
const visible = this._visibleColumns();
|
|
1089
|
+
const current = [...this._columnList.querySelectorAll('.zx-table__column-toggle')];
|
|
1090
|
+
const reusable = current.length === this._columns.length
|
|
1091
|
+
&& current.every((control, index) => /** @type {HTMLInputElement} */ (control).value === this._columns[index].id);
|
|
1092
|
+
if (reusable) {
|
|
1093
|
+
current.forEach((control, index) => {
|
|
1094
|
+
const input = /** @type {HTMLInputElement} */ (control);
|
|
1095
|
+
const checked = !this._hiddenColumns.has(this._columns[index].id);
|
|
1096
|
+
input.checked = checked;
|
|
1097
|
+
input.disabled = checked && visible.length === 1;
|
|
1098
|
+
});
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
this._columnList.replaceChildren(...this._columns.map((column) => {
|
|
1102
|
+
const checked = !this._hiddenColumns.has(column.id);
|
|
1103
|
+
return h('label', { class: 'zx-table__column-option' },
|
|
1104
|
+
h('input', {
|
|
1105
|
+
class: 'zx-table__column-toggle', type: 'checkbox', value: column.id, checked,
|
|
1106
|
+
disabled: checked && visible.length === 1
|
|
1107
|
+
}),
|
|
1108
|
+
h('span', {}, column.label));
|
|
1109
|
+
}));
|
|
1110
|
+
if (focusColumn !== null) {
|
|
1111
|
+
const control = [...this._columnList.querySelectorAll('.zx-table__column-toggle')]
|
|
1112
|
+
.find((candidate) => /** @type {HTMLInputElement} */ (candidate).value === focusColumn);
|
|
1113
|
+
queueMicrotask(() => {
|
|
1114
|
+
if (control?.isConnected) /** @type {HTMLElement} */ (control).focus();
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** @param {DragEvent} event @returns {void} */
|
|
1120
|
+
_handleRowDragStart(event) {
|
|
1121
|
+
const handle = /** @type {Element|null} */ (event.target)?.closest?.('.zx-table__row-handle');
|
|
1122
|
+
const rowElement = handle?.closest('tr[data-row]');
|
|
1123
|
+
const meta = rowElement ? this._rowMeta.get(rowElement) : null;
|
|
1124
|
+
if (!handle || !meta || this._sort && this.options.sortMode === 'local') {
|
|
1125
|
+
event.preventDefault();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
this._dragId = meta.id;
|
|
1129
|
+
rowElement.dataset.dragging = 'true';
|
|
1130
|
+
// Native drag needs a payload in some engines, but internal movement already uses `_dragId`.
|
|
1131
|
+
// Keep the transferable marker opaque so record identifiers never leave the table.
|
|
1132
|
+
event.dataTransfer?.setData('text/plain', 'zx-table-row');
|
|
1133
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/** @param {DragEvent} event @returns {void} */
|
|
1137
|
+
_handleRowDragOver(event) {
|
|
1138
|
+
if (this._dragId == null) return;
|
|
1139
|
+
const rowElement = /** @type {Element|null} */ (event.target)?.closest?.('tr[data-row]');
|
|
1140
|
+
const meta = rowElement ? this._rowMeta.get(rowElement) : null;
|
|
1141
|
+
if (!meta || Object.is(meta.id, this._dragId) || !this._canMoveTogether(this._dragId, meta.id)) return;
|
|
1142
|
+
event.preventDefault();
|
|
1143
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
1144
|
+
this._clearDropMarkers();
|
|
1145
|
+
const rectangle = rowElement.getBoundingClientRect();
|
|
1146
|
+
rowElement.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? 'after' : 'before';
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** @param {DragEvent} event @returns {void} */
|
|
1150
|
+
_handleRowDrop(event) {
|
|
1151
|
+
if (this._dragId == null) return;
|
|
1152
|
+
const rowElement = /** @type {Element|null} */ (event.target)?.closest?.('tr[data-row]');
|
|
1153
|
+
const meta = rowElement ? this._rowMeta.get(rowElement) : null;
|
|
1154
|
+
const position = rowElement?.dataset.drop === 'after' ? 'after' : 'before';
|
|
1155
|
+
if (meta && this._canMoveTogether(this._dragId, meta.id)) {
|
|
1156
|
+
event.preventDefault();
|
|
1157
|
+
this.moveRow(this._dragId, meta.id, position);
|
|
1158
|
+
}
|
|
1159
|
+
this._clearRowDrag();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
1163
|
+
_handleRowHandleKeydown(event) {
|
|
1164
|
+
const handle = /** @type {Element|null} */ (event.target)?.closest?.('.zx-table__row-handle');
|
|
1165
|
+
const rowElement = handle?.closest('tr[data-row]');
|
|
1166
|
+
const meta = rowElement ? this._rowMeta.get(rowElement) : null;
|
|
1167
|
+
if (!handle || !meta) return;
|
|
1168
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
1169
|
+
event.preventDefault();
|
|
1170
|
+
this._grabbedId = Object.is(this._grabbedId, meta.id) ? null : meta.id;
|
|
1171
|
+
handle.setAttribute('aria-pressed', String(this._grabbedId != null));
|
|
1172
|
+
// Row movement has its own visually hidden live region. Reusing the visible validation
|
|
1173
|
+
// message would make a successful drop look like a red editing error on editable grids.
|
|
1174
|
+
const status = this._rowMoveMessage ?? this._editMessage;
|
|
1175
|
+
if (status) status.textContent = this._grabbedId == null
|
|
1176
|
+
? 'Row dropped' : 'Row grabbed. Use the arrow keys to move it.';
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (event.key === 'Escape' && this._grabbedId != null) {
|
|
1180
|
+
event.preventDefault();
|
|
1181
|
+
this._grabbedId = null;
|
|
1182
|
+
handle.setAttribute('aria-pressed', 'false');
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (!Object.is(this._grabbedId, meta.id) || !['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
1186
|
+
event.preventDefault();
|
|
1187
|
+
const siblings = this._siblingIds(meta.id);
|
|
1188
|
+
const index = siblings.findIndex((id) => Object.is(id, meta.id));
|
|
1189
|
+
const targetId = siblings[index + (event.key === 'ArrowUp' ? -1 : 1)];
|
|
1190
|
+
if (targetId !== undefined) this.moveRow(meta.id, targetId, event.key === 'ArrowUp' ? 'before' : 'after');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** @param {unknown} id @returns {unknown[]} */
|
|
1194
|
+
_siblingIds(id) {
|
|
1195
|
+
const row = this.getRow(id);
|
|
1196
|
+
if (!row) return [];
|
|
1197
|
+
const parent = this._hierarchy ? readRowValue(this._hierarchy.parentId, row) : null;
|
|
1198
|
+
return this._data.filter((candidate) => !this._hierarchy
|
|
1199
|
+
|| sameHierarchyParent(readRowValue(this._hierarchy.parentId, candidate), parent))
|
|
1200
|
+
.map((candidate) => this._idFor(candidate));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** @param {unknown} left @param {unknown} right @returns {boolean} */
|
|
1204
|
+
_canMoveTogether(left, right) {
|
|
1205
|
+
if (!this._hierarchy) return true;
|
|
1206
|
+
const a = this.getRow(left);
|
|
1207
|
+
const b = this.getRow(right);
|
|
1208
|
+
return Boolean(a && b && sameHierarchyParent(
|
|
1209
|
+
readRowValue(this._hierarchy.parentId, a), readRowValue(this._hierarchy.parentId, b)));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** @param {unknown} id @returns {HTMLElement|null} */
|
|
1213
|
+
_rowHandle(id) {
|
|
1214
|
+
return /** @type {HTMLElement|null} */ (this._rowElements.get(id)?.[0]?.querySelector('.zx-table__row-handle') ?? null);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** @returns {void} */
|
|
1218
|
+
_clearDropMarkers() {
|
|
1219
|
+
for (const row of this._tbody.querySelectorAll('tr[data-drop]')) delete row.dataset.drop;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** @returns {void} */
|
|
1223
|
+
_clearRowDrag() {
|
|
1224
|
+
for (const row of this._tbody.querySelectorAll('tr[data-dragging]')) delete row.dataset.dragging;
|
|
1225
|
+
this._clearDropMarkers();
|
|
1226
|
+
this._dragId = null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** @returns {string|null} */
|
|
1230
|
+
_hierarchyColumn() {
|
|
1231
|
+
if (!this._hierarchy) return null;
|
|
1232
|
+
return this._visibleColumns().some((column) => column.id === this._hierarchy.column)
|
|
1233
|
+
? this._hierarchy.column : this._visibleColumns()[0]?.id ?? null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
953
1236
|
/** @returns {void} */
|
|
954
1237
|
_renderColumns() {
|
|
955
1238
|
const fragment = document.createDocumentFragment();
|
|
1239
|
+
if (this.options.rowReorder) fragment.append(h('col', { class: 'zx-table__reorder-column' }));
|
|
956
1240
|
if (this.options.selectable === 'multi') {
|
|
957
1241
|
fragment.append(h('col', { class: 'zx-table__selection-column' }));
|
|
958
1242
|
}
|
|
959
|
-
const
|
|
1243
|
+
const columns = this._visibleColumns();
|
|
1244
|
+
const fractions = columns.map((column) => parseFraction(column.width));
|
|
960
1245
|
const total = fractions.reduce((sum, value) => sum + value, 0);
|
|
961
1246
|
this._table.dataset.layout = total > 0 ? 'fixed' : 'auto';
|
|
962
|
-
|
|
1247
|
+
columns.forEach((column, index) => {
|
|
963
1248
|
const col = h('col');
|
|
964
1249
|
if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
|
|
965
1250
|
else if (column.width && column.width !== 'auto') col.style.width = String(column.width);
|
|
@@ -972,6 +1257,9 @@ export class Table extends Component {
|
|
|
972
1257
|
_renderHeader() {
|
|
973
1258
|
const row = h('tr', { class: 'zx-table__header-row' });
|
|
974
1259
|
if (this._growStep) row.setAttribute('aria-rowindex', '1');
|
|
1260
|
+
if (this.options.rowReorder) row.append(h('th', {
|
|
1261
|
+
class: 'zx-table__reorder-header', scope: 'col', ariaLabel: 'Row order'
|
|
1262
|
+
}));
|
|
975
1263
|
if (this.options.selectable === 'multi') {
|
|
976
1264
|
this._selectAll = h('input', {
|
|
977
1265
|
class: 'zx-table__checkbox zx-table__select-all',
|
|
@@ -1003,7 +1291,7 @@ export class Table extends Component {
|
|
|
1003
1291
|
}
|
|
1004
1292
|
|
|
1005
1293
|
this._headers = new Map();
|
|
1006
|
-
for (const column of this.
|
|
1294
|
+
for (const column of this._visibleColumns()) {
|
|
1007
1295
|
const th = h('th', { scope: 'col' });
|
|
1008
1296
|
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1009
1297
|
if (align) th.dataset.align = align;
|
|
@@ -1044,7 +1332,8 @@ export class Table extends Component {
|
|
|
1044
1332
|
fragment.append(h('tr', { class: 'zx-table__empty-row' },
|
|
1045
1333
|
h('td', {
|
|
1046
1334
|
class: 'zx-table__empty',
|
|
1047
|
-
colspan: this.
|
|
1335
|
+
colspan: this._visibleColumns().length + (this.options.selectable === 'multi' ? 1 : 0)
|
|
1336
|
+
+ (this.options.rowReorder ? 1 : 0)
|
|
1048
1337
|
}, this._emptyContent())
|
|
1049
1338
|
));
|
|
1050
1339
|
} else {
|
|
@@ -1054,6 +1343,8 @@ export class Table extends Component {
|
|
|
1054
1343
|
});
|
|
1055
1344
|
}
|
|
1056
1345
|
this._tbody.replaceChildren(fragment);
|
|
1346
|
+
this._table.setAttribute('aria-colcount', String(this._visibleColumns().length
|
|
1347
|
+
+ (this.options.selectable === 'multi' ? 1 : 0) + (this.options.rowReorder ? 1 : 0)));
|
|
1057
1348
|
/*
|
|
1058
1349
|
* With `growing` on, the DOM holds a prefix of the data — so without this a screen reader
|
|
1059
1350
|
* announces "row 3 of 10" for a table of ten thousand. The header counts as a row.
|
|
@@ -1102,6 +1393,17 @@ export class Table extends Component {
|
|
|
1102
1393
|
elements.push(tr);
|
|
1103
1394
|
this._rowElements.set(id, elements);
|
|
1104
1395
|
|
|
1396
|
+
if (this.options.rowReorder) {
|
|
1397
|
+
const handle = h('button', {
|
|
1398
|
+
class: 'zx-table__row-handle', type: 'button', draggable: true,
|
|
1399
|
+
dataset: { rowHandle: '' }, ariaLabel: `Move row ${index + 1}`,
|
|
1400
|
+
ariaPressed: String(Object.is(this._grabbedId, id)),
|
|
1401
|
+
disabled: Boolean(this._sort && this.options.sortMode === 'local'),
|
|
1402
|
+
title: this._sort && this.options.sortMode === 'local' ? 'Clear sorting to reorder rows' : null
|
|
1403
|
+
}, icon('drag', { size: 14 }));
|
|
1404
|
+
tr.append(h('td', { class: 'zx-table__reorder-cell' }, handle));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1105
1407
|
if (this.options.selectable === 'multi') {
|
|
1106
1408
|
const checkbox = h('input', {
|
|
1107
1409
|
class: 'zx-table__checkbox zx-table__row-checkbox',
|
|
@@ -1112,7 +1414,7 @@ export class Table extends Component {
|
|
|
1112
1414
|
tr.append(h('td', { class: 'zx-table__selection-cell' }, checkbox));
|
|
1113
1415
|
}
|
|
1114
1416
|
|
|
1115
|
-
this.
|
|
1417
|
+
this._visibleColumns().forEach((column) => {
|
|
1116
1418
|
const cell = /** @type {HTMLTableCellElement} */ (h('td'));
|
|
1117
1419
|
const align = column.align ?? (isNumericColumn(column) ? 'end' : null);
|
|
1118
1420
|
if (align) cell.dataset.align = align;
|
|
@@ -1149,7 +1451,7 @@ export class Table extends Component {
|
|
|
1149
1451
|
if (value && typeof value === 'object' && typeof value.nodeType === 'number') content.append(value);
|
|
1150
1452
|
else if (value != null) content.append(document.createTextNode(String(value)));
|
|
1151
1453
|
|
|
1152
|
-
if (!this._hierarchy || column.id !== this.
|
|
1454
|
+
if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
|
|
1153
1455
|
cell.replaceChildren(...content.childNodes);
|
|
1154
1456
|
return;
|
|
1155
1457
|
}
|
|
@@ -1180,6 +1482,7 @@ export class Table extends Component {
|
|
|
1180
1482
|
if (!rowElement || !this._tbody.contains(rowElement)) return;
|
|
1181
1483
|
const meta = this._rowMeta.get(rowElement);
|
|
1182
1484
|
if (!meta) return;
|
|
1485
|
+
if (target.closest('.zx-table__row-handle')) return;
|
|
1183
1486
|
// Read the row metadata first: committing re-renders the body and detaches `rowElement`.
|
|
1184
1487
|
if (this._edit) {
|
|
1185
1488
|
this.commitEdit();
|
|
@@ -1199,6 +1502,16 @@ export class Table extends Component {
|
|
|
1199
1502
|
return;
|
|
1200
1503
|
}
|
|
1201
1504
|
|
|
1505
|
+
if (isInteractiveCellContent(target)) return;
|
|
1506
|
+
|
|
1507
|
+
if (this._editMode !== false && this._editTrigger === 'single') {
|
|
1508
|
+
const cell = target?.closest('td[data-editable="true"]');
|
|
1509
|
+
if (cell && this._tbody.contains(cell)) {
|
|
1510
|
+
this.startEdit(meta.id, String(cell.dataset.column));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1202
1515
|
this.emit('rowclick', { ...meta, event });
|
|
1203
1516
|
if (this.options.selectable === 'single' && !this._selected.has(meta.id)) {
|
|
1204
1517
|
this._selected = new Set([meta.id]);
|
|
@@ -1217,7 +1530,8 @@ export class Table extends Component {
|
|
|
1217
1530
|
if (!rowElement || !this._tbody.contains(rowElement)) return;
|
|
1218
1531
|
const meta = this._rowMeta.get(rowElement);
|
|
1219
1532
|
if (!meta) return;
|
|
1220
|
-
if (
|
|
1533
|
+
if (isInteractiveCellContent(target)) return;
|
|
1534
|
+
if (this._editMode !== false && this._editTrigger === 'double') {
|
|
1221
1535
|
const cell = target?.closest('td[data-editable="true"]');
|
|
1222
1536
|
if (cell && this._tbody.contains(cell)) {
|
|
1223
1537
|
// A double-click that opens an editor is an edit gesture, not a row activation.
|
|
@@ -1542,13 +1856,14 @@ export class Table extends Component {
|
|
|
1542
1856
|
* @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
|
|
1543
1857
|
*/
|
|
1544
1858
|
_nextEditableCell(rowIndex, columnId, direction) {
|
|
1545
|
-
const
|
|
1859
|
+
const columns = this._visibleColumns();
|
|
1860
|
+
const columnCount = columns.length;
|
|
1546
1861
|
if (rowIndex < 0 || columnCount === 0) return null;
|
|
1547
1862
|
const visible = this._visibleData();
|
|
1548
1863
|
const startingId = this._idFor(this._data[rowIndex]);
|
|
1549
1864
|
let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
|
|
1550
1865
|
if (row < 0) return null;
|
|
1551
|
-
let column =
|
|
1866
|
+
let column = columns.findIndex((candidate) => candidate.id === columnId);
|
|
1552
1867
|
const limit = visible.length * columnCount + columnCount;
|
|
1553
1868
|
for (let step = 0; step < limit; step += 1) {
|
|
1554
1869
|
column += direction;
|
|
@@ -1561,7 +1876,7 @@ export class Table extends Component {
|
|
|
1561
1876
|
}
|
|
1562
1877
|
if (row < 0 || row >= visible.length) return null;
|
|
1563
1878
|
const candidateRow = visible[row];
|
|
1564
|
-
const candidateColumn =
|
|
1879
|
+
const candidateColumn = columns[column];
|
|
1565
1880
|
if (resolveEditable(candidateColumn, candidateRow) !== false) {
|
|
1566
1881
|
return {
|
|
1567
1882
|
id: this._idFor(candidateRow),
|
|
@@ -1895,6 +2210,65 @@ export function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
|
|
|
1895
2210
|
return result;
|
|
1896
2211
|
}
|
|
1897
2212
|
|
|
2213
|
+
/**
|
|
2214
|
+
* Returns a reordered copy, or null when IDs are missing or hierarchical rows are not siblings.
|
|
2215
|
+
* Moving a parent changes its sibling order; hierarchy projection keeps its descendant subtree
|
|
2216
|
+
* attached as a unit because parent references are never rewritten.
|
|
2217
|
+
* @param {TableRow[]} rows
|
|
2218
|
+
* @param {string|((row:TableRow)=>unknown)} rowId
|
|
2219
|
+
* @param {unknown} id
|
|
2220
|
+
* @param {unknown} targetId
|
|
2221
|
+
* @param {'before'|'after'} [position='before']
|
|
2222
|
+
* @param {string|((row:TableRow)=>unknown)|null} [parentId=null]
|
|
2223
|
+
* @returns {TableRow[]|null}
|
|
2224
|
+
*/
|
|
2225
|
+
export function reorderTableRows(rows, rowId, id, targetId, position = 'before', parentId = null) {
|
|
2226
|
+
if (!Array.isArray(rows) || Object.is(id, targetId)) return null;
|
|
2227
|
+
const from = rows.findIndex((row) => Object.is(readRowValue(rowId, row), id));
|
|
2228
|
+
const target = rows.findIndex((row) => Object.is(readRowValue(rowId, row), targetId));
|
|
2229
|
+
if (from < 0 || target < 0) return null;
|
|
2230
|
+
if (parentId && !sameHierarchyParent(
|
|
2231
|
+
readRowValue(parentId, rows[from]), readRowValue(parentId, rows[target]))) return null;
|
|
2232
|
+
if (parentId) {
|
|
2233
|
+
const forest = hierarchyForest(rows, rowId, parentId);
|
|
2234
|
+
const movingNode = forest.nodes[from];
|
|
2235
|
+
const targetNode = forest.nodes[target];
|
|
2236
|
+
if (!movingNode || !targetNode || movingNode.parent !== targetNode.parent) return null;
|
|
2237
|
+
|
|
2238
|
+
/** @param {TableHierarchyNode} root @returns {TableHierarchyNode[]} */
|
|
2239
|
+
const branchNodes = (root) => {
|
|
2240
|
+
const result = [];
|
|
2241
|
+
const stack = [root];
|
|
2242
|
+
while (stack.length) {
|
|
2243
|
+
const node = /** @type {TableHierarchyNode} */ (stack.pop());
|
|
2244
|
+
result.push(node);
|
|
2245
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) stack.push(node.children[index]);
|
|
2246
|
+
}
|
|
2247
|
+
return result;
|
|
2248
|
+
};
|
|
2249
|
+
const moving = new Set(branchNodes(movingNode).map((node) => node.index));
|
|
2250
|
+
const targetBranch = new Set(branchNodes(targetNode).map((node) => node.index));
|
|
2251
|
+
const block = rows.filter((_row, index) => moving.has(index));
|
|
2252
|
+
const remaining = rows.filter((_row, index) => !moving.has(index));
|
|
2253
|
+
const remainingSourceIndexes = rows.map((_row, index) => index).filter((index) => !moving.has(index));
|
|
2254
|
+
let insertion = remainingSourceIndexes.findIndex((index) => index === target);
|
|
2255
|
+
if (position === 'after') {
|
|
2256
|
+
const targetPositions = remainingSourceIndexes
|
|
2257
|
+
.map((sourceIndex, index) => targetBranch.has(sourceIndex) ? index : -1)
|
|
2258
|
+
.filter((index) => index >= 0);
|
|
2259
|
+
insertion = Math.max(...targetPositions) + 1;
|
|
2260
|
+
}
|
|
2261
|
+
if (insertion < 0) return null;
|
|
2262
|
+
remaining.splice(insertion, 0, ...block);
|
|
2263
|
+
return remaining;
|
|
2264
|
+
}
|
|
2265
|
+
const next = [...rows];
|
|
2266
|
+
const [moving] = next.splice(from, 1);
|
|
2267
|
+
const adjustedTarget = next.findIndex((row) => Object.is(readRowValue(rowId, row), targetId));
|
|
2268
|
+
next.splice(adjustedTarget + (position === 'after' ? 1 : 0), 0, moving);
|
|
2269
|
+
return next;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
1898
2272
|
/**
|
|
1899
2273
|
* Resolves the editor type of a column for one row.
|
|
1900
2274
|
*
|
|
@@ -2058,7 +2432,8 @@ function hierarchyForest(rows, rowId, parentId) {
|
|
|
2058
2432
|
/** @type {Map<TableHierarchyNode, TableHierarchyNode|null>} */
|
|
2059
2433
|
const parentOf = new Map();
|
|
2060
2434
|
for (const node of nodes) {
|
|
2061
|
-
const
|
|
2435
|
+
const parentValue = readRowValue(parentId, node.row);
|
|
2436
|
+
const parent = parentValue == null ? null : byId.get(parentValue) ?? null;
|
|
2062
2437
|
parentOf.set(node, parent === node ? null : parent);
|
|
2063
2438
|
}
|
|
2064
2439
|
|
|
@@ -2103,6 +2478,16 @@ function readRowValue(accessor, row) {
|
|
|
2103
2478
|
return typeof accessor === 'function' ? accessor(row) : row?.[accessor];
|
|
2104
2479
|
}
|
|
2105
2480
|
|
|
2481
|
+
/**
|
|
2482
|
+
* Null and undefined both identify the hierarchy root; other IDs retain exact identity.
|
|
2483
|
+
* @param {unknown} left
|
|
2484
|
+
* @param {unknown} right
|
|
2485
|
+
* @returns {boolean}
|
|
2486
|
+
*/
|
|
2487
|
+
function sameHierarchyParent(left, right) {
|
|
2488
|
+
return left == null && right == null || Object.is(left, right);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2106
2491
|
/**
|
|
2107
2492
|
* @param {unknown} option
|
|
2108
2493
|
* @param {TableColumn[]} columns
|
|
@@ -2136,6 +2521,16 @@ function isInsideEditor(target) {
|
|
|
2136
2521
|
return Boolean(target?.closest?.('.zx-table__editor'));
|
|
2137
2522
|
}
|
|
2138
2523
|
|
|
2524
|
+
/** @param {Element|null} target @returns {boolean} */
|
|
2525
|
+
function isInteractiveCellContent(target) {
|
|
2526
|
+
const cell = target?.closest?.('td[data-column]');
|
|
2527
|
+
const interactive = target?.closest?.([
|
|
2528
|
+
'a[href]', 'button', 'input', 'select', 'textarea', 'summary', '[contenteditable="true"]',
|
|
2529
|
+
'[role="button"]', '[role="link"]', '[tabindex]:not([tabindex="-1"])', '[data-zx-table-no-edit]'
|
|
2530
|
+
].join(','));
|
|
2531
|
+
return Boolean(cell && interactive && interactive !== cell && cell.contains(interactive));
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2139
2534
|
/**
|
|
2140
2535
|
* Whether a child component still owns Escape/Enter/Tab because one of its overlays is open.
|
|
2141
2536
|
* @param {Element} target Key event target.
|