@svgrid/grid 1.1.0 → 1.1.2

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 (148) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridMenus.svelte +205 -127
  3. package/dist/SvGrid.controller.svelte.d.ts +44 -1
  4. package/dist/SvGrid.controller.svelte.js +240 -44
  5. package/dist/SvGrid.css +325 -22
  6. package/dist/SvGrid.helpers.test.d.ts +1 -0
  7. package/dist/SvGrid.helpers.test.js +298 -0
  8. package/dist/SvGrid.svelte +729 -203
  9. package/dist/SvGrid.types.d.ts +91 -4
  10. package/dist/aligned-grids.d.ts +6 -0
  11. package/dist/aligned-grids.js +84 -0
  12. package/dist/aligned-grids.test.d.ts +1 -0
  13. package/dist/aligned-grids.test.js +75 -0
  14. package/dist/build-api.coverage.test.d.ts +20 -0
  15. package/dist/build-api.coverage.test.js +505 -0
  16. package/dist/build-api.js +61 -31
  17. package/dist/cell-render.test.d.ts +1 -0
  18. package/dist/cell-render.test.js +338 -0
  19. package/dist/cell-values.d.ts +1 -1
  20. package/dist/cell-values.js +7 -7
  21. package/dist/chart-export.test.d.ts +1 -0
  22. package/dist/chart-export.test.js +302 -0
  23. package/dist/chart.coverage.test.d.ts +1 -0
  24. package/dist/chart.coverage.test.js +748 -0
  25. package/dist/clipboard.js +88 -24
  26. package/dist/clipboard.test.d.ts +1 -0
  27. package/dist/clipboard.test.js +700 -0
  28. package/dist/collaboration.coverage.test.d.ts +1 -0
  29. package/dist/collaboration.coverage.test.js +200 -0
  30. package/dist/column-groups.d.ts +19 -0
  31. package/dist/column-groups.js +62 -0
  32. package/dist/column-groups.test.d.ts +1 -0
  33. package/dist/column-groups.test.js +56 -0
  34. package/dist/column-types.d.ts +10 -0
  35. package/dist/column-types.js +63 -0
  36. package/dist/column-types.test.d.ts +1 -0
  37. package/dist/column-types.test.js +62 -0
  38. package/dist/columns.test.d.ts +1 -0
  39. package/dist/columns.test.js +625 -0
  40. package/dist/core.d.ts +86 -1
  41. package/dist/core.js +2 -2
  42. package/dist/editing.d.ts +7 -0
  43. package/dist/editing.js +191 -5
  44. package/dist/editing.test.d.ts +1 -0
  45. package/dist/editing.test.js +732 -0
  46. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  47. package/dist/editors/cell-editors.coverage.test.js +139 -0
  48. package/dist/facet-buckets.test.d.ts +1 -0
  49. package/dist/facet-buckets.test.js +296 -0
  50. package/dist/filter-operators.test.d.ts +1 -0
  51. package/dist/filter-operators.test.js +135 -0
  52. package/dist/hyperformula-adapter.test.d.ts +1 -0
  53. package/dist/hyperformula-adapter.test.js +205 -0
  54. package/dist/index.d.ts +5 -2
  55. package/dist/index.js +4 -1
  56. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  57. package/dist/keyboard-handlers.coverage.test.js +495 -0
  58. package/dist/keyboard-handlers.js +7 -2
  59. package/dist/menus.js +1 -0
  60. package/dist/menus.test.d.ts +1 -0
  61. package/dist/menus.test.js +560 -0
  62. package/dist/named-views.coverage.test.d.ts +1 -0
  63. package/dist/named-views.coverage.test.js +180 -0
  64. package/dist/row-drag.d.ts +49 -0
  65. package/dist/row-drag.js +221 -0
  66. package/dist/row-drag.test.d.ts +1 -0
  67. package/dist/row-drag.test.js +142 -0
  68. package/dist/row-resize.test.d.ts +1 -0
  69. package/dist/row-resize.test.js +329 -0
  70. package/dist/scroll-sync.js +3 -0
  71. package/dist/scroll-sync.test.d.ts +1 -0
  72. package/dist/scroll-sync.test.js +290 -0
  73. package/dist/selection.d.ts +7 -1
  74. package/dist/selection.js +76 -36
  75. package/dist/selection.multi-range.test.d.ts +1 -0
  76. package/dist/selection.multi-range.test.js +55 -0
  77. package/dist/selection.test.d.ts +1 -0
  78. package/dist/selection.test.js +647 -0
  79. package/dist/server-data-source.coverage.test.d.ts +1 -0
  80. package/dist/server-data-source.coverage.test.js +154 -0
  81. package/dist/spreadsheet.d.ts +30 -0
  82. package/dist/spreadsheet.js +48 -0
  83. package/dist/spreadsheet.test.d.ts +1 -0
  84. package/dist/spreadsheet.test.js +446 -0
  85. package/dist/summaries.js +4 -4
  86. package/dist/sv-grid-scrollbar.js +13 -1
  87. package/dist/svgrid-wrapper.types.d.ts +19 -0
  88. package/dist/svgrid.behavior.test.js +20 -0
  89. package/dist/svgrid.interaction.test.js +31 -0
  90. package/dist/svgrid.new-features.wrapper.test.js +34 -2
  91. package/dist/test-setup.js +9 -3
  92. package/dist/virtualization/scroll-scaling.d.ts +17 -0
  93. package/dist/virtualization/scroll-scaling.js +35 -0
  94. package/dist/virtualization/scroll-scaling.test.js +42 -1
  95. package/package.json +2 -1
  96. package/src/FlexRender.svelte +96 -96
  97. package/src/GridMenus.svelte +205 -127
  98. package/src/SvGrid.controller.svelte.ts +204 -36
  99. package/src/SvGrid.css +277 -12
  100. package/src/SvGrid.svelte +727 -205
  101. package/src/SvGrid.types.ts +85 -4
  102. package/src/a11y.contract.test.ts +49 -49
  103. package/src/a11y.test.ts +59 -59
  104. package/src/a11y.ts +59 -59
  105. package/src/aligned-grids.test.ts +80 -0
  106. package/src/aligned-grids.ts +87 -0
  107. package/src/build-api.ts +43 -23
  108. package/src/cell-formatting.ts +169 -169
  109. package/src/cell-values.ts +4 -4
  110. package/src/clipboard.test.ts +49 -0
  111. package/src/clipboard.ts +51 -23
  112. package/src/column-groups.test.ts +59 -0
  113. package/src/column-groups.ts +80 -0
  114. package/src/column-types.test.ts +68 -0
  115. package/src/column-types.ts +82 -0
  116. package/src/core.performance.test.ts +30 -30
  117. package/src/core.ts +1077 -999
  118. package/src/createGrid.svelte.ts +42 -42
  119. package/src/createGrid.test.ts +10 -10
  120. package/src/createGridState.svelte.ts +17 -17
  121. package/src/editing.ts +161 -5
  122. package/src/flex-render.ts +3 -3
  123. package/src/index.ts +208 -196
  124. package/src/keyboard.test.ts +59 -59
  125. package/src/keyboard.ts +97 -97
  126. package/src/menus.ts +1 -0
  127. package/src/merge-objects.ts +48 -48
  128. package/src/render-component.ts +28 -28
  129. package/src/row-drag.test.ts +168 -0
  130. package/src/row-drag.ts +255 -0
  131. package/src/scroll-sync.ts +2 -0
  132. package/src/selection.multi-range.test.ts +61 -0
  133. package/src/selection.ts +71 -37
  134. package/src/spreadsheet.test.ts +46 -2
  135. package/src/spreadsheet.ts +58 -0
  136. package/src/static-functions.ts +11 -11
  137. package/src/subscribe.ts +38 -38
  138. package/src/summaries.ts +4 -4
  139. package/src/svgrid-wrapper.types.ts +412 -393
  140. package/src/svgrid.features.test.ts +157 -157
  141. package/src/svgrid.new-features.wrapper.test.ts +2 -2
  142. package/src/svgrid.wrapper.test.ts +40 -40
  143. package/src/virtualization/column-virtualizer.test.ts +27 -27
  144. package/src/virtualization/column-virtualizer.ts +30 -30
  145. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  146. package/src/virtualization/types.ts +30 -30
  147. package/src/virtualization/virtualizer.test.ts +47 -47
  148. package/src/virtualization/virtualizer.ts +296 -296
@@ -69,6 +69,19 @@ import {
69
69
  import {
70
70
  createColumns,
71
71
  } from "./columns";
72
+ import {
73
+ createRowDrag,
74
+ } from "./row-drag";
75
+ import {
76
+ createAlignedGrids,
77
+ } from "./aligned-grids";
78
+ import {
79
+ resolveColumnTypes,
80
+ } from "./column-types";
81
+ import {
82
+ computeColumnGroupMeta,
83
+ hiddenLeavesForCollapse,
84
+ } from "./column-groups";
72
85
  import {
73
86
  createGridApi,
74
87
  } from "./build-api";
@@ -242,7 +255,20 @@ export function createSvGridController<
242
255
  let gridRootEl: HTMLElement | null = $state(null);
243
256
  let filterRowValues = $state<Record<string, string>>({});
244
257
  let filterMenuValues = $state<
245
- Record<string, { operator: FilterOperator; value: string; valueTo?: string }>
258
+ Record<
259
+ string,
260
+ {
261
+ operator: FilterOperator;
262
+ value: string;
263
+ valueTo?: string;
264
+ // Optional second condition + join for multi-condition filtering
265
+ // within a single column (AND / OR).
266
+ operator2?: FilterOperator;
267
+ value2?: string;
268
+ valueTo2?: string;
269
+ join?: "AND" | "OR";
270
+ }
271
+ >
246
272
  >({});
247
273
  let verticalScrollbarEl: HTMLElement | null = $state(null);
248
274
  let horizontalScrollbarEl: HTMLElement | null = $state(null);
@@ -259,6 +285,10 @@ export function createSvGridController<
259
285
  let pendingScrollLeft = 0;
260
286
  let scrollSyncRaf: number | null = null;
261
287
  let selectionRange = $state<SelectionRange>({ anchor: null, focus: null });
288
+ // Extra committed ranges for multi-range (Ctrl+drag) selection. The
289
+ // `selectionRange` above is always the ACTIVE range being manipulated; these
290
+ // are the finished ones. Full selection = these + the active range.
291
+ let selectionRanges = $state.raw<SelectionRange[]>([]);
262
292
  let isDraggingSelection = $state(false);
263
293
  /** Excel-style fill handle drag state. While non-null we paint a "fill
264
294
  * preview" overlay on cells between the source range and the pointer
@@ -273,6 +303,9 @@ export function createSvGridController<
273
303
  } | null>(null);
274
304
  let activeAtPointerDown: { rowIndex: number; colIndex: number } | null = null;
275
305
  let editingCell = $state<CellEditState>(null);
306
+ // Full-row editing: the row currently in whole-row edit + its per-column
307
+ // draft (keyed by column id). Null when not in full-row mode.
308
+ let fullRowEdit = $state<{ rowId: string; draft: Record<string, unknown> } | null>(null);
276
309
  let editedCellValues = $state<Record<string, unknown>>({});
277
310
 
278
311
  // ---- Undo / redo (history + pointer model) ---------------------------
@@ -351,6 +384,7 @@ export function createSvGridController<
351
384
  const rowNumberColumnWidth = $derived(props.rowNumberWidth ?? 56);
352
385
  const showRowNumbersEffective = $derived(props.showRowNumbers ?? false);
353
386
  let columnMenuFor = $state<string | null>(null);
387
+ let columnMenuTab = $state<"general" | "filter" | "columns">("general");
354
388
  let columnMenuPos = $state<MenuPosition>({ x: 0, y: 0 });
355
389
  let columnMenuSearch = $state("");
356
390
  let filterMenuFor = $state<string | null>(null);
@@ -396,14 +430,25 @@ export function createSvGridController<
396
430
  };
397
431
  });
398
432
  /** Vertical overflow from the virtualizer's authoritative total size,
399
- * NOT from `scrollMetrics.scrollHeight`. Reading DOM dimensions
433
+ * NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
400
434
  * during a Svelte derived runs BEFORE the browser paints - the table
401
435
  * hasn't laid out the new rows yet, so `scrollHeight` is briefly 0
402
436
  * even after data loads. That made the overflow flag return false,
403
- * hid the scrollbar, and broke dragging. */
437
+ * hid the scrollbar, and broke dragging.
438
+ *
439
+ * However, virtualizer.getTotalSize() uses rowHeight * numRows which
440
+ * underestimates when variable-height rows are present (e.g. master-detail
441
+ * expanded rows). We therefore take the MAX of the two sources:
442
+ * - virtualizer.getTotalSize(): correct at initial load (before first paint)
443
+ * - scrollMetrics.scrollHeight: correct after detail rows expand (DOM is live,
444
+ * ResizeObserver on gridRootEl already bumps scrollVersion at that point) */
404
445
  const hasVerticalOverflow = $derived.by(() => {
405
446
  virtualizer.version;
406
- return virtualizer.getTotalSize() > viewportHeight + 1;
447
+ const virtualizerSize = virtualizer.getTotalSize();
448
+ // scrollMetrics.scrollHeight is 0 before initial paint; once the table
449
+ // is in the DOM it reflects the true content height including expanded rows.
450
+ const domSize = scrollMetrics.scrollHeight;
451
+ return Math.max(virtualizerSize, domSize) > viewportHeight + 1;
407
452
  });
408
453
 
409
454
  // Effective filter-UI flags. Each show* prop wins when explicitly set;
@@ -444,15 +489,48 @@ export function createSvGridController<
444
489
  // mutates these so add/remove operations don't need a callback round-trip.
445
490
  // svelte-ignore state_referenced_locally
446
491
  let internalData = $state.raw<ReadonlyArray<TData>>(props.data);
492
+ // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
493
+ // format defaults once, up front, so every downstream reader sees a normal
494
+ // column. Explicit fields on the ColumnDef always win.
495
+ // svelte-ignore state_referenced_locally
496
+ const resolveCols = (cols: Array<ColumnDef<TFeatures, TData>>) =>
497
+ resolveColumnTypes(cols, props.data?.[0], props.inferColumnTypes === true);
447
498
  // svelte-ignore state_referenced_locally
448
499
  let internalColumns = $state.raw<Array<ColumnDef<TFeatures, TData>>>(
449
- props.columns,
500
+ resolveCols(props.columns),
450
501
  );
451
502
  // svelte-ignore state_referenced_locally
452
503
  let hiddenColumns = $state<Record<string, boolean>>(
453
504
  initialHiddenColumns(props.columns),
454
505
  );
455
506
 
507
+ // Collapsible column groups (columnGroupShow). Meta is derived from the tree;
508
+ // `collapsedColumnGroups` is the live set of collapsed group ids, seeded once
509
+ // from each collapsible group's `openByDefault` (default: collapsed).
510
+ const columnGroupMeta = $derived(computeColumnGroupMeta(props.columns as unknown as Array<any>));
511
+ // svelte-ignore state_referenced_locally
512
+ let collapsedColumnGroups = $state<Set<string>>(
513
+ (() => {
514
+ const meta = computeColumnGroupMeta(props.columns as unknown as Array<any>);
515
+ const s = new Set<string>();
516
+ for (const id of meta.collapsibleGroupIds) if (!meta.defaultOpen.get(id)) s.add(id);
517
+ return s;
518
+ })(),
519
+ );
520
+ // Leaf ids hidden right now because their group is collapsed/expanded.
521
+ const hiddenByGroupCollapse = $derived(
522
+ hiddenLeavesForCollapse(columnGroupMeta, collapsedColumnGroups),
523
+ );
524
+ function toggleColumnGroup(groupId: string) {
525
+ const next = new Set(collapsedColumnGroups);
526
+ if (next.has(groupId)) next.delete(groupId);
527
+ else next.add(groupId);
528
+ collapsedColumnGroups = next;
529
+ }
530
+ function isColumnGroupCollapsed(groupId: string) {
531
+ return collapsedColumnGroups.has(groupId);
532
+ }
533
+
456
534
  $effect(() => {
457
535
  // When the consumer replaces `data` (e.g. a "Reset" button), drop any
458
536
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
@@ -462,7 +540,7 @@ export function createSvGridController<
462
540
  editedCellValues = {};
463
541
  });
464
542
  $effect(() => {
465
- internalColumns = props.columns;
543
+ internalColumns = resolveCols(props.columns);
466
544
  });
467
545
 
468
546
  // Captured ONCE at mount: `externalSort` is a structural choice (tree vs
@@ -554,7 +632,7 @@ export function createSvGridController<
554
632
  const allColumns = $derived.by(() => {
555
633
  let raw = grid
556
634
  .getAllColumns()
557
- .filter((column) => !hiddenColumns[column.id]);
635
+ .filter((column) => !hiddenColumns[column.id] && !hiddenByGroupCollapse[column.id]);
558
636
  // Apply user reorder (if any). Unknown ids in userColumnOrder are
559
637
  // skipped; columns not in userColumnOrder keep their original
560
638
  // relative order after the user-ordered ones.
@@ -626,6 +704,10 @@ export function createSvGridController<
626
704
  * multi-level value tree). They render as empty cells so the
627
705
  * layout stays aligned without showing duplicate labels. */
628
706
  isPlaceholder: boolean;
707
+ /** Set when this group cell has a collapse toggle. */
708
+ groupId?: string;
709
+ collapsible: boolean;
710
+ collapsed: boolean;
629
711
  };
630
712
  const groupHeaderRows = $derived.by(() => {
631
713
  const userCols: Array<ColumnDef<any, TData>> =
@@ -652,6 +734,10 @@ export function createSvGridController<
652
734
  function buildId(def: ColumnDef<any, TData>, parentId: string | undefined, fallbackIx: number): string {
653
735
  return def.id ?? def.field ?? `${parentId ?? 'col'}_d_${fallbackIx}`;
654
736
  }
737
+ // Leaves hidden by a collapsed/expanded group are skipped everywhere here,
738
+ // so group colSpan + widthPx exclude them and stay aligned with the leaves
739
+ // the body actually renders.
740
+ const hiddenLeaf = hiddenByGroupCollapse;
655
741
  function collectLeaves(
656
742
  defs: Array<ColumnDef<any, TData>>,
657
743
  parentId: string | undefined,
@@ -661,7 +747,7 @@ export function createSvGridController<
661
747
  const id = buildId(def, parentId, ix);
662
748
  if (def.columns?.length) {
663
749
  collectLeaves(def.columns, id, depthHere + 1);
664
- } else {
750
+ } else if (!hiddenLeaf[id]) {
665
751
  leafEntries.push({ id, widthPx: getColumnWidth(id) });
666
752
  }
667
753
  });
@@ -679,6 +765,9 @@ export function createSvGridController<
679
765
  const nodes: NodeAt[] = [];
680
766
  for (const def of defs) {
681
767
  const id = buildId(def, parentId, nodes.length);
768
+ // Skip leaves the collapse state hides, so leaf indices/colSpans match
769
+ // `leafEntries` (and the body's rendered columns) exactly.
770
+ if (!def.columns?.length && hiddenLeaf[id]) continue;
682
771
  const leafStart = cursor.leaf;
683
772
  if (def.columns?.length) {
684
773
  indexTree(def.columns, id, cursor);
@@ -728,6 +817,7 @@ export function createSvGridController<
728
817
  const isLeafEarly = !n.def.columns?.length;
729
818
  const headerText =
730
819
  typeof n.def.header === 'string' ? n.def.header : '';
820
+ const collapsible = columnGroupMeta.collapsibleGroupIds.has(n.id);
731
821
  return {
732
822
  key: `${n.id}_d${d}`,
733
823
  label: isLeafEarly ? '' : headerText,
@@ -735,6 +825,9 @@ export function createSvGridController<
735
825
  widthPx: sumLeafWidths(n.leafStart, n.leafEnd),
736
826
  firstLeafIndex: n.leafStart,
737
827
  isPlaceholder: isLeafEarly,
828
+ groupId: collapsible ? n.id : undefined,
829
+ collapsible,
830
+ collapsed: collapsible && collapsedColumnGroups.has(n.id),
738
831
  };
739
832
  });
740
833
  rows.push({ id: `gh_${d}`, cells });
@@ -772,6 +865,13 @@ export function createSvGridController<
772
865
  let colDropOnId = $state<string | null>(null);
773
866
  let colDropSide = $state<"before" | "after" | null>(null);
774
867
 
868
+ // Live drag state for managed row dragging. Only meaningful while a row is
869
+ // being dragged (`props.rowDragManaged`). `rowDropIndex` is the visible row
870
+ // index currently hovered; `rowDropSide` says which edge the drop line paints.
871
+ let rowDragActive = $state<boolean>(false);
872
+ let rowDropIndex = $state<number | null>(null);
873
+ let rowDropSide = $state<"before" | "after" | null>(null);
874
+
775
875
 
776
876
 
777
877
 
@@ -800,13 +900,13 @@ export function createSvGridController<
800
900
  );
801
901
  if (!needs) continue;
802
902
  const def = column.columnDef;
803
- const accessorFn = def.accessorFn;
903
+ const fieldFn = def.fieldFn;
804
904
  const field = def.field;
805
905
  const stat = computeColumnStat(
806
906
  (function* () {
807
907
  for (const row of allRows) {
808
- yield accessorFn
809
- ? accessorFn(row.original)
908
+ yield fieldFn
909
+ ? fieldFn(row.original)
810
910
  : field
811
911
  ? (row.original as Record<string, unknown>)[field]
812
912
  : row.getCellValueByColumnId(column.id);
@@ -872,29 +972,44 @@ export function createSvGridController<
872
972
  );
873
973
  }
874
974
 
875
- const menuFilters = Object.entries(filterMenuValues).filter(
876
- ([_, filter]) => {
877
- if (filter.operator === "isBlank") return true;
878
- // `between` needs both endpoints; otherwise treat as inactive.
879
- if (filter.operator === "between") {
880
- return (
881
- filter.value.trim().length > 0 &&
882
- (filter.valueTo ?? "").trim().length > 0
883
- );
884
- }
885
- return filter.value.trim().length > 0;
886
- },
887
- );
975
+ // A single condition is "active" if it has the value(s) it needs.
976
+ const condActive = (op: FilterOperator, value: string, valueTo?: string): boolean => {
977
+ if (op === "isBlank") return true;
978
+ if (op === "between") return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
979
+ return value.trim().length > 0;
980
+ };
981
+ const evalCond = (
982
+ cellValue: unknown,
983
+ columnId: string,
984
+ op: FilterOperator,
985
+ value: string,
986
+ valueTo?: string,
987
+ ): boolean =>
988
+ applyExcelFilter(
989
+ cellValue,
990
+ { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined },
991
+ { locale: props.filterLocale },
992
+ );
993
+ // A column filter is active if either of its (up to two) conditions is.
994
+ const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
995
+ const a = condActive(f.operator, f.value, f.valueTo);
996
+ const b = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
997
+ return a || b;
998
+ });
888
999
  if (menuFilters.length) {
889
1000
  rows = rows.filter((row) =>
890
- menuFilters.every(([columnId, filter]) =>
891
- applyExcelFilter(getRowColumnValue(row, columnId), {
892
- id: columnId,
893
- operator: filter.operator,
894
- value: filter.value,
895
- valueTo: filter.operator === "between" ? filter.valueTo : undefined,
896
- }, { locale: props.filterLocale }),
897
- ),
1001
+ menuFilters.every(([columnId, f]) => {
1002
+ const cellValue = getRowColumnValue(row, columnId);
1003
+ const aActive = condActive(f.operator, f.value, f.valueTo);
1004
+ const bActive = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
1005
+ const ra = aActive ? evalCond(cellValue, columnId, f.operator, f.value, f.valueTo) : null;
1006
+ const rb = bActive
1007
+ ? evalCond(cellValue, columnId, f.operator2 as FilterOperator, f.value2 ?? "", f.valueTo2)
1008
+ : null;
1009
+ if (ra === null) return rb ?? true;
1010
+ if (rb === null) return ra;
1011
+ return f.join === "OR" ? ra || rb : ra && rb;
1012
+ }),
898
1013
  );
899
1014
  }
900
1015
 
@@ -1040,7 +1155,10 @@ export function createSvGridController<
1040
1155
 
1041
1156
 
1042
1157
  // ---- Tool panel (docked columns sidebar) -------------------------------
1043
- let toolPanelOpen = $state(false);
1158
+ // svelte-ignore state_referenced_locally
1159
+ let toolPanelOpen = $state(props.toolPanelDefaultOpen === true);
1160
+ // svelte-ignore state_referenced_locally
1161
+ let toolPanelTab = $state<"columns" | "filters">(props.toolPanelDefaultTab ?? "columns");
1044
1162
  const toolPanelEnabled = $derived(props.toolPanel === true);
1045
1163
  // Every column (including hidden ones) in the user's current order, so the
1046
1164
  // panel can toggle/reorder anything. Group columns are flagged live.
@@ -1393,6 +1511,7 @@ export function createSvGridController<
1393
1511
  if (isFirstRender) return;
1394
1512
 
1395
1513
  selectionRange = { anchor: null, focus: null };
1514
+ selectionRanges = [];
1396
1515
  editingCell = null;
1397
1516
  if (scrollContainer) {
1398
1517
  scrollContainer.scrollTop = 0;
@@ -1773,7 +1892,9 @@ export function createSvGridController<
1773
1892
  });
1774
1893
 
1775
1894
  const columnMenuFacetValues = $derived.by(() => {
1776
- const columnId = filterMenuFor;
1895
+ // The funnel popover drives via `filterMenuFor`; the column menu's Filter
1896
+ // tab drives via `columnMenuFor`. Support whichever is open.
1897
+ const columnId = filterMenuFor ?? columnMenuFor;
1777
1898
  if (!columnId) return [] as Array<string>;
1778
1899
  const column = allColumns.find((entry) => entry.id === columnId);
1779
1900
  if (!column) return [] as Array<string>;
@@ -1853,6 +1974,8 @@ export function createSvGridController<
1853
1974
  set scrollSyncRaf(v) { scrollSyncRaf = v as never; },
1854
1975
  get selectionRange() { return selectionRange; },
1855
1976
  set selectionRange(v) { selectionRange = v as never; },
1977
+ get selectionRanges() { return selectionRanges; },
1978
+ set selectionRanges(v) { selectionRanges = v as never; },
1856
1979
  get isDraggingSelection() { return isDraggingSelection; },
1857
1980
  set isDraggingSelection(v) { isDraggingSelection = v as never; },
1858
1981
  get fillDrag() { return fillDrag; },
@@ -1861,6 +1984,8 @@ export function createSvGridController<
1861
1984
  set activeAtPointerDown(v) { activeAtPointerDown = v as never; },
1862
1985
  get editingCell() { return editingCell; },
1863
1986
  set editingCell(v) { editingCell = v as never; },
1987
+ get fullRowEdit() { return fullRowEdit; },
1988
+ set fullRowEdit(v) { fullRowEdit = v as never; },
1864
1989
  get editedCellValues() { return editedCellValues; },
1865
1990
  set editedCellValues(v) { editedCellValues = v as never; },
1866
1991
  get UNDO_LIMIT() { return UNDO_LIMIT; },
@@ -1913,6 +2038,8 @@ export function createSvGridController<
1913
2038
  get DATE_OPERATORS() { return DATE_OPERATORS; },
1914
2039
  get CHECKBOX_OPERATORS() { return CHECKBOX_OPERATORS; },
1915
2040
  get columnMenuFor() { return columnMenuFor; },
2041
+ get columnMenuTab() { return columnMenuTab; },
2042
+ set columnMenuTab(v) { columnMenuTab = v as never; },
1916
2043
  set columnMenuFor(v) { columnMenuFor = v as never; },
1917
2044
  get columnMenuPos() { return columnMenuPos; },
1918
2045
  set columnMenuPos(v) { columnMenuPos = v as never; },
@@ -1958,6 +2085,8 @@ export function createSvGridController<
1958
2085
  set internalColumns(v) { internalColumns = v as never; },
1959
2086
  get hiddenColumns() { return hiddenColumns; },
1960
2087
  set hiddenColumns(v) { hiddenColumns = v as never; },
2088
+ get toggleColumnGroup() { return toggleColumnGroup; },
2089
+ get isColumnGroupCollapsed() { return isColumnGroupCollapsed; },
1961
2090
  get externalSortEnabled() { return externalSortEnabled; },
1962
2091
  get externalFilterEnabled() { return externalFilterEnabled; },
1963
2092
  get passthroughSortedRowModel() { return passthroughSortedRowModel; },
@@ -1979,6 +2108,20 @@ export function createSvGridController<
1979
2108
  set colDropOnId(v) { colDropOnId = v as never; },
1980
2109
  get colDropSide() { return colDropSide; },
1981
2110
  set colDropSide(v) { colDropSide = v as never; },
2111
+ get rowDragActive() { return rowDragActive; },
2112
+ set rowDragActive(v) { rowDragActive = v as never; },
2113
+ get rowDropIndex() { return rowDropIndex; },
2114
+ set rowDropIndex(v) { rowDropIndex = v as never; },
2115
+ get rowDropSide() { return rowDropSide; },
2116
+ set rowDropSide(v) { rowDropSide = v as never; },
2117
+ get onRowDragStart() { return onRowDragStart; },
2118
+ get onRowDragOver() { return onRowDragOver; },
2119
+ get onRowDragLeave() { return onRowDragLeave; },
2120
+ get onRowDrop() { return onRowDrop; },
2121
+ get onRowsContainerDragOver() { return onRowsContainerDragOver; },
2122
+ get onRowsContainerDrop() { return onRowsContainerDrop; },
2123
+ get onRowDragEnd() { return onRowDragEnd; },
2124
+ get broadcastAlignedScroll() { return broadcastAlignedScroll; },
1982
2125
  get getCurrentColumnOrder() { return getCurrentColumnOrder; },
1983
2126
  get emitColumnOrder() { return emitColumnOrder; },
1984
2127
  get setColumnOrderInternal() { return setColumnOrderInternal; },
@@ -2014,6 +2157,8 @@ export function createSvGridController<
2014
2157
  get statusBarStats() { return statusBarStats; },
2015
2158
  get toolPanelOpen() { return toolPanelOpen; },
2016
2159
  set toolPanelOpen(v) { toolPanelOpen = v as never; },
2160
+ get toolPanelTab() { return toolPanelTab; },
2161
+ set toolPanelTab(v) { toolPanelTab = v as never; },
2017
2162
  get toolPanelEnabled() { return toolPanelEnabled; },
2018
2163
  get toolPanelColumns() { return toolPanelColumns; },
2019
2164
  get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
@@ -2096,6 +2241,7 @@ export function createSvGridController<
2096
2241
  get extendSelection() { return extendSelection; },
2097
2242
  get isCellInSelectedRange() { return isCellInSelectedRange; },
2098
2243
  get getCellRangeEdges() { return getCellRangeEdges; },
2244
+ get getSelectionRects() { return getSelectionRects; },
2099
2245
  get fillHandleCell() { return fillHandleCell; },
2100
2246
  get isInFillPreview() { return isInFillPreview; },
2101
2247
  get findColumnById() { return findColumnById; },
@@ -2120,6 +2266,12 @@ export function createSvGridController<
2120
2266
  get clearSelectedCells() { return clearSelectedCells; },
2121
2267
  get onCellDoubleClick() { return onCellDoubleClick; },
2122
2268
  get startEditingWithChar() { return startEditingWithChar; },
2269
+ get startEditing() { return startEditing; },
2270
+ get stopEditing() { return stopEditing; },
2271
+ get startFullRowEdit() { return startFullRowEdit; },
2272
+ get setFullRowDraft() { return setFullRowDraft; },
2273
+ get commitFullRowEdit() { return commitFullRowEdit; },
2274
+ get cancelFullRowEdit() { return cancelFullRowEdit; },
2123
2275
  get saveEditingCell() { return saveEditingCell; },
2124
2276
  get applyHistoryStep() { return applyHistoryStep; },
2125
2277
  get updateEditingCellValue() { return updateEditingCellValue; },
@@ -2186,10 +2338,26 @@ export function createSvGridController<
2186
2338
  const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
2187
2339
  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<TFeatures, TData>(ctx);
2188
2340
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
2189
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
2190
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
2341
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
2342
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
2191
2343
  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<TFeatures, TData>(ctx);
2344
+ const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd } = createRowDrag<TFeatures, TData>(ctx);
2345
+ const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
2192
2346
  const { buildApi } = createGridApi<TFeatures, TData>(ctx);
2193
2347
  const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
2348
+
2349
+ // Aligned grids: register in the shared group on mount, and mirror column
2350
+ // resizes to peers whenever columnWidths changes. Horizontal-scroll mirroring
2351
+ // is driven from onBodyScroll (via ctx.broadcastAlignedScroll).
2352
+ $effect(() => {
2353
+ if (props.alignedGridGroup == null) return;
2354
+ return registerAlignedGrid();
2355
+ });
2356
+ $effect(() => {
2357
+ // Track columnWidths reactively, then broadcast to aligned peers.
2358
+ void columnWidths;
2359
+ broadcastAlignedWidths();
2360
+ });
2361
+
2194
2362
  return ctx;
2195
2363
  }