@zeyos/zx 4.1.0 → 4.2.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 +61 -0
- package/DESIGN-SYSTEM.md +33 -3
- package/README.md +17 -5
- package/dist/types/components/app-icon/app-icon.d.ts +8 -1
- package/dist/types/components/aurora/aurora.d.ts +72 -0
- package/dist/types/components/calendar/calendar-model.d.ts +202 -0
- package/dist/types/components/calendar/calendar.d.ts +500 -0
- package/dist/types/components/card-view/card-view.d.ts +303 -0
- package/dist/types/components/card-view/record-card.d.ts +242 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
- package/dist/types/components/table/table.d.ts +120 -31
- package/dist/types/components/table-view/table-view.d.ts +192 -0
- package/dist/types/components/view/record-view.d.ts +464 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/zeyos/calendar.d.ts +91 -0
- package/dist/types/zeyos/icons.d.ts +19 -3
- package/dist/types/zeyos/index.d.ts +4 -0
- package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
- package/dist/types/zeyos/saved-views.d.ts +331 -0
- package/dist/types/zeyos/view.d.ts +305 -0
- package/dist/zx-zeyos.esm.js +4627 -1460
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2492 -410
- package/dist/zx.esm.js +4891 -115
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +4891 -115
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1702 -191
- package/docs/llms.md +207 -20
- package/docs/llms.txt +16 -6
- package/package.json +1 -1
- package/src/components/app-icon/app-icon.css +140 -30
- package/src/components/app-icon/app-icon.js +32 -5
- package/src/components/app-rail/app-rail.css +11 -2
- package/src/components/app-sidebar/app-sidebar.css +11 -2
- package/src/components/aurora/aurora.css +143 -0
- package/src/components/aurora/aurora.js +146 -0
- package/src/components/avatar/avatar.css +1 -1
- package/src/components/button/button.css +34 -2
- package/src/components/calendar/calendar-model.js +402 -0
- package/src/components/calendar/calendar.css +567 -0
- package/src/components/calendar/calendar.js +1524 -0
- package/src/components/card/card.css +25 -3
- package/src/components/card-view/card-view.css +389 -0
- package/src/components/card-view/card-view.js +527 -0
- package/src/components/card-view/record-card.js +392 -0
- package/src/components/context-menu/context-menu.css +2 -2
- package/src/components/date-picker/date-picker.css +15 -3
- package/src/components/datebox/date-range-box.css +15 -2
- package/src/components/datebox/datebox.css +16 -3
- package/src/components/dropdown/dropdown.css +15 -3
- package/src/components/kanban-view/kanban-view.css +216 -0
- package/src/components/kanban-view/kanban-view.js +1228 -0
- package/src/components/launcher/launcher.css +18 -5
- package/src/components/master-panel/master-panel.css +14 -3
- package/src/components/menu-button/menu-button.css +2 -2
- package/src/components/message/message.css +15 -10
- package/src/components/modal/modal.css +15 -3
- package/src/components/navigation-bar/navigation-bar.css +17 -5
- package/src/components/panel/panel.css +10 -2
- package/src/components/select/select.css +20 -4
- package/src/components/sheet/sheet.css +4 -1
- package/src/components/table/table.css +80 -4
- package/src/components/table/table.js +322 -37
- package/src/components/table-view/table-view.css +18 -0
- package/src/components/table-view/table-view.js +347 -0
- package/src/components/tag-picker/tag-picker.css +15 -3
- package/src/components/tooltip/tooltip.css +15 -3
- package/src/components/view/record-view.css +114 -0
- package/src/components/view/record-view.js +770 -0
- package/src/index.js +15 -0
- package/src/zeyos/calendar.js +115 -0
- package/src/zeyos/icons.js +15 -8
- package/src/zeyos/index.js +13 -0
- package/src/zeyos/launcher.js +2 -2
- package/src/zeyos/legacy-saved-views.js +155 -0
- package/src/zeyos/saved-views.js +815 -0
- package/src/zeyos/table.js +3 -1
- package/src/zeyos/view.js +543 -0
- package/styles/icons.css +3 -10
- package/styles/tokens/semantic.css +74 -2
- package/styles/zx.css +6 -0
|
@@ -107,6 +107,8 @@ import { sortRows } from './sort.js';
|
|
|
107
107
|
* @property {'single'|'double'} [editTrigger='double'] Gesture that starts an editable cell.
|
|
108
108
|
* @property {boolean} [rowReorder=false] Whether rows expose pointer/keyboard reorder handles.
|
|
109
109
|
* @property {boolean} [columnVisibility=false] Whether a column visibility chooser is rendered.
|
|
110
|
+
* @property {boolean} [columnReorder=false] Whether the column chooser also exposes accessible
|
|
111
|
+
* earlier/later controls. Column headers never become draggable.
|
|
110
112
|
* @property {string[]} [hiddenColumns=[]] Initially hidden column IDs; at least one stays visible.
|
|
111
113
|
* @property {false|TableHierarchyOptions} [hierarchy=false] Flat parent references projected as an
|
|
112
114
|
* expandable treegrid. Row data and mutation APIs remain flat.
|
|
@@ -126,6 +128,7 @@ import { sortRows } from './sort.js';
|
|
|
126
128
|
* @property {(event: CustomEvent<TableRowToggleDetail>) => void} [onrowtoggle]
|
|
127
129
|
* @property {(event: CustomEvent<TableRowMoveDetail>) => void} [onrowmove]
|
|
128
130
|
* @property {(event: CustomEvent<{visible:string[],hidden:string[]}>) => void} [oncolumnvisibilitychange]
|
|
131
|
+
* @property {(event: CustomEvent<{order:string[]}>) => void} [oncolumnorderchange]
|
|
129
132
|
*/
|
|
130
133
|
|
|
131
134
|
/**
|
|
@@ -136,7 +139,7 @@ import { sortRows } from './sort.js';
|
|
|
136
139
|
* @property {Event} event
|
|
137
140
|
*/
|
|
138
141
|
|
|
139
|
-
/** @typedef {{id: string, dir: TableSortDirection}} TableSortDetail */
|
|
142
|
+
/** @typedef {{id: string|null, dir: TableSortDirection|null}} TableSortDetail */
|
|
140
143
|
/** @typedef {{rows: TableRow[], ids: unknown[]}} TableSelectionDetail */
|
|
141
144
|
/** @typedef {{rows: TableRow[]}} TableDataDetail */
|
|
142
145
|
/** @typedef {{row: TableRow, id: unknown, expanded: boolean}} TableRowToggleDetail */
|
|
@@ -235,6 +238,7 @@ const FOCUSABLE = 'input, textarea, select, button, [tabindex]:not([tabindex="-1
|
|
|
235
238
|
* @fires Table#grow
|
|
236
239
|
* @fires Table#rowmove
|
|
237
240
|
* @fires Table#columnvisibilitychange
|
|
241
|
+
* @fires Table#columnorderchange
|
|
238
242
|
* @extends {Component<TableOptions>}
|
|
239
243
|
*/
|
|
240
244
|
export class Table extends Component {
|
|
@@ -260,6 +264,7 @@ export class Table extends Component {
|
|
|
260
264
|
hierarchy: false,
|
|
261
265
|
rowReorder: false,
|
|
262
266
|
columnVisibility: false,
|
|
267
|
+
columnReorder: false,
|
|
263
268
|
hiddenColumns: []
|
|
264
269
|
};
|
|
265
270
|
|
|
@@ -281,7 +286,7 @@ export class Table extends Component {
|
|
|
281
286
|
this.el = root;
|
|
282
287
|
this._createdRoot = created;
|
|
283
288
|
this._original = created ? null : snapshotTarget(root);
|
|
284
|
-
this._columns =
|
|
289
|
+
this._columns = normalizeTableColumns(this.options.columns);
|
|
285
290
|
this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
|
|
286
291
|
this._selected = new Set();
|
|
287
292
|
this._selectionAnchorId = null;
|
|
@@ -309,6 +314,8 @@ export class Table extends Component {
|
|
|
309
314
|
if (this._hiddenColumns.size >= this._columns.length && this._columns[0]) this._hiddenColumns.delete(this._columns[0].id);
|
|
310
315
|
this._dragId = null;
|
|
311
316
|
this._grabbedId = null;
|
|
317
|
+
this._columnControlIds = new Map();
|
|
318
|
+
this._columnStatus = null;
|
|
312
319
|
this._expanded = initialExpanded(this._hierarchy?.expanded);
|
|
313
320
|
|
|
314
321
|
if (this._sort && this.options.sortMode === 'local') this._sortData();
|
|
@@ -340,7 +347,8 @@ export class Table extends Component {
|
|
|
340
347
|
this._scroll.style.blockSize = typeof this.options.height === 'number' ?
|
|
341
348
|
`${this.options.height}px` : String(this.options.height);
|
|
342
349
|
}
|
|
343
|
-
this._columnControls = this.options.columnVisibility
|
|
350
|
+
this._columnControls = this.options.columnVisibility || this.options.columnReorder
|
|
351
|
+
? this._createColumnControls() : null;
|
|
344
352
|
if (this._editMode === false) {
|
|
345
353
|
root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
|
|
346
354
|
} else {
|
|
@@ -374,6 +382,9 @@ export class Table extends Component {
|
|
|
374
382
|
const input = /** @type {HTMLInputElement|null} */ (event.target);
|
|
375
383
|
if (input?.classList.contains('zx-table__column-toggle')) this.setColumnVisible(input.value, input.checked);
|
|
376
384
|
});
|
|
385
|
+
this.listen(this._columnControls, 'click', (event) => this._handleColumnControlClick(event));
|
|
386
|
+
this.listen(this._columnControls, 'keydown', (event) =>
|
|
387
|
+
this._handleColumnControlKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
377
388
|
}
|
|
378
389
|
if (this._editMode !== false) {
|
|
379
390
|
this.listen(this._tbody, 'keydown', (event) => this._handleEditKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
@@ -496,6 +507,108 @@ export class Table extends Component {
|
|
|
496
507
|
return [...this._data];
|
|
497
508
|
}
|
|
498
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Returns shallow-cloned column descriptors in their current display order.
|
|
512
|
+
* @returns {TableColumn[]} Current columns.
|
|
513
|
+
*/
|
|
514
|
+
getColumns() {
|
|
515
|
+
return this._columns.map((column) => ({ ...column }));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Replaces all column descriptors. Hidden state and the active sort follow retained stable IDs;
|
|
520
|
+
* an edit is canceled before cells are rebuilt.
|
|
521
|
+
* @param {TableColumn[]} columns New column descriptors.
|
|
522
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
523
|
+
* @returns {this}
|
|
524
|
+
* @fires Table#columnvisibilitychange
|
|
525
|
+
* @fires Table#columnorderchange
|
|
526
|
+
* @fires Table#editcancel
|
|
527
|
+
*/
|
|
528
|
+
setColumns(columns, options = {}) {
|
|
529
|
+
const next = normalizeTableColumns(columns);
|
|
530
|
+
const previousOrder = this.getColumnOrder();
|
|
531
|
+
const previousHidden = this.getHiddenColumns();
|
|
532
|
+
const previousSort = this.getSort();
|
|
533
|
+
this._abortEdit({ focus: false });
|
|
534
|
+
this._columns = next;
|
|
535
|
+
this._hiddenColumns = new Set(previousHidden.filter((id) =>
|
|
536
|
+
next.some((column) => column.id === id)));
|
|
537
|
+
if (this._hiddenColumns.size >= next.length && next[0]) this._hiddenColumns.delete(next[0].id);
|
|
538
|
+
this._sort = normalizeSort(previousSort, next);
|
|
539
|
+
this._hierarchy = normalizeHierarchy(this.options.hierarchy, next);
|
|
540
|
+
this._reconcileExpanded();
|
|
541
|
+
this._renderColumns();
|
|
542
|
+
this._renderHeader();
|
|
543
|
+
this._renderBody();
|
|
544
|
+
this._renderColumnControls();
|
|
545
|
+
if (!options.silent) {
|
|
546
|
+
if (!sameArray(previousOrder, this.getColumnOrder())) {
|
|
547
|
+
this.emit('columnorderchange', { order: this.getColumnOrder() });
|
|
548
|
+
}
|
|
549
|
+
if (!sameArray(previousHidden, this.getHiddenColumns())) {
|
|
550
|
+
this.emit('columnvisibilitychange', {
|
|
551
|
+
visible: this.getVisibleColumns(), hidden: this.getHiddenColumns()
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return this;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Returns every stable column ID in current display order. @returns {string[]} */
|
|
559
|
+
getColumnOrder() {
|
|
560
|
+
return this._columns.map((column) => column.id);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Reconciles a partial or stale preferred order with every current column.
|
|
565
|
+
* @param {string[]} order Preferred stable IDs.
|
|
566
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
567
|
+
* @returns {this}
|
|
568
|
+
* @fires Table#columnorderchange
|
|
569
|
+
* @fires Table#editcancel
|
|
570
|
+
*/
|
|
571
|
+
setColumnOrder(order, options = {}) {
|
|
572
|
+
const nextOrder = reconcileTableColumnOrder(this._columns, order);
|
|
573
|
+
if (sameArray(nextOrder, this.getColumnOrder())) return this;
|
|
574
|
+
const focus = this._activeColumnControl();
|
|
575
|
+
const byId = new Map(this._columns.map((column) => [column.id, column]));
|
|
576
|
+
this._abortEdit({ focus: false });
|
|
577
|
+
this._columns = nextOrder.map((id) => /** @type {TableColumn} */ (byId.get(id)));
|
|
578
|
+
this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
|
|
579
|
+
this._renderColumns();
|
|
580
|
+
this._renderHeader();
|
|
581
|
+
this._renderBody();
|
|
582
|
+
this._renderColumnControls(focus);
|
|
583
|
+
if (!options.silent) this.emit('columnorderchange', { order: this.getColumnOrder() });
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Moves one column to an edge of another column. Unknown IDs are a safe no-op.
|
|
589
|
+
* @param {string} id Moving stable ID.
|
|
590
|
+
* @param {string} targetId Target stable ID.
|
|
591
|
+
* @param {'before'|'after'} [position='before'] Target edge.
|
|
592
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
593
|
+
* @returns {this}
|
|
594
|
+
*/
|
|
595
|
+
moveColumn(id, targetId, position = 'before', options = {}) {
|
|
596
|
+
if (position !== 'before' && position !== 'after') {
|
|
597
|
+
throw new TypeError('Table column position must be "before" or "after"');
|
|
598
|
+
}
|
|
599
|
+
const next = moveTableColumn(this.getColumnOrder(), String(id), String(targetId), position);
|
|
600
|
+
if (sameArray(next, this.getColumnOrder())) return this;
|
|
601
|
+
this.setColumnOrder(next, options);
|
|
602
|
+
if (this._columnStatus) {
|
|
603
|
+
const column = this._columns.find((candidate) => candidate.id === String(id));
|
|
604
|
+
const index = this.getColumnOrder().indexOf(String(id));
|
|
605
|
+
if (column && index >= 0) {
|
|
606
|
+
this._columnStatus.textContent = `Moved ${column.label} to position ${index + 1} of ${this._columns.length}.`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return this;
|
|
610
|
+
}
|
|
611
|
+
|
|
499
612
|
/** Returns visible column IDs in caller order. @returns {string[]} */
|
|
500
613
|
getVisibleColumns() {
|
|
501
614
|
return this._visibleColumns().map((column) => column.id);
|
|
@@ -514,17 +627,16 @@ export class Table extends Component {
|
|
|
514
627
|
* @returns {this}
|
|
515
628
|
*/
|
|
516
629
|
setColumnVisible(id, visible = true, options = {}) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const restoreFocus =
|
|
521
|
-
|
|
522
|
-
if (!visible && !this._hiddenColumns.has(id) && this._visibleColumns().length <= 1) {
|
|
630
|
+
const columnId = String(id);
|
|
631
|
+
const column = this._columns.find((candidate) => candidate.id === columnId);
|
|
632
|
+
if (!column) throw new RangeError(`Unknown table column: ${columnId}`);
|
|
633
|
+
const restoreFocus = this._activeColumnControl();
|
|
634
|
+
if (!visible && !this._hiddenColumns.has(columnId) && this._visibleColumns().length <= 1) {
|
|
523
635
|
this._renderColumnControls(restoreFocus);
|
|
524
636
|
return this;
|
|
525
637
|
}
|
|
526
|
-
const changed = visible ? this._hiddenColumns.delete(
|
|
527
|
-
if (!visible && changed) this._hiddenColumns.add(
|
|
638
|
+
const changed = visible ? this._hiddenColumns.delete(columnId) : !this._hiddenColumns.has(columnId);
|
|
639
|
+
if (!visible && changed) this._hiddenColumns.add(columnId);
|
|
528
640
|
if (!changed) return this;
|
|
529
641
|
this._renderColumns();
|
|
530
642
|
this._renderHeader();
|
|
@@ -538,7 +650,7 @@ export class Table extends Component {
|
|
|
538
650
|
|
|
539
651
|
/** Toggles one column. @param {string} id @param {{silent?:boolean}} [options={}] @returns {this} */
|
|
540
652
|
toggleColumn(id, options = {}) {
|
|
541
|
-
return this.setColumnVisible(id, this._hiddenColumns.has(id), options);
|
|
653
|
+
return this.setColumnVisible(id, this._hiddenColumns.has(String(id)), options);
|
|
542
654
|
}
|
|
543
655
|
|
|
544
656
|
/**
|
|
@@ -619,6 +731,28 @@ export class Table extends Component {
|
|
|
619
731
|
return this;
|
|
620
732
|
}
|
|
621
733
|
|
|
734
|
+
/** Returns a defensive copy of the active sort. @returns {{id:string,dir:TableSortDirection}|null} */
|
|
735
|
+
getSort() {
|
|
736
|
+
return this._sort ? { ...this._sort } : null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Clears the active sort indicator without attempting to reconstruct the caller's original row
|
|
741
|
+
* order. The current display order becomes the unsorted order until data is replaced.
|
|
742
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
743
|
+
* @returns {this}
|
|
744
|
+
* @fires Table#sort
|
|
745
|
+
* @fires Table#editcancel
|
|
746
|
+
*/
|
|
747
|
+
clearSort(options = {}) {
|
|
748
|
+
if (!this._sort) return this;
|
|
749
|
+
this._abortEdit({ focus: false });
|
|
750
|
+
this._sort = null;
|
|
751
|
+
this._syncSortHeader();
|
|
752
|
+
if (!options.silent) this.emit('sort', { id: null, dir: null });
|
|
753
|
+
return this;
|
|
754
|
+
}
|
|
755
|
+
|
|
622
756
|
/**
|
|
623
757
|
* Returns selected rows in current display order.
|
|
624
758
|
* @returns {TableRow[]} Selected rows.
|
|
@@ -1075,6 +1209,9 @@ export class Table extends Component {
|
|
|
1075
1209
|
_createColumnControls() {
|
|
1076
1210
|
const list = h('div', { class: 'zx-table__column-list' });
|
|
1077
1211
|
this._columnList = list;
|
|
1212
|
+
this._columnStatus = h('div', {
|
|
1213
|
+
class: 'zx-table__column-status', role: 'status', ariaLive: 'polite'
|
|
1214
|
+
});
|
|
1078
1215
|
const details = h('details', { class: 'zx-table__column-controls' },
|
|
1079
1216
|
h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }), 'Columns'),
|
|
1080
1217
|
list);
|
|
@@ -1082,40 +1219,119 @@ export class Table extends Component {
|
|
|
1082
1219
|
return details;
|
|
1083
1220
|
}
|
|
1084
1221
|
|
|
1085
|
-
/**
|
|
1086
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* @param {{id:string,action:string}|null} [focusControl=null] Control that keeps focus.
|
|
1224
|
+
* @returns {void}
|
|
1225
|
+
*/
|
|
1226
|
+
_renderColumnControls(focusControl = null) {
|
|
1087
1227
|
if (!this._columnList) return;
|
|
1088
1228
|
const visible = this._visibleColumns();
|
|
1089
|
-
const
|
|
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) => {
|
|
1229
|
+
const controls = this._columns.map((column, index) => {
|
|
1102
1230
|
const checked = !this._hiddenColumns.has(column.id);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1231
|
+
let controlId = this._columnControlIds.get(column.id);
|
|
1232
|
+
if (!controlId) {
|
|
1233
|
+
controlId = uid('zx-table-column');
|
|
1234
|
+
this._columnControlIds.set(column.id, controlId);
|
|
1235
|
+
}
|
|
1236
|
+
const toggle = this.options.columnVisibility ? h('input', {
|
|
1237
|
+
id: controlId,
|
|
1238
|
+
class: 'zx-table__column-toggle', type: 'checkbox', value: column.id, checked,
|
|
1239
|
+
disabled: checked && visible.length === 1,
|
|
1240
|
+
dataset: { columnId: column.id, columnAction: 'toggle' }
|
|
1241
|
+
}) : null;
|
|
1242
|
+
const label = toggle
|
|
1243
|
+
? h('label', { for: controlId }, toggle, h('span', {}, column.label))
|
|
1244
|
+
: h('span', { class: 'zx-table__column-label' }, column.label);
|
|
1245
|
+
const order = this.options.columnReorder ? h('span', {
|
|
1246
|
+
class: 'zx-table__column-order', role: 'group', ariaLabel: `Move ${column.label}`
|
|
1247
|
+
},
|
|
1248
|
+
h('button', {
|
|
1249
|
+
class: 'zx-table__column-move', type: 'button', disabled: index === 0,
|
|
1250
|
+
ariaLabel: `Move ${column.label} earlier`,
|
|
1251
|
+
dataset: { columnId: column.id, columnAction: 'up' }
|
|
1252
|
+
}, icon('chevron-up', { size: 11 })),
|
|
1253
|
+
h('button', {
|
|
1254
|
+
class: 'zx-table__column-move', type: 'button', disabled: index === this._columns.length - 1,
|
|
1255
|
+
ariaLabel: `Move ${column.label} later`,
|
|
1256
|
+
dataset: { columnId: column.id, columnAction: 'down' }
|
|
1257
|
+
}, icon('chevron-down', { size: 11 }))) : null;
|
|
1258
|
+
return h('div', { class: 'zx-table__column-option', dataset: { columnId: column.id } }, label, order);
|
|
1259
|
+
});
|
|
1260
|
+
this._columnList.replaceChildren(...controls, this._columnStatus);
|
|
1261
|
+
if (focusControl !== null) {
|
|
1262
|
+
let control = [...this._columnList.querySelectorAll('[data-column-action]')]
|
|
1263
|
+
.find((candidate) => {
|
|
1264
|
+
const element = /** @type {HTMLElement} */ (candidate);
|
|
1265
|
+
return element.dataset.columnId === focusControl.id
|
|
1266
|
+
&& element.dataset.columnAction === focusControl.action;
|
|
1267
|
+
});
|
|
1268
|
+
if (/** @type {HTMLButtonElement|HTMLInputElement|undefined} */ (control)?.disabled) {
|
|
1269
|
+
control = [...this._columnList.querySelectorAll('.zx-table__column-move[data-column-action]')]
|
|
1270
|
+
.find((candidate) => {
|
|
1271
|
+
const element = /** @type {HTMLButtonElement|HTMLInputElement} */ (candidate);
|
|
1272
|
+
return element.dataset.columnId === focusControl.id && !element.disabled;
|
|
1273
|
+
}) ?? [...this._columnList.querySelectorAll('[data-column-action]')]
|
|
1274
|
+
.find((candidate) => {
|
|
1275
|
+
const element = /** @type {HTMLButtonElement|HTMLInputElement} */ (candidate);
|
|
1276
|
+
return element.dataset.columnId === focusControl.id && !element.disabled;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1113
1279
|
queueMicrotask(() => {
|
|
1114
1280
|
if (control?.isConnected) /** @type {HTMLElement} */ (control).focus();
|
|
1115
1281
|
});
|
|
1116
1282
|
}
|
|
1117
1283
|
}
|
|
1118
1284
|
|
|
1285
|
+
/** @returns {{id:string,action:string}|null} */
|
|
1286
|
+
_activeColumnControl() {
|
|
1287
|
+
const active = typeof document === 'undefined' ? null : /** @type {HTMLElement|null} */ (document.activeElement);
|
|
1288
|
+
if (!active || !this._columnList?.contains(active)) return null;
|
|
1289
|
+
const id = active.dataset.columnId;
|
|
1290
|
+
const action = active.dataset.columnAction;
|
|
1291
|
+
return id && action ? { id, action } : null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** @param {Event} event @returns {void} */
|
|
1295
|
+
_handleColumnControlClick(event) {
|
|
1296
|
+
const button = /** @type {Element|null} */ (event.target)?.closest?.('.zx-table__column-move');
|
|
1297
|
+
if (!button || !this._columnList?.contains(button)) return;
|
|
1298
|
+
const id = /** @type {HTMLElement} */ (button).dataset.columnId;
|
|
1299
|
+
const action = /** @type {HTMLElement} */ (button).dataset.columnAction;
|
|
1300
|
+
const index = this.getColumnOrder().indexOf(String(id));
|
|
1301
|
+
if (action === 'up' && index > 0) this.moveColumn(String(id), this._columns[index - 1].id, 'before');
|
|
1302
|
+
if (action === 'down' && index >= 0 && index < this._columns.length - 1) {
|
|
1303
|
+
this.moveColumn(String(id), this._columns[index + 1].id, 'after');
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Adds Arrow/Home/End movement to the reorder buttons; Enter and Space retain native button
|
|
1309
|
+
* activation. Focus follows the stable column id across the cancel-safe row/header rebuild.
|
|
1310
|
+
* @param {KeyboardEvent} event Keyboard event.
|
|
1311
|
+
* @returns {void}
|
|
1312
|
+
*/
|
|
1313
|
+
_handleColumnControlKeydown(event) {
|
|
1314
|
+
const control = /** @type {Element|null} */ (event.target)?.closest?.('.zx-table__column-move');
|
|
1315
|
+
if (!control || !this._columnList?.contains(control)) return;
|
|
1316
|
+
const id = String(/** @type {HTMLElement} */ (control).dataset.columnId);
|
|
1317
|
+
const order = this.getColumnOrder();
|
|
1318
|
+
const index = order.indexOf(id);
|
|
1319
|
+
if (index < 0) return;
|
|
1320
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
|
1321
|
+
event.preventDefault();
|
|
1322
|
+
if (index > 0) this.moveColumn(id, order[index - 1], 'before');
|
|
1323
|
+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
|
1324
|
+
event.preventDefault();
|
|
1325
|
+
if (index < order.length - 1) this.moveColumn(id, order[index + 1], 'after');
|
|
1326
|
+
} else if (event.key === 'Home') {
|
|
1327
|
+
event.preventDefault();
|
|
1328
|
+
if (index > 0) this.moveColumn(id, order[0], 'before');
|
|
1329
|
+
} else if (event.key === 'End') {
|
|
1330
|
+
event.preventDefault();
|
|
1331
|
+
if (index < order.length - 1) this.moveColumn(id, order.at(-1), 'after');
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1119
1335
|
/** @param {DragEvent} event @returns {void} */
|
|
1120
1336
|
_handleRowDragStart(event) {
|
|
1121
1337
|
const handle = /** @type {Element|null} */ (event.target)?.closest?.('.zx-table__row-handle');
|
|
@@ -2091,6 +2307,9 @@ export class Table extends Component {
|
|
|
2091
2307
|
/** @event Table#rowtoggle @type {CustomEvent<TableRowToggleDetail>} */
|
|
2092
2308
|
/** @event Table#stackedchange @type {CustomEvent<{stacked: boolean}>} */
|
|
2093
2309
|
/** @event Table#grow @type {CustomEvent<{rendered: number, total: number}>} */
|
|
2310
|
+
/** @event Table#rowmove @type {CustomEvent<TableRowMoveDetail>} */
|
|
2311
|
+
/** @event Table#columnvisibilitychange @type {CustomEvent<{visible:string[],hidden:string[]}>} */
|
|
2312
|
+
/** @event Table#columnorderchange @type {CustomEvent<{order:string[]}>} */
|
|
2094
2313
|
|
|
2095
2314
|
/**
|
|
2096
2315
|
* Editor opened for a cell (or, in row mode, for the row).
|
|
@@ -2541,6 +2760,67 @@ function deferToEditor(target) {
|
|
|
2541
2760
|
return target.getAttribute?.('aria-expanded') === 'true';
|
|
2542
2761
|
}
|
|
2543
2762
|
|
|
2763
|
+
/**
|
|
2764
|
+
* Validates and shallow-clones table columns so stable-id APIs never mutate caller descriptors.
|
|
2765
|
+
* @param {unknown} columns Column descriptors.
|
|
2766
|
+
* @returns {TableColumn[]} Normalized descriptors.
|
|
2767
|
+
*/
|
|
2768
|
+
export function normalizeTableColumns(columns) {
|
|
2769
|
+
if (!Array.isArray(columns)) throw new TypeError('Table columns must be an array');
|
|
2770
|
+
const seen = new Set();
|
|
2771
|
+
return columns.map((column, index) => {
|
|
2772
|
+
if (!column || typeof column !== 'object' || Array.isArray(column)) {
|
|
2773
|
+
throw new TypeError(`Table column at index ${index} must be an object`);
|
|
2774
|
+
}
|
|
2775
|
+
const id = String(column.id ?? '').trim();
|
|
2776
|
+
if (!id) throw new TypeError(`Table column at index ${index} requires an id`);
|
|
2777
|
+
if (seen.has(id)) throw new TypeError(`Duplicate table column: ${id}`);
|
|
2778
|
+
seen.add(id);
|
|
2779
|
+
return { ...column, id, label: String(column.label ?? id) };
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
/**
|
|
2784
|
+
* Reconciles saved or partial column IDs with every current column.
|
|
2785
|
+
* @param {TableColumn[]} columns Current columns.
|
|
2786
|
+
* @param {unknown} order Preferred IDs.
|
|
2787
|
+
* @returns {string[]} Complete stable order.
|
|
2788
|
+
*/
|
|
2789
|
+
export function reconcileTableColumnOrder(columns, order) {
|
|
2790
|
+
const available = columns.map((column) => column.id);
|
|
2791
|
+
const allowed = new Set(available);
|
|
2792
|
+
const preferred = Array.isArray(order) ? order.map(String) : [];
|
|
2793
|
+
const included = new Set();
|
|
2794
|
+
const result = [];
|
|
2795
|
+
for (const id of preferred) {
|
|
2796
|
+
if (allowed.has(id) && !included.has(id)) {
|
|
2797
|
+
included.add(id);
|
|
2798
|
+
result.push(id);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
for (const id of available) if (!included.has(id)) result.push(id);
|
|
2802
|
+
return result;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
/**
|
|
2806
|
+
* Moves one stable column ID without changing the supplied order.
|
|
2807
|
+
* @param {string[]} order Current IDs.
|
|
2808
|
+
* @param {string} id Moving ID.
|
|
2809
|
+
* @param {string} targetId Target ID.
|
|
2810
|
+
* @param {'before'|'after'} [position='before'] Target edge.
|
|
2811
|
+
* @returns {string[]} Reordered copy, or an unchanged copy for unknown/same IDs.
|
|
2812
|
+
*/
|
|
2813
|
+
export function moveTableColumn(order, id, targetId, position = 'before') {
|
|
2814
|
+
const next = [...order];
|
|
2815
|
+
const from = next.indexOf(id);
|
|
2816
|
+
const target = next.indexOf(targetId);
|
|
2817
|
+
if (from < 0 || target < 0 || from === target) return next;
|
|
2818
|
+
next.splice(from, 1);
|
|
2819
|
+
const insertion = next.indexOf(targetId) + (position === 'after' ? 1 : 0);
|
|
2820
|
+
next.splice(insertion, 0, id);
|
|
2821
|
+
return next;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2544
2824
|
/** @param {HTMLTableRowElement} tr @param {string} columnId @returns {HTMLTableCellElement|null} */
|
|
2545
2825
|
function cellIn(tr, columnId) {
|
|
2546
2826
|
for (const cell of tr.cells) {
|
|
@@ -2573,6 +2853,11 @@ function sameSet(left, right) {
|
|
|
2573
2853
|
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
2574
2854
|
}
|
|
2575
2855
|
|
|
2856
|
+
/** @param {unknown[]} left @param {unknown[]} right @returns {boolean} */
|
|
2857
|
+
function sameArray(left, right) {
|
|
2858
|
+
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2576
2861
|
/**
|
|
2577
2862
|
* Normalizes the `growing` option into a batch size.
|
|
2578
2863
|
* @param {unknown} growing Option value.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.zx-table-view {
|
|
2
|
+
min-inline-size: 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.zx-table-view__table {
|
|
6
|
+
min-inline-size: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.zx-table-view[data-loading="true"] .zx-table__column-controls {
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
opacity: .62;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
15
|
+
.zx-table-view .zx-table__column-controls {
|
|
16
|
+
transition: opacity var(--zx-dur-fast) var(--zx-ease);
|
|
17
|
+
}
|
|
18
|
+
}
|