flexdesk 0.10.0 → 0.11.1

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.
@@ -629,6 +629,16 @@ function compareSortKeys(a, b, asc) {
629
629
  }
630
630
  var AUTOSIZE_MAX_PX = 520;
631
631
  var VIRTUAL_FALLBACK_VIEWPORT_PX = 1e3;
632
+ var COPY_MENU_HTML = `
633
+ <div class="twm-context-menu-item" data-action="copy-tsv">
634
+ <span class="material-symbols-outlined">content_copy</span>
635
+ <span class="label">Copy Row(s)</span>
636
+ </div>
637
+ <div class="twm-context-menu-item" data-action="copy-csv">
638
+ <span class="material-symbols-outlined">table</span>
639
+ <span class="label">Copy as CSV</span>
640
+ </div>
641
+ `;
632
642
  var DataTable = class {
633
643
  /**
634
644
  * Create a DataTable instance
@@ -741,6 +751,18 @@ var DataTable = class {
741
751
  keepWhileDetached: false,
742
752
  filterKinds: null,
743
753
  onFilterChange: null,
754
+ // STORY-0010. Off — or exactly the 0.9.1 behaviour — until set. The
755
+ // typedef says what each does.
756
+ contextMenuItems: null,
757
+ onContextMenuAction: null,
758
+ onRender: null,
759
+ keyboardNav: false,
760
+ onRowKeyDown: null,
761
+ persistPage: false,
762
+ // STORY-0012. Off — every column drawn, as in 0.9.1 — until set.
763
+ hiddenColumns: null,
764
+ columnChooser: false,
765
+ onColumnsChange: null,
744
766
  ...config
745
767
  };
746
768
  this._expanded = new Set((this.config.expandedRows || []).map((k) => String(k)));
@@ -752,6 +774,8 @@ var DataTable = class {
752
774
  this._filterControls = /* @__PURE__ */ new Map();
753
775
  this._filterCounts = /* @__PURE__ */ new Map();
754
776
  this._pendingFilter = null;
777
+ this._hidden = /* @__PURE__ */ new Set();
778
+ this._setHidden(this.config.hiddenColumns);
755
779
  this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
756
780
  this._error = null;
757
781
  this._virt = null;
@@ -776,6 +800,8 @@ var DataTable = class {
776
800
  this._tableEl = null;
777
801
  this._tbodyEl = null;
778
802
  this._contextMenuEl = null;
803
+ this._contextMenuCtx = null;
804
+ this._navCursor = null;
779
805
  this._filterDropdownEl = null;
780
806
  this._filterDropdownCleanup = null;
781
807
  this._colWidths = {};
@@ -847,6 +873,10 @@ var DataTable = class {
847
873
  this._state.filters = new Map(blob.filters);
848
874
  applied = true;
849
875
  }
876
+ if (Array.isArray(blob.hidden)) {
877
+ this._setHidden(blob.hidden);
878
+ applied = true;
879
+ }
850
880
  if (blob.colWidths && typeof blob.colWidths === "object") {
851
881
  this._colWidths = {};
852
882
  for (const [k, v] of Object.entries(blob.colWidths)) {
@@ -855,12 +885,17 @@ var DataTable = class {
855
885
  this._colWidthsSig = this._colSig();
856
886
  applied = true;
857
887
  }
888
+ if (this.config.persistPage && Number.isInteger(blob.offset) && blob.offset >= 0) {
889
+ this._state.offset = blob.offset;
890
+ this._clampRestoredPage = true;
891
+ applied = true;
892
+ }
858
893
  return applied;
859
894
  }
860
895
  /** Identity of the current column set — restored widths/sort are
861
896
  * keyed by position, so a schema change invalidates them. */
862
897
  _colSig() {
863
- return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "");
898
+ return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "") + (this._hidden?.size ? `|-${this.getHiddenColumns().join(",")}` : "");
864
899
  }
865
900
  /** Snapshot the persistable slice of state to the project store.
866
901
  * No-op unless `persistKey` is set. Debounced inside the store. */
@@ -870,9 +905,135 @@ var DataTable = class {
870
905
  sortColumn: this._state.sortColumn,
871
906
  sortAscending: this._state.sortAscending,
872
907
  filters: [...this._state.filters.entries()],
873
- colWidths: { ...this._colWidths }
908
+ colWidths: { ...this._colWidths },
909
+ // STORY-0010. Only for a table that asked: one that did not writes
910
+ // the 0.9.1 blob, key for key.
911
+ ...this.config.persistPage ? { offset: this._state.offset } : {},
912
+ // STORY-0012. Only for a table that hides columns or lets them be
913
+ // hidden.
914
+ ...this._columnsOn() ? { hidden: this.getHiddenColumns() } : {}
874
915
  });
875
916
  }
917
+ // ─────────────────────────────────────────────────────────────────
918
+ // STORY-0012 — columns shown and hidden
919
+ // ─────────────────────────────────────────────────────────────────
920
+ /** Whether this table hides columns at all (and so saves the set). */
921
+ _columnsOn() {
922
+ return Array.isArray(this.config.hiddenColumns) || !!this.config.columnChooser || !!this._columnsTouched;
923
+ }
924
+ /** Replace the hidden set: whole indices of existing columns, and never
925
+ * every column — the first one stays when nothing else would. A table
926
+ * with no headers yet keeps the set for the headers `setData` brings. */
927
+ _setHidden(list) {
928
+ const n = (this.config.headers || []).length;
929
+ this._hidden = new Set((Array.isArray(list) ? list : []).map(Number).filter((c) => Number.isInteger(c) && c >= 0 && (!n || c < n)));
930
+ if (n && this._hidden.size >= n) this._hidden.delete(0);
931
+ }
932
+ _isHidden(colIdx) {
933
+ return this._hidden.has(colIdx);
934
+ }
935
+ /** The cells a drawn row has: the shown columns, and the row number. */
936
+ _domColCount() {
937
+ const n = Math.max(0, (this.config.headers?.length || 0) - this._hidden.size);
938
+ return n + (this.config.showRowNumbers ? 1 : 0);
939
+ }
940
+ /** A data column's drawn index (row number included), or -1 when hidden. */
941
+ _domIdxOf(colIdx) {
942
+ if (this._isHidden(colIdx)) return -1;
943
+ let dom = this.config.showRowNumbers ? 1 : 0;
944
+ for (const h of this._hidden) if (h < colIdx) dom--;
945
+ return dom + colIdx;
946
+ }
947
+ /** A drawn cell's data column, or null for the row number. */
948
+ _dataIdxOfDom(domIdx) {
949
+ let i = domIdx - (this.config.showRowNumbers ? 1 : 0);
950
+ if (i < 0) return null;
951
+ const n = this.config.headers?.length || 0;
952
+ for (let c = 0; c < n; c++) {
953
+ if (this._isHidden(c)) continue;
954
+ if (i === 0) return c;
955
+ i--;
956
+ }
957
+ return null;
958
+ }
959
+ /** The columns not drawn, by data index, ascending. */
960
+ getHiddenColumns() {
961
+ return [...this._hidden || []].sort((a, b) => a - b);
962
+ }
963
+ /**
964
+ * Show (`on` true) or hide a column. The last column shown is not hidden.
965
+ * The widths of the other columns stay as they were; a column shown again
966
+ * is measured. Saved under `persistKey`, and `onColumnsChange` is told.
967
+ *
968
+ * @param {number} colIdx
969
+ * @param {boolean} [on=true]
970
+ * @returns {boolean} whether the set changed
971
+ */
972
+ setColumnVisible(colIdx, on = true) {
973
+ const n = this.config.headers?.length || 0;
974
+ if (!Number.isInteger(colIdx) || colIdx < 0 || colIdx >= n) return false;
975
+ if (!!on === !this._isHidden(colIdx)) return false;
976
+ if (!on && n - this._hidden.size <= 1) return false;
977
+ this._columnsTouched = true;
978
+ const next = new Set(this._hidden);
979
+ if (on) next.delete(colIdx);
980
+ else next.add(colIdx);
981
+ this._applyHidden(next);
982
+ return true;
983
+ }
984
+ /** Swap in a new hidden set, carrying the drawn widths of the columns
985
+ * that stay drawn to their new places, and tell whoever listens. */
986
+ _applyHidden(next) {
987
+ const before = this.getHiddenColumns();
988
+ const n = this.config.headers?.length || 0;
989
+ const old = this._colWidths;
990
+ const kept = this._colWidthsSig === this._colSig();
991
+ const domBefore = [];
992
+ for (let c = 0; c < n; c++) domBefore[c] = this._domIdxOf(c);
993
+ this._setHidden([...next]);
994
+ if (kept) {
995
+ const widths = {};
996
+ if (this.config.showRowNumbers && old[0] != null) widths[0] = old[0];
997
+ for (let c = 0; c < n; c++) {
998
+ const from = domBefore[c];
999
+ const to = this._domIdxOf(c);
1000
+ if (from >= 0 && to >= 0 && old[from] != null) widths[to] = old[from];
1001
+ }
1002
+ this._colWidths = widths;
1003
+ this._colWidthsSig = this._colSig();
1004
+ }
1005
+ const after = this.getHiddenColumns();
1006
+ if (before.join() === after.join()) return;
1007
+ this._savePersisted();
1008
+ if (this._wrapperEl) this.render();
1009
+ try {
1010
+ this.config.onColumnsChange?.(after);
1011
+ } catch (err) {
1012
+ console.error("[DataTable] onColumnsChange threw", err);
1013
+ }
1014
+ }
1015
+ /** `columnChooser`: the header's right-click menu, one checkable item a
1016
+ * column. */
1017
+ _showColumnChooser(clientX, clientY) {
1018
+ const headers = this.config.headers || [];
1019
+ const lastShown = headers.length - this._hidden.size <= 1;
1020
+ const items = headers.map((h, c) => {
1021
+ const shown = !this._isHidden(c);
1022
+ return {
1023
+ label: String(h ?? ""),
1024
+ icon: shown ? "check" : "",
1025
+ checked: shown,
1026
+ disabled: shown && lastShown,
1027
+ title: shown && lastShown ? "The last column shown cannot be hidden" : "",
1028
+ onChoose: () => this.setColumnVisible(c, !shown)
1029
+ };
1030
+ });
1031
+ this._showContextMenu(clientX, clientY, { ctx: null, items, copy: false });
1032
+ }
1033
+ /** STORY-0010 `persistPage`: the client page changed, so save it. */
1034
+ _savePage() {
1035
+ if (this.config.persistPage) this._savePersisted();
1036
+ }
876
1037
  /**
877
1038
  * Update data and re-render
878
1039
  * @param {Object} updates - Partial config updates (headers, rows, etc.)
@@ -897,6 +1058,20 @@ var DataTable = class {
897
1058
  this._destroyFilterControls();
898
1059
  }
899
1060
  Object.assign(this.config, updates);
1061
+ if (Object.prototype.hasOwnProperty.call(updates, "hiddenColumns")) {
1062
+ const before = this.getHiddenColumns().join();
1063
+ this._setHidden(updates.hiddenColumns);
1064
+ if (this.getHiddenColumns().join() !== before) {
1065
+ this._savePersisted();
1066
+ try {
1067
+ this.config.onColumnsChange?.(this.getHiddenColumns());
1068
+ } catch (err) {
1069
+ console.error("[DataTable] onColumnsChange threw", err);
1070
+ }
1071
+ }
1072
+ } else if (updates.headers && this._hidden.size) {
1073
+ this._setHidden([...this._hidden]);
1074
+ }
900
1075
  if (updates.rows) {
901
1076
  this._state.selected.clear();
902
1077
  this._state.anchorIndex = null;
@@ -1060,6 +1235,7 @@ var DataTable = class {
1060
1235
  const rowCount = this._getProcessedRows().length;
1061
1236
  const maxPage = Math.ceil(rowCount / this.config.pageSize) - 1;
1062
1237
  this._state.offset = Math.max(0, Math.min(page, maxPage)) * this.config.pageSize;
1238
+ this._savePage();
1063
1239
  this.render();
1064
1240
  }
1065
1241
  /**
@@ -1074,6 +1250,7 @@ var DataTable = class {
1074
1250
  const rowCount = this._getProcessedRows().length;
1075
1251
  const idx = Math.max(0, Math.min(rowIndex, rowCount - 1));
1076
1252
  this._state.offset = Math.floor(idx / this.config.pageSize) * this.config.pageSize;
1253
+ this._savePage();
1077
1254
  this.render();
1078
1255
  }
1079
1256
  /**
@@ -1136,6 +1313,41 @@ var DataTable = class {
1136
1313
  getFilteredRowCount() {
1137
1314
  return this._getProcessedRows().length;
1138
1315
  }
1316
+ /**
1317
+ * STORY-0010. Give the table the keyboard focus (the body, where the arrow
1318
+ * keys and `onRowKeyDown` listen) without scrolling the page. False when
1319
+ * there is nothing drawn to focus.
1320
+ * @returns {boolean}
1321
+ */
1322
+ focus() {
1323
+ if (!this._tableEl) return false;
1324
+ try {
1325
+ this._tableEl.focus({ preventScroll: true });
1326
+ } catch (_) {
1327
+ return false;
1328
+ }
1329
+ return true;
1330
+ }
1331
+ /** STORY-0010 `keyboardNav`: the original indices of the page shown, in
1332
+ * the order shown — every row held, when the page is a server's or there
1333
+ * are no pages. */
1334
+ _pageOrder() {
1335
+ this._getProcessedRows();
1336
+ const order = this._processedIndexMap || [];
1337
+ if (this._isServerSide() || !this.config.pagination || this._pagingConfig()) return order;
1338
+ return order.slice(this._state.offset, this._state.offset + this.config.pageSize);
1339
+ }
1340
+ /**
1341
+ * STORY-0010. The rows the table shows, as row arrays, in the order shown:
1342
+ * filtered and sorted, across EVERY page — not only the one on screen. With
1343
+ * server-side paging (`onPageChange`, `dataSource`) they are the page the
1344
+ * consumer or the source gave, which is all the table holds. A copy:
1345
+ * changing it changes nothing in the table.
1346
+ * @returns {Array<Array>}
1347
+ */
1348
+ getDisplayedRows() {
1349
+ return this._getProcessedRows().slice();
1350
+ }
1139
1351
  /**
1140
1352
  * Render the table
1141
1353
  */
@@ -1160,6 +1372,14 @@ var DataTable = class {
1160
1372
  if (this._columnTypes.length === 0) {
1161
1373
  this._columnTypes = this._detectColumnTypes();
1162
1374
  }
1375
+ if (this._clampRestoredPage && rows.length > 0) {
1376
+ this._clampRestoredPage = false;
1377
+ if (!this._isServerSide() && !this._pagingConfig()) {
1378
+ const size = this.config.pageSize;
1379
+ const last = Math.max(0, Math.ceil(this._getProcessedRows().length / size) - 1) * size;
1380
+ this._state.offset = Math.min(Math.floor(this._state.offset / size) * size, last);
1381
+ }
1382
+ }
1163
1383
  this._wrapperEl = document.createElement("div");
1164
1384
  this._wrapperEl.className = "twm-data-table-component" + (this.config.mode === "compact" ? " twm-data-table-component--compact" : "");
1165
1385
  const fit = this._fitsContent();
@@ -1201,6 +1421,7 @@ var DataTable = class {
1201
1421
  tr.appendChild(th);
1202
1422
  }
1203
1423
  headers.forEach((h, colIdx) => {
1424
+ if (this._isHidden(colIdx)) return;
1204
1425
  const th = document.createElement("th");
1205
1426
  th.className = this._columnTypes[colIdx] || "text";
1206
1427
  th.textContent = h;
@@ -1215,7 +1436,7 @@ var DataTable = class {
1215
1436
  const bodyTable = document.createElement("table");
1216
1437
  bodyTable.className = headerTable.className;
1217
1438
  const tbody = document.createElement("tbody");
1218
- tbody.appendChild(this._buildEmptyRow(headers.length + (showRowNumbers ? 1 : 0), emptyMessage));
1439
+ tbody.appendChild(this._buildEmptyRow(this._domColCount(), emptyMessage));
1219
1440
  bodyTable.appendChild(tbody);
1220
1441
  tableWrap.appendChild(bodyTable);
1221
1442
  this._wrapperEl.appendChild(tableWrap);
@@ -1240,13 +1461,22 @@ var DataTable = class {
1240
1461
  this._headerTableEl = null;
1241
1462
  }
1242
1463
  if (keepFilterRow && this._tbodyEl) {
1243
- const n = this.config.headers.length + (this.config.showRowNumbers ? 1 : 0);
1244
- this._tbodyEl.appendChild(this._buildEmptyRow(n, emptyMessage));
1464
+ this._tbodyEl.appendChild(this._buildEmptyRow(this._domColCount(), emptyMessage));
1245
1465
  }
1246
1466
  tableWrap.appendChild(fullTable);
1247
1467
  this._wrapperEl.appendChild(tableWrap);
1248
1468
  }
1249
1469
  this._tableWrapEl = tableWrap;
1470
+ if (this.config.columnChooser && this._headerTableEl) {
1471
+ const head = this._headerTableEl;
1472
+ const onHeadMenu = (ev) => {
1473
+ ev.preventDefault();
1474
+ ev.stopPropagation();
1475
+ setTimeout(() => this._showColumnChooser(ev.clientX, ev.clientY), 0);
1476
+ };
1477
+ head.addEventListener("contextmenu", onHeadMenu);
1478
+ this._disposers.push(() => head.removeEventListener("contextmenu", onHeadMenu));
1479
+ }
1250
1480
  if (this._paginationEl && this.config.pagerPosition === "bottom") {
1251
1481
  this._paginationEl.classList.add("twm-pagination-controls--bottom");
1252
1482
  this._paginationEl.style.borderBottom = "0";
@@ -1304,7 +1534,7 @@ var DataTable = class {
1304
1534
  this._installHeaderScrollSync();
1305
1535
  if (rows.length > 0) this._installColumnResizers();
1306
1536
  }
1307
- if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function")) {
1537
+ if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function" || typeof this.config.onRowKeyDown === "function")) {
1308
1538
  this._installInteractions();
1309
1539
  }
1310
1540
  if (activeFilterColIdx !== null) {
@@ -1327,6 +1557,13 @@ var DataTable = class {
1327
1557
  }
1328
1558
  if (this._following && !this._followTimer) this._armFollow();
1329
1559
  }
1560
+ if (typeof this.config.onRender === "function") {
1561
+ try {
1562
+ this.config.onRender(this);
1563
+ } catch (err) {
1564
+ console.error("[DataTable] onRender threw", err);
1565
+ }
1566
+ }
1330
1567
  }
1331
1568
  /**
1332
1569
  * Copy selected rows to clipboard
@@ -1339,11 +1576,12 @@ var DataTable = class {
1339
1576
  return;
1340
1577
  }
1341
1578
  const { headers, rows } = this.config;
1342
- const matrix = [headers];
1579
+ const shown = (_, colIdx) => !this._isHidden(colIdx);
1580
+ const matrix = [headers.filter(shown)];
1343
1581
  for (const idx of indices) {
1344
1582
  const row = rows[idx];
1345
1583
  if (row) {
1346
- matrix.push(row.map((val, colIdx) => this._formatValue(val, colIdx)));
1584
+ matrix.push(row.map((val, colIdx) => this._formatValue(val, colIdx)).filter(shown));
1347
1585
  }
1348
1586
  }
1349
1587
  const separator = format === "csv" ? "," : " ";
@@ -1374,11 +1612,12 @@ var DataTable = class {
1374
1612
  this._notify("Download", "No data to download.", "warn");
1375
1613
  return;
1376
1614
  }
1377
- const lines = [headers.join(",")];
1615
+ const shown = (_, colIdx) => !this._isHidden(colIdx);
1616
+ const lines = [headers.filter(shown).join(",")];
1378
1617
  for (const row of rows) {
1379
1618
  lines.push(row.map(
1380
1619
  (val, colIdx) => this._csvEscape(this._formatValue(val, colIdx))
1381
- ).join(","));
1620
+ ).filter(shown).join(","));
1382
1621
  }
1383
1622
  const csv = lines.join("\n");
1384
1623
  const dialogs = this.config.host?.dialogs;
@@ -1689,6 +1928,7 @@ var DataTable = class {
1689
1928
  tr.appendChild(th);
1690
1929
  }
1691
1930
  headers.forEach((header, colIdx) => {
1931
+ if (this._isHidden(colIdx)) return;
1692
1932
  const th = document.createElement("th");
1693
1933
  th.className = "data-table__filter-cell";
1694
1934
  const control = this._filterControlFor(colIdx, header);
@@ -2008,7 +2248,7 @@ var DataTable = class {
2008
2248
  * rebuilt its box: the words, and the debounce that will apply them. */
2009
2249
  _resumePendingFilter({ colIdx, value }) {
2010
2250
  const filterRow = this._wrapperEl?.querySelector(".twm-data-table__filter-row");
2011
- const cell = filterRow?.children[this.config.showRowNumbers ? colIdx + 1 : colIdx];
2251
+ const cell = filterRow?.children[this._domIdxOf(colIdx)];
2012
2252
  const input = cell?.querySelector(".twm-data-table__filter-input");
2013
2253
  if (input && input.value !== value) {
2014
2254
  const focused = input.ownerDocument?.activeElement === input;
@@ -2028,12 +2268,12 @@ var DataTable = class {
2028
2268
  if (!row) return null;
2029
2269
  const cells = Array.from(row.children);
2030
2270
  const idx = cells.indexOf(cell);
2031
- return this.config.showRowNumbers ? idx - 1 : idx;
2271
+ return this._hidden.size ? this._dataIdxOfDom(idx) : this.config.showRowNumbers ? idx - 1 : idx;
2032
2272
  }
2033
2273
  _restoreFilterFocus(colIdx) {
2034
2274
  const filterRow = this._wrapperEl?.querySelector(".twm-data-table__filter-row");
2035
2275
  if (!filterRow) return;
2036
- const cellIdx = this.config.showRowNumbers ? colIdx + 1 : colIdx;
2276
+ const cellIdx = this._domIdxOf(colIdx);
2037
2277
  const cell = filterRow.children[cellIdx];
2038
2278
  const input = cell?.querySelector(".twm-data-table__filter-input");
2039
2279
  if (input) {
@@ -2146,6 +2386,7 @@ var DataTable = class {
2146
2386
  this._awaitWithSpinner(onPageChange(newOffset, pageSize));
2147
2387
  } else {
2148
2388
  this._state.offset = newOffset;
2389
+ this._savePage();
2149
2390
  this.render();
2150
2391
  }
2151
2392
  };
@@ -2506,6 +2747,7 @@ var DataTable = class {
2506
2747
  headerRow.appendChild(th);
2507
2748
  }
2508
2749
  headers.forEach((header, colIdx) => {
2750
+ if (this._isHidden(colIdx)) return;
2509
2751
  const th = document.createElement("th");
2510
2752
  th.className = this._columnTypes[colIdx] || "text";
2511
2753
  if (sortable) {
@@ -2652,7 +2894,7 @@ var DataTable = class {
2652
2894
  * any width already pinned for it, so a re-render starts at the widths it
2653
2895
  * had rather than at a content-sized frame that then snaps. */
2654
2896
  _createColgroup() {
2655
- const n = (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0);
2897
+ const n = this._domColCount();
2656
2898
  const colgroup = document.createElement("colgroup");
2657
2899
  for (let i = 0; i < n; i++) {
2658
2900
  const col = document.createElement("col");
@@ -2674,7 +2916,7 @@ var DataTable = class {
2674
2916
  tr.className = `twm-dt-spacer twm-dt-spacer--${edge}`;
2675
2917
  tr.setAttribute("aria-hidden", "true");
2676
2918
  const td = document.createElement("td");
2677
- td.colSpan = Math.max(1, (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0));
2919
+ td.colSpan = Math.max(1, this._domColCount());
2678
2920
  td.style.cssText = "padding:0;border:0;height:0;min-width:0;max-width:none;";
2679
2921
  tr.appendChild(td);
2680
2922
  return tr;
@@ -3076,6 +3318,7 @@ var DataTable = class {
3076
3318
  const gap = diff?.status === "gap";
3077
3319
  const changedCells = diff?.cells ? diff.cells instanceof Set ? diff.cells : new Set(diff.cells) : null;
3078
3320
  for (let colIdx = 0; colIdx < row.length; colIdx++) {
3321
+ if (this._isHidden(colIdx)) continue;
3079
3322
  const td = document.createElement("td");
3080
3323
  td.className = this._columnTypes[colIdx] || "text";
3081
3324
  const value = row[colIdx];
@@ -3263,7 +3506,7 @@ var DataTable = class {
3263
3506
  for (const p of pieces) parent.insertBefore(p, ref);
3264
3507
  }
3265
3508
  _buildDetailPieces(tr, key) {
3266
- const n = (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0);
3509
+ const n = this._domColCount();
3267
3510
  const detailTr = document.createElement("tr");
3268
3511
  detailTr.className = "twm-dt-detail-row";
3269
3512
  const td = document.createElement("td");
@@ -3997,6 +4240,7 @@ var DataTable = class {
3997
4240
  return;
3998
4241
  }
3999
4242
  this._state.offset = newOffset;
4243
+ this._savePage();
4000
4244
  this.render();
4001
4245
  };
4002
4246
  const el = document.createElement("div");
@@ -4202,7 +4446,7 @@ var DataTable = class {
4202
4446
  const v = textBox && value != null && typeof value !== "string" ? String(value) : value;
4203
4447
  if (textBox) {
4204
4448
  const filterRow = this._wrapperEl?.querySelector(".twm-data-table__filter-row");
4205
- const cell = filterRow?.children[this.config.showRowNumbers ? colIdx + 1 : colIdx];
4449
+ const cell = filterRow?.children[this._domIdxOf(colIdx)];
4206
4450
  const input = cell?.querySelector(".twm-data-table__filter-input");
4207
4451
  if (input) input.value = v || "";
4208
4452
  }
@@ -5249,6 +5493,7 @@ var DataTable = class {
5249
5493
  _installInteractions() {
5250
5494
  if (!this._tbodyEl || !this._tableEl) return;
5251
5495
  const { selectable, copyable } = this.config;
5496
+ const customMenu = this.config.contextMenuItems != null;
5252
5497
  const tbody = this._tbodyEl;
5253
5498
  const table = this._tableEl;
5254
5499
  if (selectable) {
@@ -5287,6 +5532,58 @@ var DataTable = class {
5287
5532
  tbody.addEventListener("click", handleRowClick);
5288
5533
  this._disposers.push(() => tbody.removeEventListener("click", handleRowClick));
5289
5534
  }
5535
+ if (typeof this.config.onRowKeyDown === "function") {
5536
+ const onRowKey = (ev) => {
5537
+ if (ev.isComposing || isRowControl(ev.target, table)) return;
5538
+ const tr = this._currentRowEl();
5539
+ let handled = false;
5540
+ try {
5541
+ handled = this.config.onRowKeyDown(tr ? tr.__rowIndex : null, tr ? tr.__row : null, ev) === true;
5542
+ } catch (err) {
5543
+ console.error("[DataTable] onRowKeyDown threw", err);
5544
+ }
5545
+ if (handled) {
5546
+ ev.preventDefault();
5547
+ ev.stopImmediatePropagation();
5548
+ }
5549
+ };
5550
+ table.addEventListener("keydown", onRowKey);
5551
+ this._disposers.push(() => table.removeEventListener("keydown", onRowKey));
5552
+ }
5553
+ if (this.config.keyboardNav && selectable) {
5554
+ const NAV_KEYS = ["ArrowDown", "ArrowUp", "Home", "End"];
5555
+ const onNavKey = (ev) => {
5556
+ if (!NAV_KEYS.includes(ev.key) || ev.ctrlKey || ev.metaKey || ev.altKey) return;
5557
+ if (isRowControl(ev.target, table)) return;
5558
+ const order = this._pageOrder();
5559
+ if (!order.length) return;
5560
+ ev.preventDefault();
5561
+ const selected = this._state.selected;
5562
+ const cur = this._navCursor != null && selected.has(this._navCursor) ? this._navCursor : this._state.anchorIndex;
5563
+ let pos = order.indexOf(cur);
5564
+ if (ev.key === "Home") pos = 0;
5565
+ else if (ev.key === "End") pos = order.length - 1;
5566
+ else if (pos === -1) pos = ev.key === "ArrowDown" ? 0 : order.length - 1;
5567
+ else pos = Math.max(0, Math.min(order.length - 1, pos + (ev.key === "ArrowDown" ? 1 : -1)));
5568
+ const idx = order[pos];
5569
+ this._navCursor = idx;
5570
+ const anchorPos = order.indexOf(this._state.anchorIndex);
5571
+ if (ev.shiftKey && selectable !== "single" && anchorPos !== -1) {
5572
+ const [a, b] = anchorPos <= pos ? [anchorPos, pos] : [pos, anchorPos];
5573
+ selected.clear();
5574
+ for (let i = a; i <= b; i++) selected.add(order[i]);
5575
+ } else {
5576
+ selected.clear();
5577
+ selected.add(idx);
5578
+ this._state.anchorIndex = idx;
5579
+ }
5580
+ this._updateRowSelection();
5581
+ this._notifySelectionChange();
5582
+ this.scrollToRow(idx);
5583
+ };
5584
+ table.addEventListener("keydown", onNavKey);
5585
+ this._disposers.push(() => table.removeEventListener("keydown", onNavKey));
5586
+ }
5290
5587
  if (typeof this.config.onRowActivate === "function") {
5291
5588
  this._installActivation(tbody, table);
5292
5589
  }
@@ -5304,7 +5601,7 @@ var DataTable = class {
5304
5601
  table.addEventListener("keydown", onDetailKey);
5305
5602
  this._disposers.push(() => table.removeEventListener("keydown", onDetailKey));
5306
5603
  }
5307
- if (copyable) {
5604
+ if (copyable || customMenu) {
5308
5605
  const handleContextMenu = (event) => {
5309
5606
  const rowEl = event.target.closest("tr");
5310
5607
  if (!rowEl) {
@@ -5315,9 +5612,19 @@ var DataTable = class {
5315
5612
  this._hideContextMenu();
5316
5613
  return;
5317
5614
  }
5615
+ const idx = rowEl.__rowIndex;
5616
+ let custom = null;
5617
+ if (customMenu) {
5618
+ const selection = idx !== void 0 && !this._state.selected.has(idx) ? [idx] : this.getSelection();
5619
+ const ctx = this._contextMenuCtxFor(rowEl, event.target, selection);
5620
+ custom = { ctx, items: this._customMenuItems(ctx) };
5621
+ }
5622
+ if (!copyable && !custom.items.length) {
5623
+ this._hideContextMenu();
5624
+ return;
5625
+ }
5318
5626
  event.preventDefault();
5319
5627
  event.stopPropagation();
5320
- const idx = rowEl.__rowIndex;
5321
5628
  if (idx !== void 0 && !this._state.selected.has(idx)) {
5322
5629
  this._state.selected.clear();
5323
5630
  this._state.selected.add(idx);
@@ -5329,10 +5636,12 @@ var DataTable = class {
5329
5636
  table.focus({ preventScroll: true });
5330
5637
  } catch (_) {
5331
5638
  }
5332
- setTimeout(() => this._showContextMenu(event.clientX, event.clientY), 0);
5639
+ setTimeout(() => this._showContextMenu(event.clientX, event.clientY, custom), 0);
5333
5640
  };
5334
5641
  tbody.addEventListener("contextmenu", handleContextMenu);
5335
5642
  this._disposers.push(() => tbody.removeEventListener("contextmenu", handleContextMenu));
5643
+ }
5644
+ if (copyable) {
5336
5645
  const handleKeyDown = (event) => {
5337
5646
  const ctrlLike = event.ctrlKey || event.metaKey;
5338
5647
  if (ctrlLike && !event.altKey) {
@@ -5389,20 +5698,28 @@ var DataTable = class {
5389
5698
  menu.style.display = "none";
5390
5699
  menu.style.position = "fixed";
5391
5700
  menu.style.zIndex = "10001";
5392
- menu.innerHTML = `
5393
- <div class="twm-context-menu-item" data-action="copy-tsv">
5394
- <span class="material-symbols-outlined">content_copy</span>
5395
- <span class="label">Copy Row(s)</span>
5396
- </div>
5397
- <div class="twm-context-menu-item" data-action="copy-csv">
5398
- <span class="material-symbols-outlined">table</span>
5399
- <span class="label">Copy as CSV</span>
5400
- </div>
5401
- `;
5701
+ menu.innerHTML = COPY_MENU_HTML;
5402
5702
  document.body.appendChild(menu);
5403
5703
  menu.addEventListener("click", (event) => {
5404
5704
  const item = event.target.closest(".twm-context-menu-item");
5405
5705
  if (!item || item.classList.contains("disabled")) return;
5706
+ if (item.__cmItem) {
5707
+ event.stopPropagation();
5708
+ event.preventDefault();
5709
+ const ctx = this._contextMenuCtx;
5710
+ this._hideContextMenu();
5711
+ if (typeof item.__cmItem.onChoose === "function") {
5712
+ item.__cmItem.onChoose();
5713
+ return;
5714
+ }
5715
+ if (!ctx || typeof this.config.onContextMenuAction !== "function") return;
5716
+ try {
5717
+ this.config.onContextMenuAction(item.__cmItem.action, ctx.rowIdx, ctx.row, ctx);
5718
+ } catch (err) {
5719
+ console.error("[DataTable] onContextMenuAction threw", err);
5720
+ }
5721
+ return;
5722
+ }
5406
5723
  const action = item.getAttribute("data-action");
5407
5724
  if (action === "copy-tsv") this.copyToClipboard("tsv");
5408
5725
  else if (action === "copy-csv") this.copyToClipboard("csv");
@@ -5460,11 +5777,13 @@ var DataTable = class {
5460
5777
  }
5461
5778
  this._disarmContextMenuGlobals();
5462
5779
  }
5463
- _showContextMenu(clientX, clientY) {
5780
+ _showContextMenu(clientX, clientY, custom = null) {
5464
5781
  const menu = this._ensureContextMenu();
5782
+ this._drawContextMenuItems(menu, custom);
5465
5783
  this._armContextMenuGlobals();
5466
5784
  const hasSelection = this._state.selected.size > 0;
5467
5785
  menu.querySelectorAll(".twm-context-menu-item").forEach((item) => {
5786
+ if (item.__cmItem) return;
5468
5787
  item.classList.toggle("disabled", !hasSelection);
5469
5788
  item.style.pointerEvents = hasSelection ? "" : "none";
5470
5789
  });
@@ -5481,6 +5800,106 @@ var DataTable = class {
5481
5800
  menu.style.left = `${left}px`;
5482
5801
  menu.style.top = `${top}px`;
5483
5802
  }
5803
+ /**
5804
+ * STORY-0010. The `ctx` handed to `contextMenuItems` and, later, to
5805
+ * `onContextMenuAction`: the right-clicked row and cell, and `selection`
5806
+ * (original indices, ascending) with its rows 1:1.
5807
+ */
5808
+ _contextMenuCtxFor(rowEl, target, selection) {
5809
+ const rows = this.config.rows || [];
5810
+ const rowIdx = rowEl.__rowIndex === void 0 ? null : rowEl.__rowIndex;
5811
+ const row = rowIdx === null ? null : rows[rowIdx] ?? null;
5812
+ let colIdx = null;
5813
+ const cellEl = target?.closest?.("td");
5814
+ if (cellEl && cellEl.parentNode === rowEl) {
5815
+ const domIdx = Array.prototype.indexOf.call(rowEl.children, cellEl);
5816
+ const dataIdx = this._dataIdxOfDom(domIdx);
5817
+ if (dataIdx !== null) colIdx = dataIdx;
5818
+ }
5819
+ const sel = [...selection].sort((a, b) => a - b);
5820
+ return {
5821
+ rowIdx,
5822
+ row,
5823
+ colIdx,
5824
+ value: colIdx !== null && row ? row[colIdx] : null,
5825
+ selection: sel,
5826
+ selectedRows: sel.map((i) => rows[i])
5827
+ };
5828
+ }
5829
+ /** STORY-0010. `contextMenuItems` for one right-click: none off a data row
5830
+ * (the empty row), and none from a function that threw. */
5831
+ _customMenuItems(ctx) {
5832
+ if (ctx.rowIdx === null) return [];
5833
+ const spec = this.config.contextMenuItems;
5834
+ let items = spec;
5835
+ if (typeof spec === "function") {
5836
+ try {
5837
+ items = spec(ctx.row, ctx.rowIdx, ctx);
5838
+ } catch (err) {
5839
+ console.error("[DataTable] contextMenuItems threw", err);
5840
+ items = null;
5841
+ }
5842
+ }
5843
+ return Array.isArray(items) ? items.filter(Boolean) : [];
5844
+ }
5845
+ /**
5846
+ * STORY-0010. Fill the menu for this right-click: the consumer's items,
5847
+ * then (with `copyable`) a separator and the copy items. Without custom
5848
+ * items the menu is the 0.9.1 copy menu, and is only rebuilt when the last
5849
+ * right-click drew custom ones into it. Built with DOM calls, not markup:
5850
+ * the labels are the consumer's text.
5851
+ */
5852
+ _drawContextMenuItems(menu, custom) {
5853
+ this._contextMenuCtx = custom ? custom.ctx : null;
5854
+ if (!custom) {
5855
+ if (this._contextMenuCustom) {
5856
+ menu.innerHTML = COPY_MENU_HTML;
5857
+ this._contextMenuCustom = false;
5858
+ }
5859
+ return;
5860
+ }
5861
+ this._contextMenuCustom = true;
5862
+ menu.textContent = "";
5863
+ for (const item of custom.items) {
5864
+ if (item.separator) {
5865
+ const sep = document.createElement("div");
5866
+ sep.className = "twm-context-menu__separator";
5867
+ menu.appendChild(sep);
5868
+ continue;
5869
+ }
5870
+ const el = document.createElement("div");
5871
+ el.className = "twm-context-menu-item";
5872
+ if (item.action != null) el.setAttribute("data-action", String(item.action));
5873
+ if (item.danger) el.classList.add("twm-delete-node");
5874
+ if (item.disabled) {
5875
+ el.classList.add("disabled");
5876
+ el.style.pointerEvents = "none";
5877
+ el.setAttribute("aria-disabled", "true");
5878
+ }
5879
+ if (item.title) el.title = String(item.title);
5880
+ if (typeof item.checked === "boolean") {
5881
+ el.setAttribute("role", "menuitemcheckbox");
5882
+ el.setAttribute("aria-checked", String(item.checked));
5883
+ }
5884
+ const icon = document.createElement("span");
5885
+ icon.className = "material-symbols-outlined";
5886
+ icon.textContent = item.icon == null ? "" : String(item.icon);
5887
+ const label = document.createElement("span");
5888
+ label.className = "label";
5889
+ label.textContent = item.label == null ? "" : String(item.label);
5890
+ el.append(icon, label);
5891
+ el.__cmItem = item;
5892
+ menu.appendChild(el);
5893
+ }
5894
+ if (this.config.copyable && custom.copy !== false) {
5895
+ if (custom.items.length) {
5896
+ const sep = document.createElement("div");
5897
+ sep.className = "twm-context-menu__separator";
5898
+ menu.appendChild(sep);
5899
+ }
5900
+ menu.insertAdjacentHTML("beforeend", COPY_MENU_HTML);
5901
+ }
5902
+ }
5484
5903
  _teardownContextMenu() {
5485
5904
  this._contextMenuGlobals.forEach((dispose) => {
5486
5905
  try {
@@ -5556,4 +5975,4 @@ export {
5556
5975
  resolvePaging,
5557
5976
  DataTable
5558
5977
  };
5559
- //# sourceMappingURL=chunk-JLCJWCRS.js.map
5978
+ //# sourceMappingURL=chunk-6CGH73BC.js.map