@svgrid/grid 1.1.0 → 1.1.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.
Files changed (117) hide show
  1. package/dist/GridMenus.svelte +205 -127
  2. package/dist/SvGrid.controller.svelte.d.ts +44 -1
  3. package/dist/SvGrid.controller.svelte.js +223 -38
  4. package/dist/SvGrid.css +2012 -1709
  5. package/dist/SvGrid.helpers.test.d.ts +1 -0
  6. package/dist/SvGrid.helpers.test.js +298 -0
  7. package/dist/SvGrid.svelte +2346 -2043
  8. package/dist/SvGrid.types.d.ts +91 -4
  9. package/dist/aligned-grids.d.ts +6 -0
  10. package/dist/aligned-grids.js +84 -0
  11. package/dist/aligned-grids.test.d.ts +1 -0
  12. package/dist/aligned-grids.test.js +75 -0
  13. package/dist/build-api.coverage.test.d.ts +20 -0
  14. package/dist/build-api.coverage.test.js +505 -0
  15. package/dist/build-api.js +59 -29
  16. package/dist/cell-render.test.d.ts +1 -0
  17. package/dist/cell-render.test.js +338 -0
  18. package/dist/chart-export.test.d.ts +1 -0
  19. package/dist/chart-export.test.js +302 -0
  20. package/dist/chart.coverage.test.d.ts +1 -0
  21. package/dist/chart.coverage.test.js +748 -0
  22. package/dist/clipboard.js +88 -24
  23. package/dist/clipboard.test.d.ts +1 -0
  24. package/dist/clipboard.test.js +700 -0
  25. package/dist/collaboration.coverage.test.d.ts +1 -0
  26. package/dist/collaboration.coverage.test.js +200 -0
  27. package/dist/column-groups.d.ts +19 -0
  28. package/dist/column-groups.js +62 -0
  29. package/dist/column-groups.test.d.ts +1 -0
  30. package/dist/column-groups.test.js +56 -0
  31. package/dist/column-types.d.ts +10 -0
  32. package/dist/column-types.js +63 -0
  33. package/dist/column-types.test.d.ts +1 -0
  34. package/dist/column-types.test.js +62 -0
  35. package/dist/columns.test.d.ts +1 -0
  36. package/dist/columns.test.js +625 -0
  37. package/dist/core.d.ts +85 -0
  38. package/dist/editing.d.ts +7 -0
  39. package/dist/editing.js +191 -5
  40. package/dist/editing.test.d.ts +1 -0
  41. package/dist/editing.test.js +732 -0
  42. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  43. package/dist/editors/cell-editors.coverage.test.js +139 -0
  44. package/dist/facet-buckets.test.d.ts +1 -0
  45. package/dist/facet-buckets.test.js +296 -0
  46. package/dist/filter-operators.test.d.ts +1 -0
  47. package/dist/filter-operators.test.js +135 -0
  48. package/dist/hyperformula-adapter.test.d.ts +1 -0
  49. package/dist/hyperformula-adapter.test.js +205 -0
  50. package/dist/index.d.ts +5 -2
  51. package/dist/index.js +4 -1
  52. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  53. package/dist/keyboard-handlers.coverage.test.js +495 -0
  54. package/dist/keyboard-handlers.js +7 -2
  55. package/dist/menus.js +1 -0
  56. package/dist/menus.test.d.ts +1 -0
  57. package/dist/menus.test.js +560 -0
  58. package/dist/named-views.coverage.test.d.ts +1 -0
  59. package/dist/named-views.coverage.test.js +180 -0
  60. package/dist/row-drag.d.ts +49 -0
  61. package/dist/row-drag.js +221 -0
  62. package/dist/row-drag.test.d.ts +1 -0
  63. package/dist/row-drag.test.js +142 -0
  64. package/dist/row-resize.test.d.ts +1 -0
  65. package/dist/row-resize.test.js +329 -0
  66. package/dist/scroll-sync.js +3 -0
  67. package/dist/scroll-sync.test.d.ts +1 -0
  68. package/dist/scroll-sync.test.js +290 -0
  69. package/dist/selection.d.ts +7 -1
  70. package/dist/selection.js +76 -36
  71. package/dist/selection.multi-range.test.d.ts +1 -0
  72. package/dist/selection.multi-range.test.js +55 -0
  73. package/dist/selection.test.d.ts +1 -0
  74. package/dist/selection.test.js +647 -0
  75. package/dist/server-data-source.coverage.test.d.ts +1 -0
  76. package/dist/server-data-source.coverage.test.js +154 -0
  77. package/dist/spreadsheet.d.ts +30 -0
  78. package/dist/spreadsheet.js +48 -0
  79. package/dist/spreadsheet.test.d.ts +1 -0
  80. package/dist/spreadsheet.test.js +446 -0
  81. package/dist/sv-grid-scrollbar.js +13 -1
  82. package/dist/svgrid-wrapper.types.d.ts +19 -0
  83. package/dist/svgrid.behavior.test.js +20 -0
  84. package/dist/svgrid.interaction.test.js +31 -0
  85. package/dist/svgrid.new-features.wrapper.test.js +34 -2
  86. package/dist/test-setup.js +9 -3
  87. package/dist/virtualization/scroll-scaling.d.ts +17 -0
  88. package/dist/virtualization/scroll-scaling.js +35 -0
  89. package/dist/virtualization/scroll-scaling.test.js +42 -1
  90. package/package.json +2 -1
  91. package/src/GridMenus.svelte +205 -127
  92. package/src/SvGrid.controller.svelte.ts +2352 -2195
  93. package/src/SvGrid.css +2012 -1747
  94. package/src/SvGrid.svelte +2346 -2047
  95. package/src/SvGrid.types.ts +537 -456
  96. package/src/aligned-grids.test.ts +80 -0
  97. package/src/aligned-grids.ts +87 -0
  98. package/src/build-api.ts +683 -663
  99. package/src/clipboard.test.ts +49 -0
  100. package/src/clipboard.ts +51 -23
  101. package/src/column-groups.test.ts +59 -0
  102. package/src/column-groups.ts +80 -0
  103. package/src/column-types.test.ts +68 -0
  104. package/src/column-types.ts +82 -0
  105. package/src/core.ts +78 -0
  106. package/src/editing.ts +669 -513
  107. package/src/index.ts +12 -0
  108. package/src/menus.ts +1 -0
  109. package/src/row-drag.test.ts +168 -0
  110. package/src/row-drag.ts +255 -0
  111. package/src/scroll-sync.ts +2 -0
  112. package/src/selection.multi-range.test.ts +61 -0
  113. package/src/selection.ts +71 -37
  114. package/src/spreadsheet.test.ts +489 -445
  115. package/src/spreadsheet.ts +304 -246
  116. package/src/svgrid-wrapper.types.ts +19 -0
  117. package/src/svgrid.new-features.wrapper.test.ts +2 -2
@@ -1,5 +1,5 @@
1
1
  import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
2
- import { createRowScrollScaling } from "./virtualization/scroll-scaling";
2
+ import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling";
3
3
  import "./sv-grid-scrollbar";
4
4
  import { computeColumnStat, formatsNeedingStats, } from "./conditional-formatting";
5
5
  import { rawToNumber, } from "./SvGrid.helpers";
@@ -12,6 +12,10 @@ import { createCellRender, } from "./cell-render";
12
12
  import { createEditing, } from "./editing";
13
13
  import { createSelection, } from "./selection";
14
14
  import { createColumns, } from "./columns";
15
+ import { createRowDrag, } from "./row-drag";
16
+ import { createAlignedGrids, } from "./aligned-grids";
17
+ import { resolveColumnTypes, } from "./column-types";
18
+ import { computeColumnGroupMeta, hiddenLeavesForCollapse, } from "./column-groups";
15
19
  import { createGridApi, } from "./build-api";
16
20
  import { createClipboard, } from "./clipboard";
17
21
  import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
@@ -24,35 +28,81 @@ import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNume
24
28
  */
25
29
  const MAX_DOM_SCROLL_HEIGHT_FALLBACK = 8000000;
26
30
  /**
27
- * The browser's actual maximum element height in CSS px. Browsers clamp how
28
- * tall a single element may be (and mobile / high-DPR caps differ), so we
29
- * measure it once by reading the clamped `offsetHeight` of an absurdly tall
30
- * probe kept inside a 1x1 `overflow:hidden` wrapper (so it never affects page
31
- * layout or scroll). Cached for the page lifetime; constant per browser.
31
+ * The browser's actual maximum *scrollable* element height in CSS px. Browsers
32
+ * clamp how tall a single element may be, and the cap is lower on mobile /
33
+ * high-DPR devices (the physical limit is in device px, so a 3x-DPR phone has
34
+ * ~1/3 the CSS-px cap of a 1x desktop). Past that cap a scroll container
35
+ * silently clamps its `scrollHeight` and the tail rows of a huge virtualized
36
+ * grid become unreachable.
37
+ *
38
+ * We measure two signals from one offscreen probe and keep the smaller (see
39
+ * `resolveMaxDomHeight`): the probe's clamped `offsetHeight`, AND the
40
+ * `scrollHeight` a real `overflow:auto` container exposes for it. The second
41
+ * matters because mobile WebKit/Blink can report a generous `offsetHeight` yet
42
+ * expose a smaller scrollable range - trusting the layout height alone is what
43
+ * stranded the last rows on phones. Using a real scroll container also folds in
44
+ * DPR clamping for free. Cached for the page lifetime; constant per browser.
32
45
  */
33
46
  let detectedMaxDomHeight = null;
47
+ /**
48
+ * Seed the `hiddenColumns` map from any column def marked `visible: false`.
49
+ * Walks groups so a hidden group hides all of its leaf columns. Keyed by the
50
+ * same id `setColumnVisible` uses (`id ?? field`), so user toggles afterward
51
+ * stay consistent. Run once at mount; prop changes don't re-apply it.
52
+ */
53
+ function initialHiddenColumns(defs) {
54
+ const hidden = {};
55
+ const walk = (cols, inheritedHidden) => {
56
+ for (const def of cols) {
57
+ const hide = inheritedHidden || def.visible === false;
58
+ if (def.columns?.length) {
59
+ walk(def.columns, hide);
60
+ }
61
+ else if (hide) {
62
+ const id = def.id ?? def.field;
63
+ if (id)
64
+ hidden[id] = true;
65
+ }
66
+ }
67
+ };
68
+ walk(defs, false);
69
+ return hidden;
70
+ }
34
71
  function getMaxDomScrollHeight() {
72
+ // Escape hatch: a page may pin the cap via `window.__svgridMaxDomHeight`.
73
+ // Checked before the cache so it always wins. Two uses: reproducing a
74
+ // phone's lower element-height limit on desktop (and our e2e coverage of
75
+ // the huge-list path), and overriding detection on a device where it reads
76
+ // wrong. A non-positive / non-finite value is ignored.
77
+ if (typeof window !== "undefined") {
78
+ const forced = window
79
+ .__svgridMaxDomHeight;
80
+ if (typeof forced === "number" && Number.isFinite(forced) && forced > 0) {
81
+ return forced;
82
+ }
83
+ }
35
84
  if (detectedMaxDomHeight != null)
36
85
  return detectedMaxDomHeight;
37
86
  if (typeof document === "undefined" || !document.body) {
38
87
  return MAX_DOM_SCROLL_HEIGHT_FALLBACK;
39
88
  }
40
89
  try {
90
+ // The wrapper is itself an `overflow:auto` scroll container (kept tiny and
91
+ // offscreen so it never affects page layout or scroll), so we can read the
92
+ // height it actually exposes as scrollable - not just the probe's layout
93
+ // height. On high-DPR mobile the two diverge and the scrollable one is the
94
+ // limit that matters.
41
95
  const wrap = document.createElement("div");
42
96
  wrap.style.cssText =
43
- "position:fixed;top:0;left:-9999px;width:1px;height:1px;overflow:hidden;visibility:hidden;pointer-events:none;";
97
+ "position:fixed;top:0;left:-9999px;width:1px;height:100px;overflow:auto;visibility:hidden;pointer-events:none;";
44
98
  const probe = document.createElement("div");
45
99
  probe.style.cssText = "width:1px;height:1000000000px;";
46
100
  wrap.appendChild(probe);
47
101
  document.body.appendChild(wrap);
48
- const measured = probe.offsetHeight;
102
+ const layoutCap = probe.offsetHeight;
103
+ const scrollCap = wrap.scrollHeight;
49
104
  document.body.removeChild(wrap);
50
- // A clamped read is a large finite number well under the 1e9 we asked for.
51
- // If the browser did not clamp (returned ~1e9) or returned junk, fall back.
52
- detectedMaxDomHeight =
53
- measured > 100000 && measured < 900000000
54
- ? measured
55
- : MAX_DOM_SCROLL_HEIGHT_FALLBACK;
105
+ detectedMaxDomHeight = resolveMaxDomHeight(layoutCap, scrollCap, MAX_DOM_SCROLL_HEIGHT_FALLBACK);
56
106
  }
57
107
  catch {
58
108
  detectedMaxDomHeight = MAX_DOM_SCROLL_HEIGHT_FALLBACK;
@@ -91,6 +141,10 @@ export function createSvGridController(props) {
91
141
  let pendingScrollLeft = 0;
92
142
  let scrollSyncRaf = null;
93
143
  let selectionRange = $state({ anchor: null, focus: null });
144
+ // Extra committed ranges for multi-range (Ctrl+drag) selection. The
145
+ // `selectionRange` above is always the ACTIVE range being manipulated; these
146
+ // are the finished ones. Full selection = these + the active range.
147
+ let selectionRanges = $state.raw([]);
94
148
  let isDraggingSelection = $state(false);
95
149
  /** Excel-style fill handle drag state. While non-null we paint a "fill
96
150
  * preview" overlay on cells between the source range and the pointer
@@ -98,6 +152,9 @@ export function createSvGridController(props) {
98
152
  let fillDrag = $state(null);
99
153
  let activeAtPointerDown = null;
100
154
  let editingCell = $state(null);
155
+ // Full-row editing: the row currently in whole-row edit + its per-column
156
+ // draft (keyed by column id). Null when not in full-row mode.
157
+ let fullRowEdit = $state(null);
101
158
  let editedCellValues = $state({});
102
159
  const UNDO_LIMIT = 200;
103
160
  let history = $state([]);
@@ -160,6 +217,7 @@ export function createSvGridController(props) {
160
217
  const rowNumberColumnWidth = $derived(props.rowNumberWidth ?? 56);
161
218
  const showRowNumbersEffective = $derived(props.showRowNumbers ?? false);
162
219
  let columnMenuFor = $state(null);
220
+ let columnMenuTab = $state("general");
163
221
  let columnMenuPos = $state({ x: 0, y: 0 });
164
222
  let columnMenuSearch = $state("");
165
223
  let filterMenuFor = $state(null);
@@ -236,9 +294,41 @@ export function createSvGridController(props) {
236
294
  // mutates these so add/remove operations don't need a callback round-trip.
237
295
  // svelte-ignore state_referenced_locally
238
296
  let internalData = $state.raw(props.data);
297
+ // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
298
+ // format defaults once, up front, so every downstream reader sees a normal
299
+ // column. Explicit fields on the ColumnDef always win.
300
+ // svelte-ignore state_referenced_locally
301
+ const resolveCols = (cols) => resolveColumnTypes(cols, props.data?.[0], props.inferColumnTypes === true);
239
302
  // svelte-ignore state_referenced_locally
240
- let internalColumns = $state.raw(props.columns);
241
- let hiddenColumns = $state({});
303
+ let internalColumns = $state.raw(resolveCols(props.columns));
304
+ // svelte-ignore state_referenced_locally
305
+ let hiddenColumns = $state(initialHiddenColumns(props.columns));
306
+ // Collapsible column groups (columnGroupShow). Meta is derived from the tree;
307
+ // `collapsedColumnGroups` is the live set of collapsed group ids, seeded once
308
+ // from each collapsible group's `openByDefault` (default: collapsed).
309
+ const columnGroupMeta = $derived(computeColumnGroupMeta(props.columns));
310
+ // svelte-ignore state_referenced_locally
311
+ let collapsedColumnGroups = $state((() => {
312
+ const meta = computeColumnGroupMeta(props.columns);
313
+ const s = new Set();
314
+ for (const id of meta.collapsibleGroupIds)
315
+ if (!meta.defaultOpen.get(id))
316
+ s.add(id);
317
+ return s;
318
+ })());
319
+ // Leaf ids hidden right now because their group is collapsed/expanded.
320
+ const hiddenByGroupCollapse = $derived(hiddenLeavesForCollapse(columnGroupMeta, collapsedColumnGroups));
321
+ function toggleColumnGroup(groupId) {
322
+ const next = new Set(collapsedColumnGroups);
323
+ if (next.has(groupId))
324
+ next.delete(groupId);
325
+ else
326
+ next.add(groupId);
327
+ collapsedColumnGroups = next;
328
+ }
329
+ function isColumnGroupCollapsed(groupId) {
330
+ return collapsedColumnGroups.has(groupId);
331
+ }
242
332
  $effect(() => {
243
333
  // When the consumer replaces `data` (e.g. a "Reset" button), drop any
244
334
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
@@ -248,7 +338,7 @@ export function createSvGridController(props) {
248
338
  editedCellValues = {};
249
339
  });
250
340
  $effect(() => {
251
- internalColumns = props.columns;
341
+ internalColumns = resolveCols(props.columns);
252
342
  });
253
343
  // Captured ONCE at mount: `externalSort` is a structural choice (tree vs
254
344
  // flat data) so toggling it after mount is not supported. Reading it here
@@ -334,7 +424,7 @@ export function createSvGridController(props) {
334
424
  const allColumns = $derived.by(() => {
335
425
  let raw = grid
336
426
  .getAllColumns()
337
- .filter((column) => !hiddenColumns[column.id]);
427
+ .filter((column) => !hiddenColumns[column.id] && !hiddenByGroupCollapse[column.id]);
338
428
  // Apply user reorder (if any). Unknown ids in userColumnOrder are
339
429
  // skipped; columns not in userColumnOrder keep their original
340
430
  // relative order after the user-ordered ones.
@@ -403,13 +493,17 @@ export function createSvGridController(props) {
403
493
  function buildId(def, parentId, fallbackIx) {
404
494
  return def.id ?? def.field ?? `${parentId ?? 'col'}_d_${fallbackIx}`;
405
495
  }
496
+ // Leaves hidden by a collapsed/expanded group are skipped everywhere here,
497
+ // so group colSpan + widthPx exclude them and stay aligned with the leaves
498
+ // the body actually renders.
499
+ const hiddenLeaf = hiddenByGroupCollapse;
406
500
  function collectLeaves(defs, parentId, depthHere) {
407
501
  defs.forEach((def, ix) => {
408
502
  const id = buildId(def, parentId, ix);
409
503
  if (def.columns?.length) {
410
504
  collectLeaves(def.columns, id, depthHere + 1);
411
505
  }
412
- else {
506
+ else if (!hiddenLeaf[id]) {
413
507
  leafEntries.push({ id, widthPx: getColumnWidth(id) });
414
508
  }
415
509
  });
@@ -419,6 +513,10 @@ export function createSvGridController(props) {
419
513
  const nodes = [];
420
514
  for (const def of defs) {
421
515
  const id = buildId(def, parentId, nodes.length);
516
+ // Skip leaves the collapse state hides, so leaf indices/colSpans match
517
+ // `leafEntries` (and the body's rendered columns) exactly.
518
+ if (!def.columns?.length && hiddenLeaf[id])
519
+ continue;
422
520
  const leafStart = cursor.leaf;
423
521
  if (def.columns?.length) {
424
522
  indexTree(def.columns, id, cursor);
@@ -464,6 +562,7 @@ export function createSvGridController(props) {
464
562
  const cells = at.map((n) => {
465
563
  const isLeafEarly = !n.def.columns?.length;
466
564
  const headerText = typeof n.def.header === 'string' ? n.def.header : '';
565
+ const collapsible = columnGroupMeta.collapsibleGroupIds.has(n.id);
467
566
  return {
468
567
  key: `${n.id}_d${d}`,
469
568
  label: isLeafEarly ? '' : headerText,
@@ -471,6 +570,9 @@ export function createSvGridController(props) {
471
570
  widthPx: sumLeafWidths(n.leafStart, n.leafEnd),
472
571
  firstLeafIndex: n.leafStart,
473
572
  isPlaceholder: isLeafEarly,
573
+ groupId: collapsible ? n.id : undefined,
574
+ collapsible,
575
+ collapsed: collapsible && collapsedColumnGroups.has(n.id),
474
576
  };
475
577
  });
476
578
  rows.push({ id: `gh_${d}`, cells });
@@ -504,6 +606,12 @@ export function createSvGridController(props) {
504
606
  let colDragId = $state(null);
505
607
  let colDropOnId = $state(null);
506
608
  let colDropSide = $state(null);
609
+ // Live drag state for managed row dragging. Only meaningful while a row is
610
+ // being dragged (`props.rowDragManaged`). `rowDropIndex` is the visible row
611
+ // index currently hovered; `rowDropSide` says which edge the drop line paints.
612
+ let rowDragActive = $state(false);
613
+ let rowDropIndex = $state(null);
614
+ let rowDropSide = $state(null);
507
615
  // ---- Conditional formatting --------------------------------------------
508
616
  // True when the feature is in use; gates the per-cell positioning context
509
617
  // (cells are otherwise non-relative for scroll performance).
@@ -577,23 +685,36 @@ export function createSvGridController(props) {
577
685
  .some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), props.filterLocale)
578
686
  .includes(needle)));
579
687
  }
580
- const menuFilters = Object.entries(filterMenuValues).filter(([_, filter]) => {
581
- if (filter.operator === "isBlank")
688
+ // A single condition is "active" if it has the value(s) it needs.
689
+ const condActive = (op, value, valueTo) => {
690
+ if (op === "isBlank")
582
691
  return true;
583
- // `between` needs both endpoints; otherwise treat as inactive.
584
- if (filter.operator === "between") {
585
- return (filter.value.trim().length > 0 &&
586
- (filter.valueTo ?? "").trim().length > 0);
587
- }
588
- return filter.value.trim().length > 0;
692
+ if (op === "between")
693
+ return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
694
+ return value.trim().length > 0;
695
+ };
696
+ const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: props.filterLocale });
697
+ // A column filter is active if either of its (up to two) conditions is.
698
+ const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
699
+ const a = condActive(f.operator, f.value, f.valueTo);
700
+ const b = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
701
+ return a || b;
589
702
  });
590
703
  if (menuFilters.length) {
591
- rows = rows.filter((row) => menuFilters.every(([columnId, filter]) => applyExcelFilter(getRowColumnValue(row, columnId), {
592
- id: columnId,
593
- operator: filter.operator,
594
- value: filter.value,
595
- valueTo: filter.operator === "between" ? filter.valueTo : undefined,
596
- }, { locale: props.filterLocale })));
704
+ rows = rows.filter((row) => menuFilters.every(([columnId, f]) => {
705
+ const cellValue = getRowColumnValue(row, columnId);
706
+ const aActive = condActive(f.operator, f.value, f.valueTo);
707
+ const bActive = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
708
+ const ra = aActive ? evalCond(cellValue, columnId, f.operator, f.value, f.valueTo) : null;
709
+ const rb = bActive
710
+ ? evalCond(cellValue, columnId, f.operator2, f.value2 ?? "", f.valueTo2)
711
+ : null;
712
+ if (ra === null)
713
+ return rb ?? true;
714
+ if (rb === null)
715
+ return ra;
716
+ return f.join === "OR" ? ra || rb : ra && rb;
717
+ }));
597
718
  }
598
719
  const valueFilterEntries = Object.entries(valueFilters);
599
720
  if (valueFilterEntries.length) {
@@ -739,7 +860,10 @@ export function createSvGridController(props) {
739
860
  };
740
861
  });
741
862
  // ---- Tool panel (docked columns sidebar) -------------------------------
742
- let toolPanelOpen = $state(false);
863
+ // svelte-ignore state_referenced_locally
864
+ let toolPanelOpen = $state(props.toolPanelDefaultOpen === true);
865
+ // svelte-ignore state_referenced_locally
866
+ let toolPanelTab = $state(props.toolPanelDefaultTab ?? "columns");
743
867
  const toolPanelEnabled = $derived(props.toolPanel === true);
744
868
  // Every column (including hidden ones) in the user's current order, so the
745
869
  // panel can toggle/reorder anything. Group columns are flagged live.
@@ -1053,6 +1177,7 @@ export function createSvGridController(props) {
1053
1177
  if (isFirstRender)
1054
1178
  return;
1055
1179
  selectionRange = { anchor: null, focus: null };
1180
+ selectionRanges = [];
1056
1181
  editingCell = null;
1057
1182
  if (scrollContainer) {
1058
1183
  scrollContainer.scrollTop = 0;
@@ -1218,7 +1343,16 @@ export function createSvGridController(props) {
1218
1343
  return null;
1219
1344
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
1220
1345
  const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
1221
- const target = (scrollContainer?.clientWidth ?? 0) - rowNumberWidth - selectionWidth;
1346
+ // Reserve the custom vertical scrollbar's width when it's visible. It
1347
+ // overlays the right 16px of the viewport (absolute, z-index 40) and
1348
+ // does NOT shrink clientWidth, so without this the last fitted column
1349
+ // slides under it and its right-aligned content (e.g. a number column)
1350
+ // is hidden behind the opaque scrollbar.
1351
+ const scrollbarWidth = hasVerticalOverflow ? 16 : 0;
1352
+ const target = (scrollContainer?.clientWidth ?? 0) -
1353
+ rowNumberWidth -
1354
+ selectionWidth -
1355
+ scrollbarWidth;
1222
1356
  if (target <= 0)
1223
1357
  return null;
1224
1358
  // Split base widths into pinned (user-resized) and scalable.
@@ -1322,7 +1456,9 @@ export function createSvGridController(props) {
1322
1456
  return map;
1323
1457
  });
1324
1458
  const columnMenuFacetValues = $derived.by(() => {
1325
- const columnId = filterMenuFor;
1459
+ // The funnel popover drives via `filterMenuFor`; the column menu's Filter
1460
+ // tab drives via `columnMenuFor`. Support whichever is open.
1461
+ const columnId = filterMenuFor ?? columnMenuFor;
1326
1462
  if (!columnId)
1327
1463
  return [];
1328
1464
  const column = allColumns.find((entry) => entry.id === columnId);
@@ -1393,6 +1529,8 @@ export function createSvGridController(props) {
1393
1529
  set scrollSyncRaf(v) { scrollSyncRaf = v; },
1394
1530
  get selectionRange() { return selectionRange; },
1395
1531
  set selectionRange(v) { selectionRange = v; },
1532
+ get selectionRanges() { return selectionRanges; },
1533
+ set selectionRanges(v) { selectionRanges = v; },
1396
1534
  get isDraggingSelection() { return isDraggingSelection; },
1397
1535
  set isDraggingSelection(v) { isDraggingSelection = v; },
1398
1536
  get fillDrag() { return fillDrag; },
@@ -1401,6 +1539,8 @@ export function createSvGridController(props) {
1401
1539
  set activeAtPointerDown(v) { activeAtPointerDown = v; },
1402
1540
  get editingCell() { return editingCell; },
1403
1541
  set editingCell(v) { editingCell = v; },
1542
+ get fullRowEdit() { return fullRowEdit; },
1543
+ set fullRowEdit(v) { fullRowEdit = v; },
1404
1544
  get editedCellValues() { return editedCellValues; },
1405
1545
  set editedCellValues(v) { editedCellValues = v; },
1406
1546
  get UNDO_LIMIT() { return UNDO_LIMIT; },
@@ -1453,6 +1593,8 @@ export function createSvGridController(props) {
1453
1593
  get DATE_OPERATORS() { return DATE_OPERATORS; },
1454
1594
  get CHECKBOX_OPERATORS() { return CHECKBOX_OPERATORS; },
1455
1595
  get columnMenuFor() { return columnMenuFor; },
1596
+ get columnMenuTab() { return columnMenuTab; },
1597
+ set columnMenuTab(v) { columnMenuTab = v; },
1456
1598
  set columnMenuFor(v) { columnMenuFor = v; },
1457
1599
  get columnMenuPos() { return columnMenuPos; },
1458
1600
  set columnMenuPos(v) { columnMenuPos = v; },
@@ -1498,6 +1640,8 @@ export function createSvGridController(props) {
1498
1640
  set internalColumns(v) { internalColumns = v; },
1499
1641
  get hiddenColumns() { return hiddenColumns; },
1500
1642
  set hiddenColumns(v) { hiddenColumns = v; },
1643
+ get toggleColumnGroup() { return toggleColumnGroup; },
1644
+ get isColumnGroupCollapsed() { return isColumnGroupCollapsed; },
1501
1645
  get externalSortEnabled() { return externalSortEnabled; },
1502
1646
  get externalFilterEnabled() { return externalFilterEnabled; },
1503
1647
  get passthroughSortedRowModel() { return passthroughSortedRowModel; },
@@ -1519,6 +1663,20 @@ export function createSvGridController(props) {
1519
1663
  set colDropOnId(v) { colDropOnId = v; },
1520
1664
  get colDropSide() { return colDropSide; },
1521
1665
  set colDropSide(v) { colDropSide = v; },
1666
+ get rowDragActive() { return rowDragActive; },
1667
+ set rowDragActive(v) { rowDragActive = v; },
1668
+ get rowDropIndex() { return rowDropIndex; },
1669
+ set rowDropIndex(v) { rowDropIndex = v; },
1670
+ get rowDropSide() { return rowDropSide; },
1671
+ set rowDropSide(v) { rowDropSide = v; },
1672
+ get onRowDragStart() { return onRowDragStart; },
1673
+ get onRowDragOver() { return onRowDragOver; },
1674
+ get onRowDragLeave() { return onRowDragLeave; },
1675
+ get onRowDrop() { return onRowDrop; },
1676
+ get onRowsContainerDragOver() { return onRowsContainerDragOver; },
1677
+ get onRowsContainerDrop() { return onRowsContainerDrop; },
1678
+ get onRowDragEnd() { return onRowDragEnd; },
1679
+ get broadcastAlignedScroll() { return broadcastAlignedScroll; },
1522
1680
  get getCurrentColumnOrder() { return getCurrentColumnOrder; },
1523
1681
  get emitColumnOrder() { return emitColumnOrder; },
1524
1682
  get setColumnOrderInternal() { return setColumnOrderInternal; },
@@ -1554,6 +1712,8 @@ export function createSvGridController(props) {
1554
1712
  get statusBarStats() { return statusBarStats; },
1555
1713
  get toolPanelOpen() { return toolPanelOpen; },
1556
1714
  set toolPanelOpen(v) { toolPanelOpen = v; },
1715
+ get toolPanelTab() { return toolPanelTab; },
1716
+ set toolPanelTab(v) { toolPanelTab = v; },
1557
1717
  get toolPanelEnabled() { return toolPanelEnabled; },
1558
1718
  get toolPanelColumns() { return toolPanelColumns; },
1559
1719
  get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
@@ -1636,6 +1796,7 @@ export function createSvGridController(props) {
1636
1796
  get extendSelection() { return extendSelection; },
1637
1797
  get isCellInSelectedRange() { return isCellInSelectedRange; },
1638
1798
  get getCellRangeEdges() { return getCellRangeEdges; },
1799
+ get getSelectionRects() { return getSelectionRects; },
1639
1800
  get fillHandleCell() { return fillHandleCell; },
1640
1801
  get isInFillPreview() { return isInFillPreview; },
1641
1802
  get findColumnById() { return findColumnById; },
@@ -1654,9 +1815,18 @@ export function createSvGridController(props) {
1654
1815
  get onCellClick() { return onCellClick; },
1655
1816
  get emitCellDoubleClick() { return emitCellDoubleClick; },
1656
1817
  get copySelectionToClipboard() { return copySelectionToClipboard; },
1818
+ get cutSelectionToClipboard() { return cutSelectionToClipboard; },
1819
+ get pasteFromClipboard() { return pasteFromClipboard; },
1820
+ get onGridPaste() { return onGridPaste; },
1657
1821
  get clearSelectedCells() { return clearSelectedCells; },
1658
1822
  get onCellDoubleClick() { return onCellDoubleClick; },
1659
1823
  get startEditingWithChar() { return startEditingWithChar; },
1824
+ get startEditing() { return startEditing; },
1825
+ get stopEditing() { return stopEditing; },
1826
+ get startFullRowEdit() { return startFullRowEdit; },
1827
+ get setFullRowDraft() { return setFullRowDraft; },
1828
+ get commitFullRowEdit() { return commitFullRowEdit; },
1829
+ get cancelFullRowEdit() { return cancelFullRowEdit; },
1660
1830
  get saveEditingCell() { return saveEditingCell; },
1661
1831
  get applyHistoryStep() { return applyHistoryStep; },
1662
1832
  get updateEditingCellValue() { return updateEditingCellValue; },
@@ -1723,10 +1893,25 @@ export function createSvGridController(props) {
1723
1893
  const { computeSummaries, hasRenderedColumn } = createSummaries(ctx);
1724
1894
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus(ctx);
1725
1895
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
1726
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard } = createEditing(ctx);
1727
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
1896
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
1897
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
1728
1898
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, startColumnResize, onColumnResizeMove, endColumnResize, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns(ctx);
1899
+ const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd } = createRowDrag(ctx);
1900
+ const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids(ctx);
1729
1901
  const { buildApi } = createGridApi(ctx);
1730
1902
  const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
1903
+ // Aligned grids: register in the shared group on mount, and mirror column
1904
+ // resizes to peers whenever columnWidths changes. Horizontal-scroll mirroring
1905
+ // is driven from onBodyScroll (via ctx.broadcastAlignedScroll).
1906
+ $effect(() => {
1907
+ if (props.alignedGridGroup == null)
1908
+ return;
1909
+ return registerAlignedGrid();
1910
+ });
1911
+ $effect(() => {
1912
+ // Track columnWidths reactively, then broadcast to aligned peers.
1913
+ void columnWidths;
1914
+ broadcastAlignedWidths();
1915
+ });
1731
1916
  return ctx;
1732
1917
  }