@tipolox/litgrid-web 1.0.3 → 1.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.
@@ -61,6 +61,7 @@ export class DataGrid extends LitElement {
61
61
  this.scrollbarWidth = 0;
62
62
  this.columnDefs = [];
63
63
  this.sourceColumnDefs = [];
64
+ this._inferColumns = true;
64
65
  this.columnWidths = new Map();
65
66
  this.hiddenColumnKeys = new Set();
66
67
  this._columnStateStorageKey = null;
@@ -73,6 +74,7 @@ export class DataGrid extends LitElement {
73
74
  this.previousBodyCursor = '';
74
75
  this.previousBodyUserSelect = '';
75
76
  this.activeHeaderMenu = null;
77
+ this.headerMenuCapabilities = { sortable: true, filterable: true };
76
78
  this.columnChooserOpen = false;
77
79
  this.columnChooserPosition = null;
78
80
  this.quickSearchVisible = false;
@@ -99,6 +101,7 @@ export class DataGrid extends LitElement {
99
101
  this.shouldRestoreHeaderMenuFocus = false;
100
102
  this.screenReaderMessage = '';
101
103
  this.lastScreenReaderAnnouncement = '';
104
+ this.paginationFocusTarget = null;
102
105
  // Use a larger cap (2^26) to avoid hitting browser element-height limits.
103
106
  this.maxScrollableHeight = 67108864;
104
107
  this.lastTotalSize = 0;
@@ -149,22 +152,7 @@ export class DataGrid extends LitElement {
149
152
  this.requestUpdate();
150
153
  };
151
154
  this.stopColumnReorder = () => {
152
- const state = this.columnReorderState;
153
- if (!state)
154
- return;
155
- this.columnReorderState = null;
156
- window.removeEventListener('pointermove', this.handleColumnReorder);
157
- window.removeEventListener('pointerup', this.stopColumnReorder);
158
- window.removeEventListener('pointercancel', this.stopColumnReorder);
159
- this.removeColumnDragPreview();
160
- if (state.hasMoved)
161
- this.stopBodyResize();
162
- if (state.hasMoved && state.sourceIndex !== state.targetIndex) {
163
- this.moveColumn(state.columnKey, state.targetIndex);
164
- }
165
- else {
166
- this.requestUpdate();
167
- }
155
+ this.finishColumnReorder();
168
156
  };
169
157
  this.handleColumnResize = (event) => {
170
158
  if (!this.columnResizeState) {
@@ -175,14 +163,7 @@ export class DataGrid extends LitElement {
175
163
  this.requestUpdate();
176
164
  };
177
165
  this.stopColumnResize = () => {
178
- if (!this.columnResizeState) {
179
- return;
180
- }
181
- this.columnResizeState = null;
182
- this.commitColumnState('resize');
183
- this.stopBodyResize();
184
- window.removeEventListener('pointermove', this.handleColumnResize);
185
- window.removeEventListener('pointerup', this.stopColumnResize);
166
+ this.finishColumnResize();
186
167
  };
187
168
  this.handleRowResize = (event) => {
188
169
  if (!this.rowResizeState) {
@@ -243,6 +224,21 @@ export class DataGrid extends LitElement {
243
224
  this.ariaLabel = 'Data grid';
244
225
  this.ariaDescription = '';
245
226
  this.screenReaderAnnouncements = true;
227
+ this.engine.onSelectionChange(detail => {
228
+ this.requestUpdate();
229
+ this.dispatchEvent(new CustomEvent('selection-change', {
230
+ detail, bubbles: true, composed: true
231
+ }));
232
+ });
233
+ this.engine.onQueryChange(detail => {
234
+ this.quickSearchDraft = detail.query.search;
235
+ this.clearBestFitWidths();
236
+ this.resetVerticalScroll();
237
+ this.requestUpdate();
238
+ this.dispatchEvent(new CustomEvent('query-change', {
239
+ detail, bubbles: true, composed: true
240
+ }));
241
+ });
246
242
  }
247
243
  static { this.styles = [lightThemeStyles, darkThemeStyles, css `
248
244
  :host {
@@ -770,7 +766,16 @@ export class DataGrid extends LitElement {
770
766
  firstUpdated() {
771
767
  this.updateScrollbarWidth();
772
768
  }
769
+ willUpdate() {
770
+ // Reconcile after schema and persisted visibility settle, before triggers disappear.
771
+ this.reconcileHeaderMenuCapabilities();
772
+ }
773
773
  updated() {
774
+ const paginationTarget = this.paginationFocusTarget;
775
+ this.paginationFocusTarget = null;
776
+ if (paginationTarget?.isConnected && paginationTarget.disabled) {
777
+ this.renderRoot.querySelector('.pagination button:not([disabled]), .pagination select')?.focus();
778
+ }
774
779
  this.updateScrollbarWidth();
775
780
  if (this.shouldFocusViewport) {
776
781
  this.shouldFocusViewport = false;
@@ -852,7 +857,7 @@ export class DataGrid extends LitElement {
852
857
  }
853
858
  }
854
859
  getResolvedColumns() {
855
- if (this.columnDefs.length > 0) {
860
+ if (this.columnDefs.length > 0 || !this._inferColumns) {
856
861
  return this.columnDefs;
857
862
  }
858
863
  const firstRow = this.engine.getRows()[0];
@@ -1084,7 +1089,7 @@ export class DataGrid extends LitElement {
1084
1089
  return null;
1085
1090
  }
1086
1091
  }
1087
- restorePersistedColumnState() {
1092
+ restorePersistedColumnState(notify = true) {
1088
1093
  if (!this._columnStateStorageKey || this.sourceColumnDefs.length === 0)
1089
1094
  return;
1090
1095
  const storage = this.getColumnStateStorage();
@@ -1100,7 +1105,8 @@ export class DataGrid extends LitElement {
1100
1105
  this.hiddenColumnKeys = resolvedState.hiddenKeys;
1101
1106
  this.activeCell = null;
1102
1107
  this.requestUpdate();
1103
- this.dispatchColumnStateChange('restore');
1108
+ if (notify)
1109
+ this.dispatchColumnStateChange('restore');
1104
1110
  }
1105
1111
  catch {
1106
1112
  // Storage may be unavailable or blocked by browser privacy settings.
@@ -1162,6 +1168,7 @@ export class DataGrid extends LitElement {
1162
1168
  });
1163
1169
  }
1164
1170
  const isOpening = this.activeHeaderMenu !== columnKey;
1171
+ this.headerMenuCapabilities = this.getColumnCapabilities(columnKey);
1165
1172
  this.activeHeaderMenu = isOpening ? columnKey : null;
1166
1173
  this.headerMenuTrigger = isOpening && event.currentTarget instanceof HTMLButtonElement
1167
1174
  ? event.currentTarget
@@ -1169,6 +1176,19 @@ export class DataGrid extends LitElement {
1169
1176
  this.shouldFocusHeaderMenu = isOpening;
1170
1177
  this.requestUpdate();
1171
1178
  }
1179
+ getColumnCapabilities(columnKey) {
1180
+ const column = this.getResolvedColumns().find(column => column.key === columnKey);
1181
+ return { sortable: !!column && column.sortable !== false, filterable: !!column && column.filterable !== false };
1182
+ }
1183
+ reconcileHeaderMenuCapabilities() {
1184
+ if (this.activeHeaderMenu === null)
1185
+ return;
1186
+ const next = this.getColumnCapabilities(this.activeHeaderMenu);
1187
+ if (!this.isColumnVisible(this.activeHeaderMenu)
1188
+ || next.sortable !== this.headerMenuCapabilities.sortable || next.filterable !== this.headerMenuCapabilities.filterable) {
1189
+ this.closeHeaderMenu(true);
1190
+ }
1191
+ }
1172
1192
  closeHeaderMenu(restoreFocus = false) {
1173
1193
  if (this.activeHeaderMenu === null && !this.columnChooserOpen)
1174
1194
  return;
@@ -1300,6 +1320,10 @@ export class DataGrid extends LitElement {
1300
1320
  }
1301
1321
  announceRowSelection(rowIndex, intent = 'replace') {
1302
1322
  const anchor = this.engine.getSelection().anchorRowIndex ?? rowIndex;
1323
+ if (intent === 'range' && this.engine.getConfig().selection.rowKey !== undefined) {
1324
+ this.announceScreenReader(`${this.getDisplayedSelectedRowCount()} displayed rows selected.`);
1325
+ return;
1326
+ }
1303
1327
  this.announceScreenReader(intent === 'range' && anchor !== rowIndex
1304
1328
  ? `Rows ${Math.min(anchor, rowIndex) + 1} through ${Math.max(anchor, rowIndex) + 1} selected.`
1305
1329
  : `Row ${rowIndex + 1} ${this.engine.isRowSelected(rowIndex) ? 'selected' : 'deselected'}.`);
@@ -1308,24 +1332,34 @@ export class DataGrid extends LitElement {
1308
1332
  this.announceScreenReader(`${this.getColumnLabel(columnKey)}, row ${rowIndex + 1} ${this.engine.isCellSelected(rowIndex, columnKey) ? 'selected' : 'deselected'}.`);
1309
1333
  }
1310
1334
  announceVisibleRowCount(context) {
1335
+ if (this.engine.getConfig().queryMode === 'external') {
1336
+ this.announceScreenReader(`${context}. Server query requested.`);
1337
+ return;
1338
+ }
1311
1339
  const rowCount = this.engine.getRowCount();
1312
1340
  this.announceScreenReader(rowCount === 0 ? `${context}. No matching rows.` : `${context}. ${rowCount} ${rowCount === 1 ? 'row' : 'rows'} shown.`);
1313
1341
  }
1314
1342
  handleSortAscending(columnKey) {
1343
+ if (!this.getColumnCapabilities(columnKey).sortable)
1344
+ return;
1315
1345
  this.engine.sortBy(columnKey, 'asc');
1316
- this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted ascending.`);
1346
+ this.announceScreenReader(`${this.getColumnLabel(columnKey)} ${this.engine.getConfig().queryMode === 'external' ? 'ascending sort requested' : 'sorted ascending'}.`);
1317
1347
  this.requestUpdate();
1318
1348
  this.closeHeaderMenu(true);
1319
1349
  }
1320
1350
  handleSortDescending(columnKey) {
1351
+ if (!this.getColumnCapabilities(columnKey).sortable)
1352
+ return;
1321
1353
  this.engine.sortBy(columnKey, 'desc');
1322
- this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted descending.`);
1354
+ this.announceScreenReader(`${this.getColumnLabel(columnKey)} ${this.engine.getConfig().queryMode === 'external' ? 'descending sort requested' : 'sorted descending'}.`);
1323
1355
  this.requestUpdate();
1324
1356
  this.closeHeaderMenu(true);
1325
1357
  }
1326
- handleClearSort() {
1358
+ handleClearSort(columnKey) {
1359
+ if (!this.getColumnCapabilities(columnKey).sortable)
1360
+ return;
1327
1361
  this.engine.clearSort();
1328
- this.announceScreenReader('Sorting cleared.');
1362
+ this.announceScreenReader(this.engine.getConfig().queryMode === 'external' ? 'Clear sorting requested.' : 'Sorting cleared.');
1329
1363
  this.requestUpdate();
1330
1364
  this.closeHeaderMenu(true);
1331
1365
  }
@@ -1498,7 +1532,8 @@ export class DataGrid extends LitElement {
1498
1532
  this.clearQuickSearchDebounce();
1499
1533
  const threshold = Math.max(0, Math.floor(this.quickSearchDebounceThreshold));
1500
1534
  const debounceMs = Math.max(0, Math.floor(this.quickSearchDebounceMs));
1501
- if (this.sourceRowCount < threshold || debounceMs === 0) {
1535
+ const external = this.engine.getConfig().queryMode === 'external';
1536
+ if (debounceMs === 0 || (!external && this.sourceRowCount < threshold)) {
1502
1537
  this.applyQuickSearch(query);
1503
1538
  return;
1504
1539
  }
@@ -1530,23 +1565,65 @@ export class DataGrid extends LitElement {
1530
1565
  getFilters() {
1531
1566
  return this.engine.getFilters();
1532
1567
  }
1568
+ sortBy(columnKey, direction) {
1569
+ this.engine.sortBy(columnKey, direction);
1570
+ this.resetVerticalScroll();
1571
+ this.requestUpdate();
1572
+ }
1573
+ clearSort() {
1574
+ this.engine.clearSort();
1575
+ this.resetVerticalScroll();
1576
+ this.requestUpdate();
1577
+ }
1578
+ getQuery() { return this.engine.getQuery(); }
1579
+ refreshQuery() { this.engine.refreshQuery(); }
1580
+ get externalState() { return this.engine.getExternalState(); }
1581
+ set externalState(value) {
1582
+ if (value)
1583
+ this.setExternalState(value);
1584
+ }
1585
+ setExternalState(value) {
1586
+ if (!this.engine.setExternalState(value))
1587
+ return false;
1588
+ this.clearQuickSearchDebounce();
1589
+ this.quickSearchDraft = this.engine.getQuickSearch();
1590
+ this.filterDrafts = new Map(this.engine.getFilters().map(filter => [filter.columnKey, filter]));
1591
+ this.sourceRowCount = this.engine.getRowCount();
1592
+ this.clearBestFitWidths();
1593
+ this.requestUpdate();
1594
+ return true;
1595
+ }
1596
+ getPageLabel() {
1597
+ const pagination = this.engine.getPagination();
1598
+ if (this.engine.getConfig().queryMode === 'external' && pagination.totalKnown && pagination.pageIndex > 0 && pagination.pageIndex >= pagination.pageCount)
1599
+ return `Page ${pagination.pageIndex + 1} (out of range)`;
1600
+ return pagination.totalKnown === false ? `Page ${pagination.pageIndex + 1}`
1601
+ : `Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}`;
1602
+ }
1533
1603
  setPage(pageIndex) {
1604
+ this.rememberPaginationFocus();
1534
1605
  this.engine.setPage(pageIndex);
1535
1606
  this.resetVerticalScroll();
1536
1607
  const pagination = this.engine.getPagination();
1537
- this.announceScreenReader(`Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}.`);
1608
+ this.announceScreenReader(`${this.getPageLabel()}.`);
1538
1609
  this.requestUpdate();
1539
1610
  }
1540
1611
  setPageSize(pageSize) {
1612
+ this.rememberPaginationFocus();
1541
1613
  this.engine.setPageSize(pageSize);
1542
1614
  this.resetVerticalScroll();
1543
1615
  const pagination = this.engine.getPagination();
1544
- this.announceScreenReader(`${pagination.pageSize} rows per page. Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}.`);
1616
+ this.announceScreenReader(`${pagination.pageSize} rows per page. ${this.getPageLabel()}.`);
1545
1617
  this.requestUpdate();
1546
1618
  }
1547
1619
  getPagination() {
1548
1620
  return this.engine.getPagination();
1549
1621
  }
1622
+ rememberPaginationFocus() {
1623
+ const active = this.shadowRoot?.activeElement;
1624
+ if (active instanceof HTMLButtonElement && active.closest('.pagination'))
1625
+ this.paginationFocusTarget = active;
1626
+ }
1550
1627
  getTotalRowCount() {
1551
1628
  return this.engine.getTotalRowCount();
1552
1629
  }
@@ -1565,13 +1642,18 @@ export class DataGrid extends LitElement {
1565
1642
  this.announceRowSelection(rowIndex, effectiveIntent);
1566
1643
  this.requestUpdate();
1567
1644
  }
1645
+ announceAllRowsSelected() {
1646
+ const count = this.engine.getSelectableRowCount();
1647
+ const qualifier = count === this.engine.getRowCount() ? '' : 'selectable ';
1648
+ this.announceScreenReader(`All ${count} ${qualifier}displayed rows selected.`);
1649
+ }
1568
1650
  selectAllRows() {
1569
- if (this.engine.getConfig().selection.mode !== 'multi-row' || this.engine.getRowCount() === 0)
1651
+ if (this.engine.getConfig().selection.mode !== 'multi-row' || this.engine.getSelectableRowCount() === 0)
1570
1652
  return;
1571
- const unchanged = this.engine.getSelection().rowIndexes.size === this.engine.getRowCount();
1653
+ const unchanged = this.engine.getSelection().rowIndexes.size === this.engine.getSelectableRowCount();
1572
1654
  this.engine.selectAllRows();
1573
1655
  if (!unchanged)
1574
- this.announceScreenReader(`All ${this.engine.getRowCount()} displayed rows selected.`);
1656
+ this.announceAllRowsSelected();
1575
1657
  this.requestUpdate();
1576
1658
  }
1577
1659
  clearSelection() {
@@ -1585,6 +1667,21 @@ export class DataGrid extends LitElement {
1585
1667
  getSelection() {
1586
1668
  return this.engine.getSelection();
1587
1669
  }
1670
+ getSelectionSnapshot() {
1671
+ return this.engine.getSelectionSnapshot();
1672
+ }
1673
+ selectRowByKey(key, intent) {
1674
+ const previous = this.engine.getSelectionSnapshot();
1675
+ const selected = this.engine.selectRowByKey(key, intent);
1676
+ const next = this.engine.getSelectionSnapshot();
1677
+ if (selected && next !== previous) {
1678
+ const index = next.rows.find(row => row.key === key)?.rowIndex
1679
+ ?? previous.rows.find(row => row.key === key)?.rowIndex;
1680
+ if (index !== undefined)
1681
+ this.announceRowSelection(index, intent);
1682
+ }
1683
+ return selected;
1684
+ }
1588
1685
  isRowSelected(rowIndex) {
1589
1686
  return this.engine.isRowSelected(rowIndex);
1590
1687
  }
@@ -1636,6 +1733,8 @@ export class DataGrid extends LitElement {
1636
1733
  }
1637
1734
  }
1638
1735
  applyHeaderFilter(columnKey) {
1736
+ if (!this.getColumnCapabilities(columnKey).filterable)
1737
+ return;
1639
1738
  const filter = this.getFilterDraft(columnKey);
1640
1739
  this.setFilter({
1641
1740
  ...filter,
@@ -1644,6 +1743,8 @@ export class DataGrid extends LitElement {
1644
1743
  this.closeHeaderMenu(true);
1645
1744
  }
1646
1745
  clearHeaderFilter(columnKey) {
1746
+ if (!this.getColumnCapabilities(columnKey).filterable)
1747
+ return;
1647
1748
  this.clearFilter(columnKey);
1648
1749
  this.closeHeaderMenu(true);
1649
1750
  }
@@ -1743,6 +1844,24 @@ export class DataGrid extends LitElement {
1743
1844
  this.columnDragPreview?.remove();
1744
1845
  this.columnDragPreview = null;
1745
1846
  }
1847
+ finishColumnReorder(commit = true) {
1848
+ const state = this.columnReorderState;
1849
+ if (!state)
1850
+ return;
1851
+ this.columnReorderState = null;
1852
+ window.removeEventListener('pointermove', this.handleColumnReorder);
1853
+ window.removeEventListener('pointerup', this.stopColumnReorder);
1854
+ window.removeEventListener('pointercancel', this.stopColumnReorder);
1855
+ this.removeColumnDragPreview();
1856
+ if (state.hasMoved)
1857
+ this.stopBodyResize();
1858
+ if (commit && state.hasMoved && state.sourceIndex !== state.targetIndex) {
1859
+ this.moveColumn(state.columnKey, state.targetIndex);
1860
+ }
1861
+ else {
1862
+ this.requestUpdate();
1863
+ }
1864
+ }
1746
1865
  getGridCellId(rowIndex, columnIndex) {
1747
1866
  return `${this.gridId}-cell-${rowIndex}-${columnIndex}`;
1748
1867
  }
@@ -1789,6 +1908,17 @@ export class DataGrid extends LitElement {
1789
1908
  window.addEventListener('pointermove', this.handleColumnResize);
1790
1909
  window.addEventListener('pointerup', this.stopColumnResize);
1791
1910
  }
1911
+ finishColumnResize(commit = true) {
1912
+ if (!this.columnResizeState) {
1913
+ return;
1914
+ }
1915
+ this.columnResizeState = null;
1916
+ if (commit)
1917
+ this.commitColumnState('resize');
1918
+ this.stopBodyResize();
1919
+ window.removeEventListener('pointermove', this.handleColumnResize);
1920
+ window.removeEventListener('pointerup', this.stopColumnResize);
1921
+ }
1792
1922
  startRowResize(event, rowIndex) {
1793
1923
  event.preventDefault();
1794
1924
  event.stopPropagation();
@@ -2244,6 +2374,8 @@ export class DataGrid extends LitElement {
2244
2374
  `;
2245
2375
  }
2246
2376
  set data(value) {
2377
+ if (this.engine.getConfig().queryMode === 'external')
2378
+ return;
2247
2379
  this.clearQuickSearchDebounce();
2248
2380
  this.sourceRowCount = Array.isArray(value) ? value.length : 0;
2249
2381
  this.engine.setData(value ?? []);
@@ -2256,22 +2388,15 @@ export class DataGrid extends LitElement {
2256
2388
  this.selectRow(rowIndex, 'toggle');
2257
2389
  }
2258
2390
  getDisplayedSelectedRowCount() {
2259
- const rowCount = this.engine.getRowCount();
2260
- let selectedCount = 0;
2261
- for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
2262
- if (this.engine.isRowSelected(rowIndex)) {
2263
- selectedCount += 1;
2264
- }
2265
- }
2266
- return selectedCount;
2391
+ return this.engine.getSelectionSnapshot().rowIndexes.length;
2267
2392
  }
2268
2393
  areAllDisplayedRowsSelected() {
2269
- const rowCount = this.engine.getRowCount();
2394
+ const rowCount = this.engine.getSelectableRowCount();
2270
2395
  return rowCount > 0 && this.getDisplayedSelectedRowCount() === rowCount;
2271
2396
  }
2272
2397
  hasPartiallySelectedDisplayedRows() {
2273
2398
  const selectedCount = this.getDisplayedSelectedRowCount();
2274
- return selectedCount > 0 && selectedCount < this.engine.getRowCount();
2399
+ return selectedCount > 0 && selectedCount < this.engine.getSelectableRowCount();
2275
2400
  }
2276
2401
  handleSelectAllRows() {
2277
2402
  if (this.areAllDisplayedRowsSelected()) {
@@ -2280,7 +2405,80 @@ export class DataGrid extends LitElement {
2280
2405
  }
2281
2406
  else {
2282
2407
  this.engine.selectAllRows();
2283
- this.announceScreenReader(`All ${this.engine.getRowCount()} displayed rows selected.`);
2408
+ this.announceAllRowsSelected();
2409
+ }
2410
+ this.requestUpdate();
2411
+ }
2412
+ get inferColumns() {
2413
+ return this._inferColumns;
2414
+ }
2415
+ set inferColumns(value) {
2416
+ if (this._inferColumns === value)
2417
+ return;
2418
+ this._inferColumns = value;
2419
+ this.requestUpdate();
2420
+ }
2421
+ /** Reconcile an explicit schema, optionally switching its persistence namespace atomically. */
2422
+ updateColumnSchema(value, applyOrder = false, storageKey = this._columnStateStorageKey) {
2423
+ const columns = value.map(column => ({ ...column }));
2424
+ const nextByKey = new Map(columns.map(column => [column.key, column]));
2425
+ if (nextByKey.size !== columns.length)
2426
+ throw new Error('Duplicate column schema keys.');
2427
+ const previousByKey = new Map(this.sourceColumnDefs.map(column => [column.key, column]));
2428
+ const activeCell = this.activeCell;
2429
+ const activeColumnKey = activeCell
2430
+ ? this.getVisibleColumns()[activeCell.columnIndex]?.key
2431
+ : undefined;
2432
+ const nextStorageKey = typeof storageKey === 'string' && storageKey.trim() ? storageKey : null;
2433
+ const storageChanged = nextStorageKey !== this._columnStateStorageKey;
2434
+ this.finishColumnResize(false);
2435
+ this.finishColumnReorder(false);
2436
+ if (storageChanged) {
2437
+ // Install the target schema and defaults before restoring its namespace.
2438
+ // Do not reconcile/persist the outgoing view, or carry its customization
2439
+ // into an unsaved target. A namespace switch itself performs no writes.
2440
+ this._columnStateStorageKey = nextStorageKey;
2441
+ this.sourceColumnDefs = columns;
2442
+ this.columnDefs = [...columns];
2443
+ this.columnWidths.clear();
2444
+ this.hiddenColumnKeys = new Set(columns.filter(column => column.hidden).map(column => column.key));
2445
+ this.restorePersistedColumnState(false);
2446
+ }
2447
+ else {
2448
+ for (const key of this.columnWidths.keys()) {
2449
+ if (!nextByKey.has(key))
2450
+ this.columnWidths.delete(key);
2451
+ }
2452
+ for (const key of this.hiddenColumnKeys) {
2453
+ if (!nextByKey.has(key))
2454
+ this.hiddenColumnKeys.delete(key);
2455
+ }
2456
+ for (const column of columns) {
2457
+ const previous = previousByKey.get(column.key);
2458
+ if (!previous || previous.width !== column.width)
2459
+ this.columnWidths.delete(column.key);
2460
+ if (!previous || previous.hidden !== column.hidden) {
2461
+ if (column.hidden)
2462
+ this.hiddenColumnKeys.add(column.key);
2463
+ else
2464
+ this.hiddenColumnKeys.delete(column.key);
2465
+ }
2466
+ }
2467
+ // Surviving columns keep user order; newly registered columns follow them.
2468
+ // An explicit Order change reapplies the complete deterministic schema order.
2469
+ const retained = this.columnDefs.filter(column => nextByKey.has(column.key))
2470
+ .map(column => nextByKey.get(column.key));
2471
+ const retainedKeys = new Set(retained.map(column => column.key));
2472
+ this.columnDefs = applyOrder ? columns : [
2473
+ ...retained, ...columns.filter(column => !retainedKeys.has(column.key))
2474
+ ];
2475
+ this.sourceColumnDefs = columns;
2476
+ this.persistColumnState();
2477
+ }
2478
+ this.clearBestFitWidths();
2479
+ if (activeCell) {
2480
+ const columnIndex = this.getVisibleColumns().findIndex(column => column.key === activeColumnKey);
2481
+ this.activeCell = columnIndex < 0 ? null : { ...activeCell, columnIndex };
2284
2482
  }
2285
2483
  this.requestUpdate();
2286
2484
  }
@@ -2333,6 +2531,12 @@ export class DataGrid extends LitElement {
2333
2531
  render() {
2334
2532
  const count = this.engine.getRowCount();
2335
2533
  const pagination = this.engine.getPagination();
2534
+ const external = this.engine.getConfig().queryMode === 'external';
2535
+ const externalState = external ? this.engine.getExternalState() : null;
2536
+ const outOfRange = external && pagination.totalKnown && pagination.pageIndex > 0 && pagination.pageIndex >= pagination.pageCount;
2537
+ const externalStatus = externalState?.loading ? 'Loading results.' : externalState?.error
2538
+ ? externalState.error : outOfRange ? 'Requested page is out of range. Choose a previous page.'
2539
+ : count === 0 ? (pagination.totalRows === 0 ? 'No results.' : 'No rows on this page.') : '';
2336
2540
  const pageSizeOptions = [25, 50, 100].includes(pagination.pageSize)
2337
2541
  ? [25, 50, 100]
2338
2542
  : [pagination.pageSize, 25, 50, 100].sort((left, right) => left - right);
@@ -2448,14 +2652,16 @@ export class DataGrid extends LitElement {
2448
2652
  return html `
2449
2653
  <div
2450
2654
  class="shell"
2655
+ aria-busy=${external ? String(!!externalState?.loading) : nothing}
2451
2656
  style=${`--virtual-grid-template: ${gridTemplate}; --total-column-width: ${totalColumnWidth}px; --total-row-width: ${totalRowWidth}px; --row-header-width: ${rowHeaderWidth}px; --header-grid-template: ${headerGridTemplate}; --scrollbar-width: ${this.scrollbarWidth}px;`}
2452
2657
  @keydown=${this.handleShellKeyDown}
2453
2658
  >
2454
2659
  <p id=${`${this.gridId}-description`} class="screen-reader-only">${this.getScreenReaderDescription()}</p>
2660
+ ${external && externalStatus ? html `<div class="external-status" role=${externalState?.error ? 'alert' : 'status'}>${externalStatus}</div>` : nothing}
2455
2661
  ${this.screenReaderAnnouncements
2456
2662
  ? html `<div class="screen-reader-only" role="status" aria-live="polite" aria-atomic="true">${this.screenReaderMessage}</div>`
2457
2663
  : ''}
2458
- ${this.quickSearchVisible
2664
+ ${this.quickSearchVisible || external
2459
2665
  ? html `
2460
2666
  <div class="quick-search" role="search">
2461
2667
  <input
@@ -2501,7 +2707,7 @@ export class DataGrid extends LitElement {
2501
2707
  aria-label="Select all displayed rows"
2502
2708
  .checked=${this.areAllDisplayedRowsSelected()}
2503
2709
  .indeterminate=${this.hasPartiallySelectedDisplayedRows()}
2504
- ?disabled=${this.engine.getRowCount() === 0}
2710
+ ?disabled=${this.engine.getSelectableRowCount() === 0}
2505
2711
  @change=${() => this.handleSelectAllRows()}
2506
2712
  />`
2507
2713
  : html `<span>#</span>`}
@@ -2565,19 +2771,23 @@ export class DataGrid extends LitElement {
2565
2771
  ${this.quickSearchVisible ? 'Hide Quick Search' : 'Show Quick Search'}
2566
2772
  </button>
2567
2773
  <hr class="header-menu-divider" />
2568
- <button type="button" @click=${() => this.handleSortAscending(column.key)}>
2569
- Sort Asc
2570
- </button>
2571
- <button type="button" @click=${() => this.handleSortDescending(column.key)}>
2572
- Sort Desc
2573
- </button>
2574
- <button type="button" @click=${() => this.handleClearSort()}>
2575
- Clear Sort
2576
- </button>
2774
+ ${column.sortable === false ? '' : html `
2775
+ <button type="button" @click=${() => this.handleSortAscending(column.key)}>
2776
+ Sort Asc
2777
+ </button>
2778
+ <button type="button" @click=${() => this.handleSortDescending(column.key)}>
2779
+ Sort Desc
2780
+ </button>
2781
+ <button type="button" @click=${() => this.handleClearSort(column.key)}>
2782
+ Clear Sort
2783
+ </button>
2784
+ `}
2577
2785
  <button type="button" @click=${(event) => this.toggleColumnChooser(event)}>
2578
2786
  Columns
2579
2787
  </button>
2580
2788
  ${(() => {
2789
+ if (column.filterable === false)
2790
+ return '';
2581
2791
  const draft = this.getFilterDraft(column.key);
2582
2792
  const valueFree = this.isValueFreeFilter(draft.operator);
2583
2793
  return html `
@@ -2692,7 +2902,7 @@ export class DataGrid extends LitElement {
2692
2902
  ? html `
2693
2903
  <div class="pagination" role="navigation" aria-label="Pagination">
2694
2904
  <div class="pagination-summary">
2695
- <span>${pagination.totalRows} rows</span>
2905
+ <span>${pagination.totalKnown === false ? 'Total unknown' : `${pagination.totalRows} rows`}</span>
2696
2906
  <label>
2697
2907
  Rows per page
2698
2908
  <select
@@ -2708,7 +2918,7 @@ export class DataGrid extends LitElement {
2708
2918
  </label>
2709
2919
  </div>
2710
2920
  <div class="pagination-navigation">
2711
- <span>Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}</span>
2921
+ <span>${this.getPageLabel()}</span>
2712
2922
  <button
2713
2923
  type="button"
2714
2924
  ?disabled=${pagination.pageIndex === 0}
@@ -2716,7 +2926,7 @@ export class DataGrid extends LitElement {
2716
2926
  >Previous</button>
2717
2927
  <button
2718
2928
  type="button"
2719
- ?disabled=${pagination.pageCount === 0 || pagination.pageIndex >= pagination.pageCount - 1}
2929
+ ?disabled=${pagination.totalKnown === false ? !pagination.hasMore : pagination.pageCount === 0 || pagination.pageIndex >= pagination.pageCount - 1}
2720
2930
  @click=${() => this.setPage(pagination.pageIndex + 1)}
2721
2931
  >Next</button>
2722
2932
  </div>