@svgrid/grid 2.6.22 → 2.6.23

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.
Files changed (115) hide show
  1. package/dist/GridMenus.svelte +184 -6
  2. package/dist/SvAutoComplete.svelte +4 -0
  3. package/dist/SvComboBox.svelte +5 -1
  4. package/dist/SvDateRangeInput.svelte +25 -3
  5. package/dist/SvDropDownList.svelte +4 -0
  6. package/dist/SvForm.svelte +8 -8
  7. package/dist/SvGrid.controller.svelte.d.ts +48 -1
  8. package/dist/SvGrid.controller.svelte.js +214 -52
  9. package/dist/SvGrid.css +221 -0
  10. package/dist/SvGrid.helpers.d.ts +55 -0
  11. package/dist/SvGrid.helpers.js +83 -0
  12. package/dist/SvGrid.svelte +145 -0
  13. package/dist/SvGrid.types.d.ts +105 -0
  14. package/dist/SvGridCellEditor.svelte +99 -17
  15. package/dist/SvGridSelect.svelte +6 -2
  16. package/dist/SvGridSelect.svelte.d.ts +1 -1
  17. package/dist/SvListBox.svelte +8 -0
  18. package/dist/SvMultiSelect.svelte +6 -2
  19. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  20. package/dist/SvNumberInput.svelte +4 -0
  21. package/dist/SvRichCell.svelte +125 -0
  22. package/dist/SvRichCell.svelte.d.ts +15 -0
  23. package/dist/SvTextArea.svelte +5 -1
  24. package/dist/SvTreeSelect.svelte +6 -2
  25. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  26. package/dist/builtin-editors.d.ts +2 -2
  27. package/dist/builtin-editors.js +19 -3
  28. package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
  29. package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
  30. package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
  31. package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
  32. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
  33. package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
  34. package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
  35. package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
  36. package/dist/cdn/date-format-BNii4zeD.js +1393 -0
  37. package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
  38. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  39. package/dist/cdn/{src-D1lXwq1l.js → src-DGo7IHug.js} +5730 -5198
  40. package/dist/cdn/{src-C9Hihx1W.js → src-skQN5eqh.js} +7216 -6684
  41. package/dist/cdn/svgrid.js +14 -12
  42. package/dist/cdn/svgrid.svelte-external.js +14 -12
  43. package/dist/cell-values.d.ts +16 -0
  44. package/dist/cell-values.js +28 -0
  45. package/dist/clipboard.d.ts +22 -0
  46. package/dist/clipboard.js +405 -27
  47. package/dist/editing.js +12 -7
  48. package/dist/editors/cell-editors.d.ts +11 -0
  49. package/dist/editors/cell-editors.js +18 -1
  50. package/dist/grid-messages.d.ts +15 -0
  51. package/dist/grid-messages.js +15 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +4 -0
  54. package/dist/list-option.d.ts +6 -0
  55. package/dist/menus.js +40 -7
  56. package/dist/sanitize-html.d.ts +37 -0
  57. package/dist/sanitize-html.js +234 -0
  58. package/dist/selection-bar-view.svelte.d.ts +25 -0
  59. package/dist/selection-bar-view.svelte.js +13 -0
  60. package/dist/selection.d.ts +2 -1
  61. package/dist/selection.js +129 -13
  62. package/package.json +1 -1
  63. package/src/GridMenus.svelte +184 -6
  64. package/src/SvAutoComplete.svelte +4 -0
  65. package/src/SvComboBox.svelte +5 -1
  66. package/src/SvDateRangeInput.svelte +25 -3
  67. package/src/SvDropDownList.svelte +4 -0
  68. package/src/SvForm.svelte +8 -8
  69. package/src/SvGrid.controller.svelte.ts +251 -61
  70. package/src/SvGrid.css +221 -0
  71. package/src/SvGrid.helpers.ts +87 -0
  72. package/src/SvGrid.svelte +145 -0
  73. package/src/SvGrid.types.ts +112 -0
  74. package/src/SvGridCellEditor.svelte +99 -17
  75. package/src/SvGridSelect.svelte +6 -2
  76. package/src/SvListBox.svelte +8 -0
  77. package/src/SvMultiSelect.svelte +6 -2
  78. package/src/SvNumberInput.svelte +4 -0
  79. package/src/SvRichCell.svelte +125 -0
  80. package/src/SvTextArea.svelte +5 -1
  81. package/src/SvTreeSelect.svelte +6 -2
  82. package/src/builtin-editors.test.ts +18 -1
  83. package/src/builtin-editors.ts +19 -3
  84. package/src/cell-values.ts +30 -0
  85. package/src/clipboard.test.ts +93 -20
  86. package/src/clipboard.ts +433 -33
  87. package/src/date-string-column.test.ts +87 -0
  88. package/src/editing.test.ts +25 -0
  89. package/src/editing.ts +12 -9
  90. package/src/editor-block-prop.test.ts +132 -0
  91. package/src/editors/cell-editors.ts +27 -1
  92. package/src/grid-messages.ts +34 -0
  93. package/src/index.ts +13 -0
  94. package/src/list-option.ts +6 -0
  95. package/src/menus.test.ts +43 -0
  96. package/src/menus.ts +37 -7
  97. package/src/move-cells.test.ts +850 -0
  98. package/src/number-editor-literal.test.ts +84 -0
  99. package/src/number-editor.grid.test.ts +160 -0
  100. package/src/sanitize-html.test.ts +182 -0
  101. package/src/sanitize-html.ts +235 -0
  102. package/src/selection-bar-view.svelte.ts +36 -0
  103. package/src/selection.test.ts +157 -0
  104. package/src/selection.ts +131 -13
  105. package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
  106. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  107. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  108. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  109. package/src/svgrid.upsell-license.test.ts +117 -0
  110. package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
  111. package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
  112. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  113. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  114. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  115. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
package/src/SvForm.svelte CHANGED
@@ -177,7 +177,7 @@
177
177
  {#if o.type === 'textarea'}
178
178
  <textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
179
179
  {:else if o.type === 'number'}
180
- <SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
180
+ <SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
181
181
  {:else if o.type === 'password'}
182
182
  <SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
183
183
  {:else if o.type === 'mask'}
@@ -185,17 +185,17 @@
185
185
  {:else if o.type === 'phone'}
186
186
  <SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
187
187
  {:else if o.type === 'country'}
188
- <SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
188
+ <SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} block onChange={o.onChange} />
189
189
  {:else if o.type === 'select'}
190
- <SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
190
+ <SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
191
191
  {:else if o.type === 'combobox'}
192
- <SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
192
+ <SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
193
193
  {:else if o.type === 'multiselect'}
194
- <SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
194
+ <SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
195
195
  {:else if o.type === 'radio'}
196
196
  <SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
197
197
  {:else if o.type === 'tags'}
198
- <SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
198
+ <SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
199
199
  {:else if o.type === 'slider'}
200
200
  <SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
201
201
  {:else if o.type === 'checkbox'}
@@ -203,9 +203,9 @@
203
203
  {:else if o.type === 'switch'}
204
204
  <div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
205
205
  {:else if o.type === 'date'}
206
- <SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
206
+ <SvDateTimePicker id={o.id} value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder ?? 'Select date'} onChange={o.onChange} />
207
207
  {:else if o.type === 'datetime'}
208
- <SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
208
+ <SvDateTimePicker id={o.id} value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={o.onChange} />
209
209
  {:else if o.type === 'color'}
210
210
  <SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
211
211
  {:else if o.type === 'file'}
@@ -390,6 +390,29 @@ export function createSvGridController<
390
390
  targetRow: number;
391
391
  targetCol: number;
392
392
  } | null>(null);
393
+ /** An in-flight Excel-style range MOVE (drag the selection's border to
394
+ * relocate its values). Same source rectangle as `fillDrag`, plus where
395
+ * inside that rectangle the pointer grabbed it - the drop offset is
396
+ * `target - grab`, which is what lets a drag started on the range's LEFT
397
+ * edge land the range where the pointer is rather than jumping it.
398
+ * `copy` mirrors the Ctrl / Cmd key and is re-read on every move, so the
399
+ * user can change their mind mid-drag exactly as in a spreadsheet. */
400
+ let moveDrag = $state<{
401
+ sourceMinRow: number;
402
+ sourceMaxRow: number;
403
+ sourceMinCol: number;
404
+ sourceMaxCol: number;
405
+ grabRow: number;
406
+ grabCol: number;
407
+ targetRow: number;
408
+ targetCol: number;
409
+ copy: boolean;
410
+ } | null>(null);
411
+ /** True while the pointer hovers the grab strip on a selection border, so
412
+ * the root can switch to a move cursor. Kept as its own flag rather than
413
+ * recomputed per cell: it flips at most twice per border crossing and only
414
+ * one attribute on one element reads it. */
415
+ let moveGrabHover = $state(false);
393
416
  let activeAtPointerDown: { rowIndex: number; colIndex: number } | null = null;
394
417
  let editingCell = $state<CellEditState>(null);
395
418
  // Full-row editing: the row currently in whole-row edit + its per-column
@@ -626,6 +649,14 @@ export function createSvGridController<
626
649
  (props.selectionMode ?? "both") === "both"),
627
650
  );
628
651
 
652
+ // Range drag-and-drop rides on cell selection: there is no range to grab
653
+ // without it, so it can never be on alone. Default ON, matching the fill
654
+ // handle - both are spreadsheet affordances users arrive expecting, and
655
+ // neither is reachable until something is actually selected.
656
+ const moveCellsEffective = $derived(
657
+ (props.moveCells ?? true) && enableCellSelectionEffective,
658
+ );
659
+
629
660
  // The aggregate footer row. `summary` is the shortcut alias, so it wins over
630
661
  // the fine-grained prop - the same precedence `selectable` uses.
631
662
  //
@@ -1326,35 +1357,22 @@ export function createSvGridController<
1326
1357
  * compute the correct "X to Y of Z" range and total page count when
1327
1358
  * filters reduce the dataset.
1328
1359
  */
1329
- const allRowsBeforePagination = $derived.by(function allRowsBeforePagination_d() {
1330
- // Depend on dataStateVersion (row-model-affecting store changes) NOT
1331
- // gridStateVersion - so moving the active cell / selection does not force
1332
- // this O(rows) pipeline to re-run. Filter-input state (globalFilter etc.)
1333
- // and internalData are read below and tracked as their own dependencies.
1334
- dataStateVersion;
1335
- // Touch internalData + internalColumns so the row model re-derives when
1336
- // the consumer replaces the data array (e.g. via a "Reset" button).
1337
- void internalData;
1338
- void internalColumns;
1339
- const rawRows = grid.getRowModel().rows;
1340
- // External-filter mode: the consumer fetched / pre-filtered the rows
1341
- // themselves (server-side data sources). Skip every local filter pass
1342
- // so the data isn't double-filtered against the visible page.
1343
- if (externalFilterEnabled) return rawRows;
1344
-
1345
- let rows = rawRows;
1346
- if (globalFilter.trim()) {
1347
- const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
1348
- rows = rows.filter((row) =>
1349
- row
1350
- .getAllCells()
1351
- .some((cell) =>
1352
- normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
1353
- .includes(needle),
1354
- ),
1355
- );
1356
- }
1357
-
1360
+ /**
1361
+ * The per-column filter stages: the menu's (up to two) conditions, then the
1362
+ * set-filter checklists. Split out of `allRowsBeforePagination` so the facet
1363
+ * list can run the SAME pipeline with its own column left out - a checklist
1364
+ * that offers values excluded by the other columns' filters sends you to an
1365
+ * empty grid. Re-implementing the stages beside the original would have let
1366
+ * the two drift.
1367
+ *
1368
+ * `excludeColumnId` omits that column's own filters, which is what "every
1369
+ * other filter" means for the column whose menu is open.
1370
+ */
1371
+ function applyColumnFilters(
1372
+ inputRows: Array<Row<TData>>,
1373
+ excludeColumnId?: string,
1374
+ ): Array<Row<TData>> {
1375
+ let rows = inputRows;
1358
1376
  /**
1359
1377
  * Resolve a column ONCE and return a reader for it.
1360
1378
  *
@@ -1396,7 +1414,8 @@ export function createSvGridController<
1396
1414
  { locale: (props.filterLocale ?? props.localization?.locale) },
1397
1415
  );
1398
1416
  // A column filter is active if either of its (up to two) conditions is.
1399
- const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
1417
+ const menuFilters = Object.entries(filterMenuValues).filter(([id, f]) => {
1418
+ if (id === excludeColumnId) return false;
1400
1419
  const a = condActive(f.operator, f.value, f.valueTo);
1401
1420
  const b = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
1402
1421
  return a || b;
@@ -1431,7 +1450,9 @@ export function createSvGridController<
1431
1450
  );
1432
1451
  }
1433
1452
 
1434
- const valueFilterEntries = Object.entries(valueFilters);
1453
+ const valueFilterEntries = Object.entries(valueFilters).filter(
1454
+ ([id]) => id !== excludeColumnId,
1455
+ );
1435
1456
  if (valueFilterEntries.length) {
1436
1457
  // Resolve bucket defs up front so we don't re-hit the derived map
1437
1458
  // for every row × column combination. Columns without bucketing
@@ -1461,6 +1482,40 @@ export function createSvGridController<
1461
1482
  );
1462
1483
  }
1463
1484
 
1485
+ return rows;
1486
+ }
1487
+
1488
+ const allRowsBeforePagination = $derived.by(function allRowsBeforePagination_d() {
1489
+ // Depend on dataStateVersion (row-model-affecting store changes) NOT
1490
+ // gridStateVersion - so moving the active cell / selection does not force
1491
+ // this O(rows) pipeline to re-run. Filter-input state (globalFilter etc.)
1492
+ // and internalData are read below and tracked as their own dependencies.
1493
+ dataStateVersion;
1494
+ // Touch internalData + internalColumns so the row model re-derives when
1495
+ // the consumer replaces the data array (e.g. via a "Reset" button).
1496
+ void internalData;
1497
+ void internalColumns;
1498
+ const rawRows = grid.getRowModel().rows;
1499
+ // External-filter mode: the consumer fetched / pre-filtered the rows
1500
+ // themselves (server-side data sources). Skip every local filter pass
1501
+ // so the data isn't double-filtered against the visible page.
1502
+ if (externalFilterEnabled) return rawRows;
1503
+
1504
+ let rows = rawRows;
1505
+ if (globalFilter.trim()) {
1506
+ const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
1507
+ rows = rows.filter((row) =>
1508
+ row
1509
+ .getAllCells()
1510
+ .some((cell) =>
1511
+ normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
1512
+ .includes(needle),
1513
+ ),
1514
+ );
1515
+ }
1516
+
1517
+ rows = applyColumnFilters(rows);
1518
+
1464
1519
  // --- Advanced filter (Pro) ---------------------------------------------
1465
1520
  // Runs LAST for two reasons: it evaluates over the smallest row set, and
1466
1521
  // its aggregates (`SUM(amount) > 1000`) fold over what the user is
@@ -1647,6 +1702,48 @@ export function createSvGridController<
1647
1702
  return grid.getState().rowSelection ?? {};
1648
1703
  });
1649
1704
 
1705
+ // ---- Selection bar ----------------------------------------------------
1706
+ // The floating bulk-action bar. Off unless asked for: it overlaps a row, and
1707
+ // what belongs on it is app-specific. The RENDERER is Enterprise and arrives
1708
+ // through the selection-bar-view seam; everything here is free.
1709
+ const selectionBarOn = $derived(props.selectionBar !== undefined && props.selectionBar !== false);
1710
+ /** The prop in its three shapes - `true`, an action array, or a full config -
1711
+ * normalised to the config object the renderer reads. */
1712
+ const selectionBarConfig = $derived.by(function selectionBarConfig_d() {
1713
+ const raw = props.selectionBar;
1714
+ if (Array.isArray(raw)) return { actions: raw };
1715
+ if (raw && typeof raw === "object") return raw as Record<string, unknown>;
1716
+ return {};
1717
+ });
1718
+ const selectionBarActions = $derived(
1719
+ (selectionBarConfig.actions as unknown[] | undefined) ?? [],
1720
+ );
1721
+ const selectionBarPosition = $derived(
1722
+ selectionBarConfig.position === "top" ? "top" : "bottom",
1723
+ );
1724
+ const selectionBarMaxVisible = $derived(
1725
+ typeof selectionBarConfig.maxVisible === "number" && selectionBarConfig.maxVisible > 0
1726
+ ? selectionBarConfig.maxVisible
1727
+ : 6,
1728
+ );
1729
+ const selectionBarHideClear = $derived(selectionBarConfig.hideClear === true);
1730
+ /** Selected rows in DISPLAY order, plus their ids - what every action on the
1731
+ * bar receives, and what its count shows. Built from `allRows` rather than
1732
+ * the selection map so the order matches what the user is looking at. */
1733
+ const selectionBarTarget = $derived.by(function selectionBarTarget_d() {
1734
+ const rows: TData[] = [];
1735
+ const ids: string[] = [];
1736
+ if (!selectionBarOn) return { rows, ids };
1737
+ for (const row of allRows) {
1738
+ if (isGroupRow(row)) continue;
1739
+ if (rowSelectionState[row.id]) {
1740
+ rows.push(row.original as TData);
1741
+ ids.push(row.id);
1742
+ }
1743
+ }
1744
+ return { rows, ids };
1745
+ });
1746
+
1650
1747
  // Forward selection changes to the consumer. Skips the very first invocation
1651
1748
  // (the initial empty state) so consumers don't get a spurious callback on mount.
1652
1749
  let lastSelectionSerialized = "";
@@ -3297,22 +3394,60 @@ export function createSvGridController<
3297
3394
  * column id (not just the open menu) so the filter row can drive it too.
3298
3395
  */
3299
3396
  function facetValuesForColumn(columnId: string): Array<string> {
3397
+ return facetEntriesForColumn(columnId).map((entry) => entry.value);
3398
+ }
3399
+
3400
+ /**
3401
+ * The values a column's checklist offers, with the row count behind each.
3402
+ *
3403
+ * Scoped to the rows passing every OTHER column's filters, not to the whole
3404
+ * dataset. Offering the full set means a checklist full of values that
3405
+ * cannot appear: filter Country to Germany and the City list still showed
3406
+ * every city on file, most of them a one-click route to an empty grid.
3407
+ * The column's OWN filter is excluded, so unticking a value never makes the
3408
+ * rest of the list vanish underneath the pointer.
3409
+ */
3410
+ function facetEntriesForColumn(
3411
+ columnId: string,
3412
+ ): Array<{ value: string; count: number | null }> {
3300
3413
  if (!columnId) return [];
3301
- // Server-provided distinct values win (fetched + cached above).
3302
- if (props.serverFilterValues) return serverFacetValues[columnId] ?? [];
3414
+ // Server-provided distinct values win (fetched + cached above). The grid
3415
+ // has not seen the rows behind them, so there is nothing to count.
3416
+ if (props.serverFilterValues)
3417
+ return (serverFacetValues[columnId] ?? []).map((value) => ({
3418
+ value,
3419
+ count: null,
3420
+ }));
3303
3421
  const column = allColumns.find((entry) => entry.id === columnId);
3304
3422
  if (!column) return [];
3305
- // Range-bucketed facets for numeric / date columns with many values.
3423
+
3424
+ const scope = applyColumnFilters(grid.getRowModel().rows, columnId);
3306
3425
  const buckets = facetBucketsByColumn.get(columnId);
3307
- if (buckets) return buckets.map((b) => b.label);
3426
+ if (buckets) {
3427
+ // Range-bucketed facets for numeric / date columns with many values.
3428
+ const counts = new Map<string, number>(buckets.map((b) => [b.label, 0]));
3429
+ for (const row of scope) {
3430
+ const num = rawToNumber(getColumnBaseValue(row, column), buckets[0]!.isDate);
3431
+ if (!Number.isFinite(num)) continue;
3432
+ for (const bucket of buckets) {
3433
+ if (isInBucket(num, bucket)) {
3434
+ counts.set(bucket.label, (counts.get(bucket.label) ?? 0) + 1);
3435
+ break;
3436
+ }
3437
+ }
3438
+ }
3439
+ return buckets.map((b) => ({ value: b.label, count: counts.get(b.label) ?? 0 }));
3440
+ }
3441
+
3308
3442
  // Default: distinct-value facets.
3309
- const seen = new Set<string>();
3310
- for (const rowData of props.data) {
3311
- seen.add(String(getColumnAccessorValue(rowData, column) ?? ""));
3443
+ const counts = new Map<string, number>();
3444
+ for (const row of scope) {
3445
+ const value = String(getColumnBaseValue(row, column) ?? "");
3446
+ counts.set(value, (counts.get(value) ?? 0) + 1);
3312
3447
  }
3313
- return Array.from(seen).sort((a, b) =>
3314
- a.localeCompare(b, undefined, { numeric: true }),
3315
- );
3448
+ return Array.from(counts.keys())
3449
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
3450
+ .map((value) => ({ value, count: counts.get(value) ?? 0 }));
3316
3451
  }
3317
3452
 
3318
3453
  /**
@@ -3334,10 +3469,12 @@ export function createSvGridController<
3334
3469
  // out a fresh empty array/set on every read.
3335
3470
  const EMPTY_FACETS: Array<string> = [];
3336
3471
  const EMPTY_FACET_SET: ReadonlySet<string> = new Set<string>();
3472
+ const EMPTY_FACET_ENTRIES: Array<{ value: string; count: number | null }> = [];
3337
3473
  let facetCache: {
3338
3474
  columnId: string;
3339
3475
  source: Array<string> | null;
3340
3476
  values: Array<string>;
3477
+ counts: Map<string, number>;
3341
3478
  } | null = null;
3342
3479
  const columnMenuFacetValues = $derived.by(function columnMenuFacetValues_d() {
3343
3480
  // The funnel popover drives via `filterMenuFor`; the column menu's Filter
@@ -3353,10 +3490,14 @@ export function createSvGridController<
3353
3490
  if (facetCache?.columnId === columnId && facetCache.source === source) {
3354
3491
  return facetCache.values;
3355
3492
  }
3493
+ const entries = untrack(() => facetEntriesForColumn(columnId));
3356
3494
  facetCache = {
3357
3495
  columnId,
3358
3496
  source,
3359
- values: untrack(() => facetValuesForColumn(columnId)),
3497
+ values: entries.map((e) => e.value),
3498
+ counts: new Map(
3499
+ entries.flatMap((e) => (e.count == null ? [] : [[e.value, e.count] as const])),
3500
+ ),
3360
3501
  };
3361
3502
  return facetCache.values;
3362
3503
  });
@@ -3378,12 +3519,22 @@ export function createSvGridController<
3378
3519
  });
3379
3520
 
3380
3521
  /** Visible facets as listbox options ((Blanks) label for the empty value). */
3381
- const columnMenuFacetOptions = $derived(
3382
- columnMenuVisibleFacets.map((value) => ({
3383
- value,
3384
- label: value === "" ? "(Blanks)" : value,
3385
- })),
3386
- );
3522
+ const columnMenuFacetOptions = $derived.by(function columnMenuFacetOptions_d() {
3523
+ // Reading the values first materializes the snapshot (and its counts).
3524
+ const values = columnMenuVisibleFacets;
3525
+ const counts = facetCache?.counts;
3526
+ return values.map((value) => {
3527
+ const count = counts?.get(value);
3528
+ return {
3529
+ value,
3530
+ label: value === "" ? "(Blanks)" : value,
3531
+ // How many rows this value would keep, given the other columns'
3532
+ // filters. Absent in server-values mode, where the grid never sees
3533
+ // the rows behind the list.
3534
+ hint: count == null ? undefined : String(count),
3535
+ };
3536
+ });
3537
+ });
3387
3538
 
3388
3539
  /**
3389
3540
  * Checked facets for the open menu. An ABSENT `valueFilters` entry means
@@ -3396,26 +3547,45 @@ export function createSvGridController<
3396
3547
  return valueFilters[columnId] ?? new Set(columnMenuFacetValues);
3397
3548
  });
3398
3549
 
3550
+ /**
3551
+ * The offered values, in their own derived so the QUERY does not re-run the
3552
+ * scan. It used to sit in `inSuggestValues` beside the `inSuggestQuery`
3553
+ * read, so every keystroke in a chip input rebuilt the distinct-value set -
3554
+ * and once the scan started narrowing itself against the other columns'
3555
+ * filters, every keystroke ran the filter pipeline too. Split out, it
3556
+ * re-runs only when the dropdown moves to another column.
3557
+ */
3558
+ const inSuggestEntries = $derived.by(function inSuggestEntries_d() {
3559
+ const columnId = inSuggestFor;
3560
+ if (!columnId) return EMPTY_FACET_ENTRIES;
3561
+ return untrack(() => facetEntriesForColumn(columnId));
3562
+ });
3563
+
3399
3564
  // Distinct values offered in the `in` / `notIn` suggestions dropdown for the
3400
3565
  // active chip input, narrowed by whatever the user has typed. Uncapped - the
3401
3566
  // dropdown windows its rows, so a high-cardinality column costs a list of
3402
3567
  // strings, not thousands of nodes.
3403
3568
  const inSuggestValues = $derived.by(function inSuggestValues_d() {
3404
- if (!inSuggestFor) return EMPTY_FACETS;
3405
3569
  const query = inSuggestQuery.trim().toLowerCase();
3406
- const all = facetValuesForColumn(inSuggestFor);
3407
- return query
3408
- ? all.filter((value) => value.toLowerCase().includes(query))
3409
- : all;
3570
+ const all = inSuggestEntries;
3571
+ return (
3572
+ query ? all.filter((e) => e.value.toLowerCase().includes(query)) : all
3573
+ ).map((e) => e.value);
3410
3574
  });
3411
3575
 
3412
3576
  /** `in` / `notIn` suggestions as listbox options. */
3413
- const inSuggestOptions = $derived(
3414
- inSuggestValues.map((value) => ({
3415
- value,
3416
- label: value === "" ? "(Blanks)" : value,
3417
- })),
3418
- );
3577
+ const inSuggestOptions = $derived.by(function inSuggestOptions_d() {
3578
+ const query = inSuggestQuery.trim().toLowerCase();
3579
+ const shown = query
3580
+ ? inSuggestEntries.filter((e) => e.value.toLowerCase().includes(query))
3581
+ : inSuggestEntries;
3582
+ return shown.map((e) => ({
3583
+ value: e.value,
3584
+ label: e.value === "" ? "(Blanks)" : e.value,
3585
+ // Same match count the menu's checklist shows.
3586
+ hint: e.count == null ? undefined : String(e.count),
3587
+ }));
3588
+ });
3419
3589
 
3420
3590
 
3421
3591
 
@@ -3482,6 +3652,10 @@ export function createSvGridController<
3482
3652
  set isDraggingSelection(v) { isDraggingSelection = v as never; },
3483
3653
  get fillDrag() { return fillDrag; },
3484
3654
  set fillDrag(v) { fillDrag = v as never; },
3655
+ get moveDrag() { return moveDrag; },
3656
+ set moveDrag(v) { moveDrag = v as never; },
3657
+ get moveGrabHover() { return moveGrabHover; },
3658
+ set moveGrabHover(v) { moveGrabHover = v as never; },
3485
3659
  get activeAtPointerDown() { return activeAtPointerDown; },
3486
3660
  set activeAtPointerDown(v) { activeAtPointerDown = v as never; },
3487
3661
  get editingCell() { return editingCell; },
@@ -3585,6 +3759,7 @@ export function createSvGridController<
3585
3759
  get showRowSelectionEffective() { return showRowSelectionEffective; },
3586
3760
  get rowSummariesEnabled() { return rowSummariesEnabled; },
3587
3761
  get enableCellSelectionEffective() { return enableCellSelectionEffective; },
3762
+ get moveCellsEffective() { return moveCellsEffective; },
3588
3763
  get flushScheduledScrollSync() { return flushScheduledScrollSync; },
3589
3764
  get scheduleScrollSync() { return scheduleScrollSync; },
3590
3765
  get internalData() { return internalData; },
@@ -3666,6 +3841,12 @@ export function createSvGridController<
3666
3841
  get allRowsBeforePagination() { return allRowsBeforePagination; },
3667
3842
  get allRows() { return allRows; },
3668
3843
  get rowSelectionState() { return rowSelectionState; },
3844
+ get selectionBarOn() { return selectionBarOn; },
3845
+ get selectionBarActions() { return selectionBarActions; },
3846
+ get selectionBarPosition() { return selectionBarPosition; },
3847
+ get selectionBarMaxVisible() { return selectionBarMaxVisible; },
3848
+ get selectionBarHideClear() { return selectionBarHideClear; },
3849
+ get selectionBarTarget() { return selectionBarTarget; },
3669
3850
  get lastSelectionSerialized() { return lastSelectionSerialized; },
3670
3851
  set lastSelectionSerialized(v) { lastSelectionSerialized = v as never; },
3671
3852
  get lastCellRangeSerialized() { return lastCellRangeSerialized; },
@@ -3914,6 +4095,7 @@ export function createSvGridController<
3914
4095
  get getSelectionRects() { return getSelectionRects; },
3915
4096
  get fillHandleCell() { return fillHandleCell; },
3916
4097
  get isInFillPreview() { return isInFillPreview; },
4098
+ get dragPreviewRect() { return dragPreviewRect; },
3917
4099
  get fillMarqueeEdges() { return fillMarqueeEdges; },
3918
4100
  get findColumnById() { return findColumnById; },
3919
4101
  /**
@@ -3932,6 +4114,14 @@ export function createSvGridController<
3932
4114
  get startFillDrag() { return startFillDrag; },
3933
4115
  get onFillPointerMove() { return onFillPointerMove; },
3934
4116
  get onFillPointerUp() { return onFillPointerUp; },
4117
+ get isOnMoveGrabStrip() { return isOnMoveGrabStrip; },
4118
+ get startMoveDrag() { return startMoveDrag; },
4119
+ get onMovePointerMove() { return onMovePointerMove; },
4120
+ get onMovePointerUp() { return onMovePointerUp; },
4121
+ get trackEdgeScroll() { return trackEdgeScroll; },
4122
+ get stopEdgeScroll() { return stopEdgeScroll; },
4123
+ get applyMoveRange() { return applyMoveRange; },
4124
+ get moveDestRect() { return moveDestRect; },
3935
4125
  get toggleBooleanCell() { return toggleBooleanCell; },
3936
4126
  get onCellPointerDown() { return onCellPointerDown; },
3937
4127
  get onCellPointerEnter() { return onCellPointerEnter; },
@@ -4034,12 +4224,12 @@ export function createSvGridController<
4034
4224
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
4035
4225
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
4036
4226
  const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
4037
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
4227
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
4038
4228
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
4039
4229
  const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag<TFeatures, TData>(ctx);
4040
4230
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
4041
4231
  const { buildApi } = createGridApi<TFeatures, TData>(ctx);
4042
- const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
4232
+ const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
4043
4233
 
4044
4234
  // Dev-time configuration checks. Silent misconfiguration was the grid's
4045
4235
  // biggest usability gap - a misspelled `field` rendered a column of blank