@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
package/src/SvGrid.svelte CHANGED
@@ -3,53 +3,48 @@
3
3
  generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData"
4
4
  >
5
5
  import {
6
- getGridCellA11yProps,
7
- getGridCellDomId,
8
- getGridHeaderA11yProps,
9
- getGridRootA11yProps,
10
- getGridRowA11yProps,
11
- type EditorContext,
12
- type CellEditorOption,
13
- type Column,
14
- type Row,
15
- type RowData,
16
- type TableFeatures,
17
- } from "./index";
6
+ getGridCellA11yProps,
7
+ getGridCellDomId,
8
+ getGridHeaderA11yProps,
9
+ getGridRootA11yProps,
10
+ getGridRowA11yProps,
11
+ type EditorContext,
12
+ type CellEditorOption,
13
+ type Column,
14
+ type Row,
15
+ type RowData,
16
+ type TableFeatures,
17
+ } from "./index";
18
18
  import "./sv-grid-scrollbar";
19
19
  import "./SvGrid.css";
20
20
  import type { Snippet } from "svelte";
21
21
  import {
22
- RenderSnippetConfig,
23
- RenderComponentConfig,
24
- } from "./render-component";
25
- import {
26
- buildSparkline,
27
- toSparklineValues,
28
- } from "./sparkline";
22
+ RenderSnippetConfig,
23
+ RenderComponentConfig,
24
+ } from "./render-component";
25
+ import { buildSparkline, toSparklineValues } from "./sparkline";
29
26
  import SvGridDropdown from "./SvGridDropdown.svelte";
30
27
  import type {
31
- Props,
32
- SelectionPoint,
33
- SelectionRange,
34
- CellEditState,
35
- FilterOperator,
36
- FilterOption,
37
- MenuPosition,
38
- } from "./SvGrid.types";
39
- import {
40
- cfTextStyle,
41
- fmtStat,
42
- getEditableInputValue,
43
- getEditorInputType,
44
- toValueArray,
45
- getOptionLabel,
46
- getOptionColor,
47
- colorfulChipStyle,
48
- getEditorClass,
49
- } from "./SvGrid.helpers";
28
+ Props,
29
+ SelectionPoint,
30
+ SelectionRange,
31
+ CellEditState,
32
+ FilterOperator,
33
+ FilterOption,
34
+ MenuPosition,
35
+ } from "./SvGrid.types";
50
36
  import {
51
- createSvGridController,
52
- } from "./SvGrid.controller.svelte";
37
+ cfTextStyle,
38
+ fmtStat,
39
+ getEditableInputValue,
40
+ getEditorInputType,
41
+ toValueArray,
42
+ getOptionLabel,
43
+ getOptionColor,
44
+ colorfulChipStyle,
45
+ getEditorClass,
46
+ } from "./SvGrid.helpers";
47
+ import { createSvGridController } from "./SvGrid.controller.svelte";
53
48
  import GridMenus from "./GridMenus.svelte";
54
49
  import GridFooter from "./GridFooter.svelte";
55
50
  let props: Props<TFeatures, TData> = $props();
@@ -78,7 +73,9 @@
78
73
  const hasVerticalOverflow = $derived(ctrl.hasVerticalOverflow);
79
74
  const showGlobalFilterEffective = $derived(ctrl.showGlobalFilterEffective);
80
75
  const showFilterRowEffective = $derived(ctrl.showFilterRowEffective);
81
- const showInlineColumnFilterEffective = $derived(ctrl.showInlineColumnFilterEffective);
76
+ const showInlineColumnFilterEffective = $derived(
77
+ ctrl.showInlineColumnFilterEffective,
78
+ );
82
79
  const showRowSelectionEffective = $derived(ctrl.showRowSelectionEffective);
83
80
  const grid = $derived(ctrl.grid);
84
81
  const allColumns = $derived(ctrl.allColumns);
@@ -94,6 +91,36 @@
94
91
  const onColumnHeaderDragLeave = $derived(ctrl.onColumnHeaderDragLeave);
95
92
  const onColumnHeaderDrop = $derived(ctrl.onColumnHeaderDrop);
96
93
  const onColumnHeaderDragEnd = $derived(ctrl.onColumnHeaderDragEnd);
94
+ // Managed row dragging
95
+ const rowDragManagedEffective = $derived(props.rowDragManaged === true);
96
+ const rowDropIndex = $derived(ctrl.rowDropIndex);
97
+ const rowDropSide = $derived(ctrl.rowDropSide);
98
+ const onRowDragStart = $derived(ctrl.onRowDragStart);
99
+ const onRowDragOver = $derived(ctrl.onRowDragOver);
100
+ const onRowDragLeave = $derived(ctrl.onRowDragLeave);
101
+ const onRowDrop = $derived(ctrl.onRowDrop);
102
+ const onRowsContainerDragOver = $derived(ctrl.onRowsContainerDragOver);
103
+ const onRowsContainerDrop = $derived(ctrl.onRowsContainerDrop);
104
+ const onRowDragEndHandler = $derived(ctrl.onRowDragEnd);
105
+ // Drag attributes spread onto each body <tr>; empty object when disabled so
106
+ // rows stay non-draggable and no handlers fire.
107
+ function rowDragAttrs(rowIndex: number) {
108
+ if (!rowDragManagedEffective) return {};
109
+ return {
110
+ draggable: true,
111
+ ondragstart: (e: DragEvent) => onRowDragStart(e, rowIndex),
112
+ ondragover: (e: DragEvent) => onRowDragOver(e, rowIndex),
113
+ ondragleave: () => onRowDragLeave(rowIndex),
114
+ ondrop: (e: DragEvent) => onRowDrop(e, rowIndex),
115
+ ondragend: () => onRowDragEndHandler(),
116
+ };
117
+ }
118
+ function rowDropClass(rowIndex: number) {
119
+ if (!rowDragManagedEffective || rowDropIndex !== rowIndex) return "";
120
+ return rowDropSide === "after"
121
+ ? "sv-grid-row-drop-after"
122
+ : "sv-grid-row-drop-before";
123
+ }
97
124
  const getColumnBaseValue = $derived(ctrl.getColumnBaseValue);
98
125
  const hasConditionalFormats = $derived(ctrl.hasConditionalFormats);
99
126
  const cellConditionalFormat = $derived(ctrl.cellConditionalFormat);
@@ -114,7 +141,9 @@
114
141
  const toggleGroupInPanel = $derived(ctrl.toggleGroupInPanel);
115
142
  const virtualizer = $derived(ctrl.virtualizer);
116
143
  const rowVirtualizationEnabled = $derived(ctrl.rowVirtualizationEnabled);
117
- const columnVirtualizationEnabled = $derived(ctrl.columnVirtualizationEnabled);
144
+ const columnVirtualizationEnabled = $derived(
145
+ ctrl.columnVirtualizationEnabled,
146
+ );
118
147
  const virtualRows = $derived(ctrl.virtualRows);
119
148
  // DOM-space spacer heights + capped total: identical to the logical
120
149
  // virtualizer values for normal grids, scaled down past
@@ -125,6 +154,88 @@
125
154
  const rowDomTotalSize = $derived(ctrl.rowDomTotalSize);
126
155
  const renderedColumns = $derived(ctrl.renderedColumns);
127
156
  const totalColumnWidth = $derived(ctrl.totalColumnWidth);
157
+
158
+ // ---- Built-in cell flash (ColumnDef `cellFlash`) ----------------------
159
+ // Flashes a cell when its value changes (edits, streaming feeds, server
160
+ // pushes). Keyed by rowId so virtualization recycling a <td> into a new row
161
+ // on scroll does NOT trigger a spurious flash - only a same-row value change
162
+ // does. Inert (active:false) for columns without `cellFlash`.
163
+ function cellFlashAction(
164
+ node: HTMLElement,
165
+ params: {
166
+ rowId: string;
167
+ value: unknown;
168
+ active: boolean;
169
+ className: string;
170
+ },
171
+ ) {
172
+ let prevRow = params.rowId;
173
+ let prev = params.value;
174
+ return {
175
+ update(next: {
176
+ rowId: string;
177
+ value: unknown;
178
+ active: boolean;
179
+ className: string;
180
+ }) {
181
+ if (next.rowId !== prevRow) {
182
+ // A different row scrolled into this recycled slot - reset, no flash.
183
+ prevRow = next.rowId;
184
+ prev = next.value;
185
+ return;
186
+ }
187
+ if (next.active && next.className && !Object.is(next.value, prev)) {
188
+ node.classList.remove(next.className);
189
+ void node.offsetWidth; // reflow so the animation restarts
190
+ node.classList.add(next.className);
191
+ }
192
+ prev = next.value;
193
+ },
194
+ };
195
+ }
196
+ function flashClassFor(
197
+ cfg: boolean | { className?: string } | undefined,
198
+ ): string {
199
+ console.log("flash cell");
200
+ if (cfg && typeof cfg === "object" && cfg.className) return cfg.className;
201
+ return "sv-grid-cell-flash";
202
+ }
203
+
204
+ // ---- Full-row editing -------------------------------------------------
205
+ const fullRowEdit = $derived(ctrl.fullRowEdit);
206
+ // Commit the whole row when the user clicks away from its editors (Excel /
207
+ // AG-Grid feel). Clicking within any full-row editor keeps editing.
208
+ $effect(() => {
209
+ if (!fullRowEdit) return;
210
+ const onDown = (e: PointerEvent) => {
211
+ const t = e.target as HTMLElement | null;
212
+ if (t && t.closest(".sv-grid-fr-editor")) return;
213
+ ctrl.commitFullRowEdit();
214
+ };
215
+ document.addEventListener("pointerdown", onDown, true);
216
+ return () => document.removeEventListener("pointerdown", onDown, true);
217
+ });
218
+ function fullRowKeydown(e: KeyboardEvent) {
219
+ if (e.key === "Enter") {
220
+ e.preventDefault();
221
+ ctrl.commitFullRowEdit();
222
+ ctrl.gridRootEl?.focus({ preventScroll: true });
223
+ } else if (e.key === "Escape") {
224
+ e.preventDefault();
225
+ ctrl.cancelFullRowEdit();
226
+ ctrl.gridRootEl?.focus({ preventScroll: true });
227
+ }
228
+ }
229
+ function optionValueOf(o: unknown): string {
230
+ return typeof o === "object" && o !== null && "value" in o
231
+ ? String((o as { value: unknown }).value)
232
+ : String(o);
233
+ }
234
+ function optionLabelOf(o: unknown): string {
235
+ return typeof o === "object" && o !== null && "label" in o
236
+ ? String((o as { label: unknown }).label)
237
+ : String(o);
238
+ }
128
239
  const hasHorizontalOverflow = $derived(ctrl.hasHorizontalOverflow);
129
240
  const columnWindowStart = $derived(ctrl.columnWindowStart);
130
241
  const columnWindowRightSpacer = $derived(ctrl.columnWindowRightSpacer);
@@ -175,6 +286,11 @@
175
286
  const goToPage = $derived(ctrl.goToPage);
176
287
  const setPageSize = $derived(ctrl.setPageSize);
177
288
  const updateFilterRow = $derived(ctrl.updateFilterRow);
289
+ const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
290
+ const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
291
+ const updateFilterOperator = $derived(ctrl.updateFilterOperator);
292
+ const operatorsForColumn = $derived(ctrl.operatorsForColumn);
293
+ const clearColumnFilter = $derived(ctrl.clearColumnFilter);
178
294
  const toggleCheckboxWithKeyboard = $derived(ctrl.toggleCheckboxWithKeyboard);
179
295
  const operatorOption = $derived(ctrl.operatorOption);
180
296
  const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
@@ -442,7 +558,10 @@
442
558
  <!-- Custom editor slot. The columnDef provides a snippet that
443
559
  receives the editor context (value + commit + cancel) so the
444
560
  consumer fully owns the in-cell UI. -->
445
- {@const customEditor = column.columnDef.cellEditor as unknown as import('svelte').Snippet<[EditorContext<TData>]>}
561
+ {@const customEditor = column.columnDef
562
+ .cellEditor as unknown as import("svelte").Snippet<
563
+ [EditorContext<TData>]
564
+ >}
446
565
  {@render customEditor({
447
566
  cell: row.getAllCells().find((c) => c.column.id === column.id)!,
448
567
  row,
@@ -453,19 +572,23 @@
453
572
  update: (next: unknown) => {
454
573
  // Stage the draft without closing. Live-preview controls
455
574
  // (sliders, color pickers) call this on every input tick.
456
- ctrl.editingCell = ctrl.editingCell ? { ...ctrl.editingCell, value: next } : ctrl.editingCell
575
+ ctrl.editingCell = ctrl.editingCell
576
+ ? { ...ctrl.editingCell, value: next }
577
+ : ctrl.editingCell;
457
578
  },
458
579
  commit: (next?: unknown) => {
459
580
  // Write + close. If the caller passed a value, stage it
460
581
  // first; otherwise save whatever update() last wrote.
461
582
  if (next !== undefined) {
462
- ctrl.editingCell = ctrl.editingCell ? { ...ctrl.editingCell, value: next } : ctrl.editingCell
583
+ ctrl.editingCell = ctrl.editingCell
584
+ ? { ...ctrl.editingCell, value: next }
585
+ : ctrl.editingCell;
463
586
  }
464
- saveEditingCell()
587
+ saveEditingCell();
465
588
  },
466
589
  cancel: () => {
467
- ctrl.editingCell = null
468
- ctrl.gridRootEl?.focus({ preventScroll: true })
590
+ ctrl.editingCell = null;
591
+ ctrl.gridRootEl?.focus({ preventScroll: true });
469
592
  },
470
593
  })}
471
594
  {:else if ctrl.editingCell?.editorType === "checkbox"}
@@ -519,12 +642,11 @@
519
642
  {@const selectedArr = toValueArray(ctrl.editingCell?.value)}
520
643
  {@render chipsEditor(opts, multi, selectedArr)}
521
644
  {:else if ctrl.editingCell?.editorType === "rating"}
522
- {@const ratingVal = Math.max(0, Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)))}
523
- <span
524
- class="sv-grid-rating-editor"
525
- role="radiogroup"
526
- aria-label="Rating"
527
- >
645
+ {@const ratingVal = Math.max(
646
+ 0,
647
+ Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)),
648
+ )}
649
+ <span class="sv-grid-rating-editor" role="radiogroup" aria-label="Rating">
528
650
  {#each [1, 2, 3, 4, 5] as n (n)}
529
651
  <button
530
652
  type="button"
@@ -540,8 +662,8 @@
540
662
  : ctrl.editingCell;
541
663
  saveEditingCell();
542
664
  }}
543
- onkeydown={onEditorKeyDown}
544
- >★</button>
665
+ onkeydown={onEditorKeyDown}>★</button
666
+ >
545
667
  {/each}
546
668
  <button
547
669
  type="button"
@@ -554,8 +676,8 @@
554
676
  ? { ...ctrl.editingCell, value: 0 }
555
677
  : ctrl.editingCell;
556
678
  saveEditingCell();
557
- }}
558
- >×</button>
679
+ }}>×</button
680
+ >
559
681
  </span>
560
682
  {:else if ctrl.editingCell?.editorType === "select"}
561
683
  <!-- Custom dropdown: opens a themed popover identical in feel to
@@ -613,18 +735,21 @@
613
735
  (event.currentTarget as HTMLTextAreaElement).value,
614
736
  )}
615
737
  onkeydown={(event) => {
616
- event.stopPropagation()
738
+ event.stopPropagation();
617
739
  if (event.key === "Escape") {
618
- event.preventDefault()
619
- ctrl.editingCell = null
620
- ctrl.gridRootEl?.focus({ preventScroll: true })
621
- return
740
+ event.preventDefault();
741
+ ctrl.editingCell = null;
742
+ ctrl.gridRootEl?.focus({ preventScroll: true });
743
+ return;
622
744
  }
623
745
  // Tab and Ctrl/Cmd+Enter both commit. Plain Enter inserts a newline.
624
- if (event.key === "Tab" || (event.key === "Enter" && (event.ctrlKey || event.metaKey))) {
625
- event.preventDefault()
626
- saveEditingCell()
627
- ctrl.gridRootEl?.focus({ preventScroll: true })
746
+ if (
747
+ event.key === "Tab" ||
748
+ (event.key === "Enter" && (event.ctrlKey || event.metaKey))
749
+ ) {
750
+ event.preventDefault();
751
+ saveEditingCell();
752
+ ctrl.gridRootEl?.focus({ preventScroll: true });
628
753
  }
629
754
  }}
630
755
  onblur={() => saveEditingCell()}
@@ -668,8 +793,8 @@
668
793
  ? { ...ctrl.editingCell, value: opt.value }
669
794
  : ctrl.editingCell;
670
795
  saveEditingCell();
671
- }}
672
- >{opt.label}</button>
796
+ }}>{opt.label}</button
797
+ >
673
798
  {/each}
674
799
  </div>
675
800
  {/if}
@@ -717,6 +842,63 @@
717
842
  {/if}
718
843
  {/snippet}
719
844
 
845
+ <!-- Full-row editor: a lightweight inline editor per editable cell, shown
846
+ for every editable column of the row in full-row edit. Kept separate
847
+ from `editorBody` (which owns the 14 rich single-cell editors) so
848
+ cell editing is untouched. Covers the common editor types. -->
849
+ {#snippet fullRowEditor(column: Column<TData>, row: Row<TData>)}
850
+ {@const et = column.columnDef.editorType ?? "text"}
851
+ {@const val = fullRowEdit?.draft[column.id]}
852
+ {#if et === "checkbox"}
853
+ <input
854
+ type="checkbox"
855
+ class="sv-grid-fr-editor sv-grid-fr-checkbox"
856
+ checked={Boolean(val)}
857
+ onchange={(e) =>
858
+ ctrl.setFullRowDraft(column.id, e.currentTarget.checked)}
859
+ onkeydown={fullRowKeydown}
860
+ onpointerdown={(e) => e.stopPropagation()}
861
+ onclick={(e) => e.stopPropagation()}
862
+ />
863
+ {:else if et === "list" || et === "select" || et === "rich-select"}
864
+ {@const opts = getColumnEditorOptions(column, row)}
865
+ <select
866
+ class="sv-grid-cell-editor sv-grid-fr-editor"
867
+ value={String(val ?? "")}
868
+ onchange={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
869
+ onkeydown={fullRowKeydown}
870
+ onpointerdown={(e) => e.stopPropagation()}
871
+ onclick={(e) => e.stopPropagation()}
872
+ >
873
+ {#each opts as o (optionValueOf(o))}
874
+ <option value={optionValueOf(o)}>{optionLabelOf(o)}</option>
875
+ {/each}
876
+ </select>
877
+ {:else}
878
+ {@const inputType =
879
+ et === "number"
880
+ ? "number"
881
+ : et === "date"
882
+ ? "date"
883
+ : et === "datetime"
884
+ ? "datetime-local"
885
+ : et === "time"
886
+ ? "time"
887
+ : et === "password"
888
+ ? "password"
889
+ : "text"}
890
+ <input
891
+ type={inputType}
892
+ class="sv-grid-cell-editor sv-grid-fr-editor"
893
+ value={String(val ?? "")}
894
+ oninput={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
895
+ onkeydown={fullRowKeydown}
896
+ onpointerdown={(e) => e.stopPropagation()}
897
+ onclick={(e) => e.stopPropagation()}
898
+ />
899
+ {/if}
900
+ {/snippet}
901
+
720
902
  {#snippet chipsEditor(
721
903
  opts: CellEditorOption[],
722
904
  multi: boolean,
@@ -900,7 +1082,11 @@
900
1082
  inline editing, no row-selection checkbox, no fill handle.
901
1083
  Position-sticky CSS keeps it anchored to the top of the body or
902
1084
  the bottom of the viewport while the rest scrolls. -->
903
- {#snippet pinnedRowBody(rowData: TData, where: "top" | "bottom", index: number)}
1085
+ {#snippet pinnedRowBody(
1086
+ rowData: TData,
1087
+ where: "top" | "bottom",
1088
+ index: number,
1089
+ )}
904
1090
  <tr
905
1091
  class={`sv-grid-row sv-grid-pinned-row sv-grid-pinned-row-${where}`}
906
1092
  data-pinned-row={where}
@@ -910,7 +1096,8 @@
910
1096
  <td
911
1097
  class="sv-grid-cell sv-grid-row-number-cell"
912
1098
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
913
- >{where === "top" ? "↑" : "↓"}</td>
1099
+ >{where === "top" ? "↑" : "↓"}</td
1100
+ >
914
1101
  {/if}
915
1102
  {#if showRowSelectionEffective}
916
1103
  <td
@@ -927,13 +1114,17 @@
927
1114
  {/if}
928
1115
  {#each renderedColumns as rendered (rendered.column.id)}
929
1116
  {@const value = getPinnedCellValue(rowData, rendered.column)}
930
- {@const userCellClass = computePinnedCellClass(rowData, rendered.column)}
1117
+ {@const userCellClass = computePinnedCellClass(
1118
+ rowData,
1119
+ rendered.column,
1120
+ )}
931
1121
  <td
932
1122
  class={`sv-grid-cell ${userCellClass}`}
933
1123
  data-col-id={rendered.column.id}
934
1124
  data-pinned={isColumnPinned(rendered.column.id) ?? undefined}
935
1125
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
936
- >{formatPinnedValue(rendered.column, value)}</td>
1126
+ >{formatPinnedValue(rendered.column, value)}</td
1127
+ >
937
1128
  {/each}
938
1129
  {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
939
1130
  <td
@@ -956,13 +1147,45 @@
956
1147
  </label>
957
1148
  {/if}
958
1149
 
1150
+ {#if toolPanelEnabled}
1151
+ <div class="sv-grid-toolbar">
1152
+ <button
1153
+ type="button"
1154
+ class="sv-grid-toolbar-btn"
1155
+ class:is-active={ctrl.toolPanelOpen}
1156
+ aria-label={ctrl.toolPanelOpen
1157
+ ? "Close tool panel"
1158
+ : "Open tool panel (columns & filters)"}
1159
+ aria-expanded={ctrl.toolPanelOpen}
1160
+ onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1161
+ >
1162
+ <svg
1163
+ viewBox="0 0 24 24"
1164
+ width="15"
1165
+ height="15"
1166
+ fill="none"
1167
+ stroke="currentColor"
1168
+ stroke-width="2"
1169
+ stroke-linecap="round"
1170
+ stroke-linejoin="round"
1171
+ aria-hidden="true"
1172
+ >
1173
+ <rect x="3" y="4" width="6" height="16" rx="1" />
1174
+ <rect x="11" y="4" width="4" height="16" rx="1" />
1175
+ <rect x="17" y="4" width="4" height="16" rx="1" />
1176
+ </svg>
1177
+ Columns &amp; Filters
1178
+ </button>
1179
+ </div>
1180
+ {/if}
1181
+
959
1182
  <div
960
1183
  class="sv-grid-shell"
961
1184
  style={`height: ${
962
1185
  typeof props.containerHeight === "string"
963
1186
  ? props.containerHeight
964
1187
  : `${props.containerHeight ?? 520}px`
965
- }; --sg-thead-h: ${headerHeight}px; --sg-pinned-row-h: ${(typeof props.rowHeight === 'number' ? props.rowHeight : 30)}px;`}
1188
+ }; --sg-thead-h: ${headerHeight}px; --sg-pinned-row-h: ${typeof props.rowHeight === "number" ? props.rowHeight : 30}px;`}
966
1189
  >
967
1190
  <div
968
1191
  class="sv-grid-container sv-grid-container-custom-scrollbars"
@@ -1018,7 +1241,43 @@
1018
1241
  style={`width: ${cell.widthPx}px; min-width: ${cell.widthPx}px; max-width: ${cell.widthPx}px;`}
1019
1242
  >
1020
1243
  {#if !cell.isPlaceholder}
1021
- <span class="sv-grid-group-header-label">{cell.label}</span>
1244
+ {#if cell.collapsible}
1245
+ <button
1246
+ type="button"
1247
+ class="sv-grid-group-toggle"
1248
+ class:is-collapsed={cell.collapsed}
1249
+ aria-expanded={!cell.collapsed}
1250
+ aria-label={cell.collapsed
1251
+ ? `Expand ${cell.label}`
1252
+ : `Collapse ${cell.label}`}
1253
+ title={cell.collapsed
1254
+ ? "Expand group"
1255
+ : "Collapse group"}
1256
+ onclick={() => ctrl.toggleColumnGroup(cell.groupId!)}
1257
+ >
1258
+ <span class="sv-grid-group-header-label"
1259
+ >{cell.label}</span
1260
+ >
1261
+ <svg
1262
+ class="sv-grid-group-caret"
1263
+ viewBox="0 0 16 16"
1264
+ width="10"
1265
+ height="10"
1266
+ fill="none"
1267
+ stroke="currentColor"
1268
+ stroke-width="2.5"
1269
+ stroke-linecap="round"
1270
+ stroke-linejoin="round"
1271
+ aria-hidden="true"
1272
+ >
1273
+ <polyline points="4 6 8 10 12 6"></polyline>
1274
+ </svg>
1275
+ </button>
1276
+ {:else}
1277
+ <span class="sv-grid-group-header-label"
1278
+ >{cell.label}</span
1279
+ >
1280
+ {/if}
1022
1281
  {/if}
1023
1282
  </th>
1024
1283
  {/each}
@@ -1079,14 +1338,18 @@
1079
1338
  )}
1080
1339
  <th
1081
1340
  class="sv-grid-column"
1082
- class:is-drag-target-before={colDropOnId === header.column.id && colDropSide === "before"}
1083
- class:is-drag-target-after={colDropOnId === header.column.id && colDropSide === "after"}
1341
+ class:is-drag-target-before={colDropOnId ===
1342
+ header.column.id && colDropSide === "before"}
1343
+ class:is-drag-target-after={colDropOnId ===
1344
+ header.column.id && colDropSide === "after"}
1084
1345
  class:is-dragging={colDragId === header.column.id}
1085
1346
  data-svgrid-header-col={header.column.id}
1086
1347
  data-align={getColumnAlign(rendered.column)}
1087
1348
  data-pinned={isColumnPinned(rendered.column.id) ??
1088
1349
  undefined}
1089
- draggable={(props.enableColumnReorder ?? false) ? true : undefined}
1350
+ draggable={(props.enableColumnReorder ?? false)
1351
+ ? true
1352
+ : undefined}
1090
1353
  ondragstart={(e) =>
1091
1354
  (props.enableColumnReorder ?? false) &&
1092
1355
  onColumnHeaderDragStart(e, header.column.id)}
@@ -1116,7 +1379,9 @@
1116
1379
  {#if !header.isPlaceholder}
1117
1380
  <div class="sv-grid-header-cell">
1118
1381
  {#if typeof header.column.columnDef.header === "function"}
1119
- {@const rendered = header.column.columnDef.header(header.getContext())}
1382
+ {@const rendered = header.column.columnDef.header(
1383
+ header.getContext(),
1384
+ )}
1120
1385
  <!-- Custom header (snippet/component): rendered
1121
1386
  OUTSIDE the sort button so the consumer's
1122
1387
  own interactive elements (menu buttons,
@@ -1129,20 +1394,30 @@
1129
1394
  role="button"
1130
1395
  tabindex="-1"
1131
1396
  onclick={(event) => {
1132
- if (!header.column.getCanSort()) return
1397
+ if (!header.column.getCanSort()) return;
1133
1398
  // If the click landed on an interactive
1134
1399
  // element inside the custom header, let
1135
1400
  // that element handle it.
1136
- const t = event.target as HTMLElement | null
1137
- if (t && t.closest('button, a, input, select, textarea, [role="button"], [role="menuitem"]') &&
1138
- !(t.classList.contains('sv-grid-header-custom'))) return
1139
- onHeaderSortClick(event, header.column.id)
1401
+ const t = event.target as HTMLElement | null;
1402
+ if (
1403
+ t &&
1404
+ t.closest(
1405
+ 'button, a, input, select, textarea, [role="button"], [role="menuitem"]',
1406
+ ) &&
1407
+ !t.classList.contains("sv-grid-header-custom")
1408
+ )
1409
+ return;
1410
+ onHeaderSortClick(event, header.column.id);
1140
1411
  }}
1141
1412
  onkeydown={(event) => {
1142
- if (event.key !== 'Enter' && event.key !== ' ') return
1143
- if (!header.column.getCanSort()) return
1144
- event.preventDefault()
1145
- onHeaderSortClick(event as unknown as MouseEvent, header.column.id)
1413
+ if (event.key !== "Enter" && event.key !== " ")
1414
+ return;
1415
+ if (!header.column.getCanSort()) return;
1416
+ event.preventDefault();
1417
+ onHeaderSortClick(
1418
+ event as unknown as MouseEvent,
1419
+ header.column.id,
1420
+ );
1146
1421
  }}
1147
1422
  >
1148
1423
  {#if rendered instanceof RenderSnippetConfig}
@@ -1157,10 +1432,12 @@
1157
1432
  {#if header.column.getCanSort()}
1158
1433
  {#if sortDirection === "asc"}
1159
1434
  <span class="sv-grid-header-icon"
1160
- >{@render icon("sort-asc")}</span>
1435
+ >{@render icon("sort-asc")}</span
1436
+ >
1161
1437
  {:else if sortDirection === "desc"}
1162
1438
  <span class="sv-grid-header-icon"
1163
- >{@render icon("sort-desc")}</span>
1439
+ >{@render icon("sort-desc")}</span
1440
+ >
1164
1441
  {/if}
1165
1442
  {/if}
1166
1443
  </div>
@@ -1172,7 +1449,8 @@
1172
1449
  onHeaderSortClick(event, header.column.id)}
1173
1450
  >
1174
1451
  <span class="sv-grid-header-label">
1175
- {typeof header.column.columnDef.header === "string"
1452
+ {typeof header.column.columnDef.header ===
1453
+ "string"
1176
1454
  ? header.column.columnDef.header
1177
1455
  : header.id}
1178
1456
  </span>
@@ -1327,9 +1605,15 @@
1327
1605
  >
1328
1606
  </button>
1329
1607
  {#if activeOperator !== "isBlank"}
1608
+ {@const frType = getEditorInputType(
1609
+ rendered.column.columnDef.editorType ?? "text",
1610
+ )}
1330
1611
  <input
1331
1612
  class="sv-grid-filter-value"
1332
- placeholder="Filter rows"
1613
+ type={frType}
1614
+ placeholder={activeOperator === "between"
1615
+ ? "From"
1616
+ : "Filter…"}
1333
1617
  data-svgrid-filter-col={rendered.column.id}
1334
1618
  value={filterRowValues[rendered.column.id] ?? ""}
1335
1619
  oninput={(event) =>
@@ -1338,6 +1622,21 @@
1338
1622
  (event.currentTarget as HTMLInputElement).value,
1339
1623
  )}
1340
1624
  />
1625
+ {#if activeOperator === "between"}
1626
+ <input
1627
+ class="sv-grid-filter-value sv-grid-filter-value-to"
1628
+ type={frType}
1629
+ placeholder="To"
1630
+ value={filterMenuValues[rendered.column.id]
1631
+ ?.valueTo ?? ""}
1632
+ oninput={(event) =>
1633
+ updateFilterMenuValueTo(
1634
+ rendered.column.id,
1635
+ (event.currentTarget as HTMLInputElement)
1636
+ .value,
1637
+ )}
1638
+ />
1639
+ {/if}
1341
1640
  {/if}
1342
1641
  </div>
1343
1642
  </th>
@@ -1355,14 +1654,24 @@
1355
1654
  </thead>
1356
1655
  {#if props.pinnedTopRows && props.pinnedTopRows.length > 0}
1357
1656
  <!-- svelte-ignore a11y_no_redundant_roles -->
1358
- <tbody class="sv-grid-pinned sv-grid-pinned-top-body" role="rowgroup">
1657
+ <tbody
1658
+ class="sv-grid-pinned sv-grid-pinned-top-body"
1659
+ role="rowgroup"
1660
+ >
1359
1661
  {#each props.pinnedTopRows as r, i (i)}
1360
1662
  {@render pinnedRowBody(r, "top", i)}
1361
1663
  {/each}
1362
1664
  </tbody>
1363
1665
  {/if}
1364
1666
  <!-- svelte-ignore a11y_no_redundant_roles -->
1365
- <tbody class="sv-grid-body" role="rowgroup">
1667
+ <tbody
1668
+ class="sv-grid-body"
1669
+ role="rowgroup"
1670
+ ondragover={rowDragManagedEffective
1671
+ ? onRowsContainerDragOver
1672
+ : undefined}
1673
+ ondrop={rowDragManagedEffective ? onRowsContainerDrop : undefined}
1674
+ >
1366
1675
  {#if !allRows.length && !(props.loading && props.loadingOverlay)}
1367
1676
  <tr class="sv-grid-row sv-grid-empty-row">
1368
1677
  <td
@@ -1416,10 +1725,13 @@
1416
1725
  {:else}
1417
1726
  {@const userRowClass = computeRowClass(row, rowIndex)}
1418
1727
  <tr
1419
- class={`sv-grid-row ${userRowClass}`}
1728
+ class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1420
1729
  class:sv-grid-row-selected={isRowSelected(row.id)}
1421
- class:sv-grid-row-alt={props.zebraRows && rowIndex % 2 === 1}
1730
+ class:sv-grid-row-alt={props.zebraRows &&
1731
+ rowIndex % 2 === 1}
1732
+ class:sv-grid-row-draggable={rowDragManagedEffective}
1422
1733
  {...getGridRowA11yProps(rowIndex + 1)}
1734
+ {...rowDragAttrs(rowIndex)}
1423
1735
  style={`height: ${rowItem.size}px;`}
1424
1736
  >
1425
1737
  {#if showRowNumbersEffective}
@@ -1474,6 +1786,10 @@
1474
1786
  {@const isEditing =
1475
1787
  ctrl.editingCell?.rowId === row.id &&
1476
1788
  ctrl.editingCell?.columnId === rendered.column.id}
1789
+ {@const inRowEdit =
1790
+ !!fullRowEdit &&
1791
+ fullRowEdit.rowId === row.id &&
1792
+ rendered.column.id in fullRowEdit.draft}
1477
1793
  {@const rangeEdges = getCellRangeEdges(
1478
1794
  rowIndex,
1479
1795
  colIndex,
@@ -1482,12 +1798,21 @@
1482
1798
  fillHandleCell &&
1483
1799
  fillHandleCell.rowIndex === rowIndex &&
1484
1800
  fillHandleCell.colIndex === colIndex}
1485
- {@const userCellClass = computeCellClass(row, rendered.column)}
1486
- {@const cellTooltip = computeCellTooltip(row, rendered.column)}
1487
- {@const cellNote = computeCellNote(row, rendered.column)}
1801
+ {@const userCellClass = computeCellClass(
1802
+ row,
1803
+ rendered.column,
1804
+ )}
1805
+ {@const cellTooltip = computeCellTooltip(
1806
+ row,
1807
+ rendered.column,
1808
+ )}
1809
+ {@const cellNote = computeCellNote(
1810
+ row,
1811
+ rendered.column,
1812
+ )}
1488
1813
  <td
1489
1814
  class={`sv-grid-cell ${userCellClass}`}
1490
- class:sv-grid-cell-editing={isEditing}
1815
+ class:sv-grid-cell-editing={isEditing || inRowEdit}
1491
1816
  class:sv-grid-cell-active={activeCell.rowIndex ===
1492
1817
  rowIndex && activeCell.colIndex === colIndex}
1493
1818
  class:sv-grid-cell-has-fill-handle={hasFillHandle}
@@ -1522,7 +1847,20 @@
1522
1847
  emitCellDoubleClick(rowIndex, colIndex)}
1523
1848
  onclick={() => onCellClick(rowIndex, colIndex)}
1524
1849
  oncontextmenu={(event) =>
1525
- openContextMenu(event, rowIndex, colIndex, rendered.column.id)}
1850
+ openContextMenu(
1851
+ event,
1852
+ rowIndex,
1853
+ colIndex,
1854
+ rendered.column.id,
1855
+ )}
1856
+ use:cellFlashAction={{
1857
+ rowId: row.id,
1858
+ value: cellValue,
1859
+ active: !!rendered.column.columnDef.cellFlash,
1860
+ className: flashClassFor(
1861
+ rendered.column.columnDef.cellFlash,
1862
+ ),
1863
+ }}
1526
1864
  {...getGridCellA11yProps({
1527
1865
  id: getGridCellDomId("svgrid", rowIndex, colIndex),
1528
1866
  rowIndex: rowIndex + 1,
@@ -1530,10 +1868,16 @@
1530
1868
  selected: isRowSelected(row.id),
1531
1869
  })}
1532
1870
  >
1533
- {#if isEditing}
1871
+ {#if inRowEdit}
1872
+ {@render fullRowEditor(rendered.column, row)}
1873
+ {:else if isEditing}
1534
1874
  {@render editorBody(rendered.column, row)}
1535
1875
  {:else}
1536
- {@render cellBodyWithFormat(row, rendered.column, cellValue)}
1876
+ {@render cellBodyWithFormat(
1877
+ row,
1878
+ rendered.column,
1879
+ cellValue,
1880
+ )}
1537
1881
  {/if}
1538
1882
  {#if !isEditing && fillHandleCell && fillHandleCell.rowIndex === rowIndex && fillHandleCell.colIndex === colIndex}
1539
1883
  <!-- Excel-style fill handle: drag down/right to
@@ -1554,12 +1898,15 @@
1554
1898
  class="sv-grid-cell-note-corner"
1555
1899
  aria-label="Note"
1556
1900
  onpointerenter={(event) => {
1557
- event.stopPropagation()
1558
- showTooltipFor(event.currentTarget as HTMLElement, cellNote)
1901
+ event.stopPropagation();
1902
+ showTooltipFor(
1903
+ event.currentTarget as HTMLElement,
1904
+ cellNote,
1905
+ );
1559
1906
  }}
1560
1907
  onpointerleave={(event) => {
1561
- event.stopPropagation()
1562
- hideTooltip()
1908
+ event.stopPropagation();
1909
+ hideTooltip();
1563
1910
  }}
1564
1911
  ></span>
1565
1912
  {/if}
@@ -1614,10 +1961,13 @@
1614
1961
  {:else}
1615
1962
  {@const userRowClass = computeRowClass(row, rowIndex)}
1616
1963
  <tr
1617
- class={`sv-grid-row ${userRowClass}`}
1964
+ class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1618
1965
  class:sv-grid-row-selected={isRowSelected(row.id)}
1619
- class:sv-grid-row-alt={props.zebraRows && rowIndex % 2 === 1}
1966
+ class:sv-grid-row-alt={props.zebraRows &&
1967
+ rowIndex % 2 === 1}
1968
+ class:sv-grid-row-draggable={rowDragManagedEffective}
1620
1969
  {...getGridRowA11yProps(rowIndex + 1)}
1970
+ {...rowDragAttrs(rowIndex)}
1621
1971
  >
1622
1972
  {#if showRowNumbersEffective}
1623
1973
  <td
@@ -1671,16 +2021,26 @@
1671
2021
  {@const isEditing =
1672
2022
  ctrl.editingCell?.rowId === row.id &&
1673
2023
  ctrl.editingCell?.columnId === rendered.column.id}
2024
+ {@const inRowEdit =
2025
+ !!fullRowEdit &&
2026
+ fullRowEdit.rowId === row.id &&
2027
+ rendered.column.id in fullRowEdit.draft}
1674
2028
  {@const rangeEdges = getCellRangeEdges(
1675
2029
  rowIndex,
1676
2030
  colIndex,
1677
2031
  )}
1678
- {@const userCellClass = computeCellClass(row, rendered.column)}
1679
- {@const cellTooltip = computeCellTooltip(row, rendered.column)}
1680
- {@const cellNote = computeCellNote(row, rendered.column)}
2032
+ {@const userCellClass = computeCellClass(
2033
+ row,
2034
+ rendered.column,
2035
+ )}
2036
+ {@const cellTooltip = computeCellTooltip(
2037
+ row,
2038
+ rendered.column,
2039
+ )}
2040
+ {@const cellNote = computeCellNote(row, rendered.column)}
1681
2041
  <td
1682
2042
  class={`sv-grid-cell ${userCellClass}`}
1683
- class:sv-grid-cell-editing={isEditing}
2043
+ class:sv-grid-cell-editing={isEditing || inRowEdit}
1684
2044
  class:sv-grid-cell-active={activeCell.rowIndex ===
1685
2045
  rowIndex && activeCell.colIndex === colIndex}
1686
2046
  class:sv-grid-cell-cf={hasConditionalFormats}
@@ -1703,18 +2063,36 @@
1703
2063
  onpointerdown={(event) =>
1704
2064
  onCellPointerDown(rowIndex, colIndex, event)}
1705
2065
  onpointerenter={(event) => {
1706
- onCellPointerEnter(rowIndex, colIndex)
2066
+ onCellPointerEnter(rowIndex, colIndex);
1707
2067
  // Column tooltip fires on whole-cell hover.
1708
2068
  // Per-cell notes are gated on the corner hot-
1709
2069
  // zone below (Excel-style: hover the small
1710
2070
  // triangle to read the note).
1711
- if (cellTooltip) showTooltipFor(event.currentTarget as HTMLElement, cellTooltip)
2071
+ if (cellTooltip)
2072
+ showTooltipFor(
2073
+ event.currentTarget as HTMLElement,
2074
+ cellTooltip,
2075
+ );
1712
2076
  }}
1713
2077
  onpointerleave={hideTooltip}
1714
- ondblclick={() => emitCellDoubleClick(rowIndex, colIndex)}
2078
+ ondblclick={() =>
2079
+ emitCellDoubleClick(rowIndex, colIndex)}
1715
2080
  onclick={() => onCellClick(rowIndex, colIndex)}
1716
2081
  oncontextmenu={(event) =>
1717
- openContextMenu(event, rowIndex, colIndex, rendered.column.id)}
2082
+ openContextMenu(
2083
+ event,
2084
+ rowIndex,
2085
+ colIndex,
2086
+ rendered.column.id,
2087
+ )}
2088
+ use:cellFlashAction={{
2089
+ rowId: row.id,
2090
+ value: cellValue,
2091
+ active: !!rendered.column.columnDef.cellFlash,
2092
+ className: flashClassFor(
2093
+ rendered.column.columnDef.cellFlash,
2094
+ ),
2095
+ }}
1718
2096
  {...getGridCellA11yProps({
1719
2097
  id: getGridCellDomId("svgrid", rowIndex, colIndex),
1720
2098
  rowIndex: rowIndex + 1,
@@ -1722,10 +2100,16 @@
1722
2100
  selected: isRowSelected(row.id),
1723
2101
  })}
1724
2102
  >
1725
- {#if isEditing}
2103
+ {#if inRowEdit}
2104
+ {@render fullRowEditor(rendered.column, row)}
2105
+ {:else if isEditing}
1726
2106
  {@render editorBody(rendered.column, row)}
1727
2107
  {:else}
1728
- {@render cellBodyWithFormat(row, rendered.column, cellValue)}
2108
+ {@render cellBodyWithFormat(
2109
+ row,
2110
+ rendered.column,
2111
+ cellValue,
2112
+ )}
1729
2113
  {/if}
1730
2114
  {#if cellNote != null && !isEditing}
1731
2115
  <!-- Excel-style per-cell note indicator. The
@@ -1739,12 +2123,15 @@
1739
2123
  class="sv-grid-cell-note-corner"
1740
2124
  aria-label="Note"
1741
2125
  onpointerenter={(event) => {
1742
- event.stopPropagation()
1743
- showTooltipFor(event.currentTarget as HTMLElement, cellNote)
2126
+ event.stopPropagation();
2127
+ showTooltipFor(
2128
+ event.currentTarget as HTMLElement,
2129
+ cellNote,
2130
+ );
1744
2131
  }}
1745
2132
  onpointerleave={(event) => {
1746
- event.stopPropagation()
1747
- hideTooltip()
2133
+ event.stopPropagation();
2134
+ hideTooltip();
1748
2135
  }}
1749
2136
  ></span>
1750
2137
  {/if}
@@ -1764,7 +2151,10 @@
1764
2151
  </tbody>
1765
2152
  {#if props.pinnedBottomRows && props.pinnedBottomRows.length > 0}
1766
2153
  <!-- svelte-ignore a11y_no_redundant_roles -->
1767
- <tbody class="sv-grid-pinned sv-grid-pinned-bottom-body" role="rowgroup">
2154
+ <tbody
2155
+ class="sv-grid-pinned sv-grid-pinned-bottom-body"
2156
+ role="rowgroup"
2157
+ >
1768
2158
  {#each props.pinnedBottomRows as r, i (i)}
1769
2159
  {@render pinnedRowBody(r, "bottom", i)}
1770
2160
  {/each}
@@ -1866,7 +2256,7 @@
1866
2256
  content-size={scrollMetrics.scrollHeight ||
1867
2257
  rowDomTotalSize + headerHeight}
1868
2258
  value={scrollMetrics.scrollTop}
1869
- step={typeof props.rowHeight === 'number' ? props.rowHeight : 30}
2259
+ step={typeof props.rowHeight === "number" ? props.rowHeight : 30}
1870
2260
  style={`top: ${headerHeight}px; height: calc(100% - ${headerHeight + (hasHorizontalOverflow ? 16 : 0)}px);`}
1871
2261
  ></sv-grid-scrollbar>
1872
2262
  {/if}
@@ -1899,8 +2289,23 @@
1899
2289
  Enter cycles to the next hit; Esc closes. -->
1900
2290
  <div class="sv-grid-find" role="search" aria-label="Find in grid">
1901
2291
  <svg class="sv-grid-find-icon" viewBox="0 0 16 16" aria-hidden="true">
1902
- <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/>
1903
- <line x1="10.2" y1="10.2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
2292
+ <circle
2293
+ cx="7"
2294
+ cy="7"
2295
+ r="4.5"
2296
+ fill="none"
2297
+ stroke="currentColor"
2298
+ stroke-width="1.5"
2299
+ />
2300
+ <line
2301
+ x1="10.2"
2302
+ y1="10.2"
2303
+ x2="14"
2304
+ y2="14"
2305
+ stroke="currentColor"
2306
+ stroke-width="1.5"
2307
+ stroke-linecap="round"
2308
+ />
1904
2309
  </svg>
1905
2310
  <input
1906
2311
  class="sv-grid-find-input"
@@ -1910,40 +2315,73 @@
1910
2315
  bind:value={ctrl.findQuery}
1911
2316
  oninput={() => (ctrl.findHitIndex = 0)}
1912
2317
  onkeydown={(event) => {
1913
- event.stopPropagation()
1914
- if (event.key === 'Enter') {
1915
- event.preventDefault()
1916
- if (findHits.length === 0) return
1917
- ctrl.findHitIndex = (ctrl.findHitIndex + (event.shiftKey ? -1 : 1) + findHits.length) % findHits.length
1918
- const hit = findHits[ctrl.findHitIndex]
1919
- if (hit) { setActiveCell(hit.rowIndex, hit.colIndex); scrollActiveCellIntoView(hit.rowIndex, hit.colIndex) }
2318
+ event.stopPropagation();
2319
+ if (event.key === "Enter") {
2320
+ event.preventDefault();
2321
+ if (findHits.length === 0) return;
2322
+ ctrl.findHitIndex =
2323
+ (ctrl.findHitIndex +
2324
+ (event.shiftKey ? -1 : 1) +
2325
+ findHits.length) %
2326
+ findHits.length;
2327
+ const hit = findHits[ctrl.findHitIndex];
2328
+ if (hit) {
2329
+ setActiveCell(hit.rowIndex, hit.colIndex);
2330
+ scrollActiveCellIntoView(hit.rowIndex, hit.colIndex);
2331
+ }
2332
+ }
2333
+ if (event.key === "Escape") {
2334
+ event.preventDefault();
2335
+ ctrl.findOpen = false;
2336
+ ctrl.findQuery = "";
1920
2337
  }
1921
- if (event.key === 'Escape') { event.preventDefault(); ctrl.findOpen = false; ctrl.findQuery = '' }
1922
2338
  }}
1923
2339
  />
1924
2340
  <span class="sv-grid-find-count">
1925
2341
  {findHits.length === 0 && ctrl.findQuery.trim()
1926
- ? 'No matches'
2342
+ ? "No matches"
1927
2343
  : findHits.length === 0
1928
- ? ''
2344
+ ? ""
1929
2345
  : `${ctrl.findHitIndex + 1} of ${findHits.length}`}
1930
2346
  </span>
1931
- <button type="button" class="sv-grid-find-step" aria-label="Previous match"
2347
+ <button
2348
+ type="button"
2349
+ class="sv-grid-find-step"
2350
+ aria-label="Previous match"
1932
2351
  disabled={findHits.length === 0}
1933
2352
  onclick={() => {
1934
- ctrl.findHitIndex = (ctrl.findHitIndex - 1 + findHits.length) % findHits.length
1935
- const hit = findHits[ctrl.findHitIndex]
1936
- if (hit) { setActiveCell(hit.rowIndex, hit.colIndex); scrollActiveCellIntoView(hit.rowIndex, hit.colIndex) }
1937
- }}>↑</button>
1938
- <button type="button" class="sv-grid-find-step" aria-label="Next match"
2353
+ ctrl.findHitIndex =
2354
+ (ctrl.findHitIndex - 1 + findHits.length) % findHits.length;
2355
+ const hit = findHits[ctrl.findHitIndex];
2356
+ if (hit) {
2357
+ setActiveCell(hit.rowIndex, hit.colIndex);
2358
+ scrollActiveCellIntoView(hit.rowIndex, hit.colIndex);
2359
+ }
2360
+ }}>↑</button
2361
+ >
2362
+ <button
2363
+ type="button"
2364
+ class="sv-grid-find-step"
2365
+ aria-label="Next match"
1939
2366
  disabled={findHits.length === 0}
1940
2367
  onclick={() => {
1941
- ctrl.findHitIndex = (ctrl.findHitIndex + 1) % findHits.length
1942
- const hit = findHits[ctrl.findHitIndex]
1943
- if (hit) { setActiveCell(hit.rowIndex, hit.colIndex); scrollActiveCellIntoView(hit.rowIndex, hit.colIndex) }
1944
- }}>↓</button>
1945
- <button type="button" class="sv-grid-find-close" aria-label="Close find"
1946
- onclick={() => { ctrl.findOpen = false; ctrl.findQuery = '' }}>✕</button>
2368
+ ctrl.findHitIndex = (ctrl.findHitIndex + 1) % findHits.length;
2369
+ const hit = findHits[ctrl.findHitIndex];
2370
+ if (hit) {
2371
+ setActiveCell(hit.rowIndex, hit.colIndex);
2372
+ scrollActiveCellIntoView(hit.rowIndex, hit.colIndex);
2373
+ }
2374
+ }}>↓</button
2375
+ >
2376
+ <button
2377
+ type="button"
2378
+ class="sv-grid-find-close"
2379
+ aria-label="Close find"
2380
+ onclick={() => {
2381
+ ctrl.findOpen = false;
2382
+ ctrl.findQuery = "";
2383
+ }}>✕</button
2384
+ >
1947
2385
  </div>
1948
2386
  {/if}
1949
2387
 
@@ -1971,25 +2409,11 @@
1971
2409
  {/if}
1972
2410
 
1973
2411
  {#if toolPanelEnabled}
1974
- <button
1975
- type="button"
1976
- class="sv-grid-tool-panel-toggle"
1977
- class:is-open={ctrl.toolPanelOpen}
1978
- aria-label={ctrl.toolPanelOpen ? "Close columns panel" : "Open columns panel"}
1979
- aria-expanded={ctrl.toolPanelOpen}
1980
- onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1981
- title="Columns"
1982
- >
1983
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1984
- <rect x="3" y="4" width="6" height="16" rx="1" />
1985
- <rect x="11" y="4" width="4" height="16" rx="1" />
1986
- <rect x="17" y="4" width="4" height="16" rx="1" />
1987
- </svg>
1988
- </button>
1989
2412
  {#if ctrl.toolPanelOpen}
1990
- <aside class="sv-grid-tool-panel" aria-label="Columns tool panel">
2413
+ {@const panelTab = ctrl.toolPanelTab}
2414
+ <aside class="sv-grid-tool-panel" aria-label="Tool panel">
1991
2415
  <div class="sv-grid-tool-panel-head">
1992
- <span>Columns</span>
2416
+ <span>{panelTab === "filters" ? "Filters" : "Columns"}</span>
1993
2417
  <button
1994
2418
  type="button"
1995
2419
  class="sv-grid-tool-panel-close"
@@ -1997,46 +2421,144 @@
1997
2421
  onclick={() => (ctrl.toolPanelOpen = false)}>✕</button
1998
2422
  >
1999
2423
  </div>
2000
- <ul class="sv-grid-tool-panel-list">
2001
- {#each toolPanelColumns as column, i (column.id)}
2002
- {@const visible = !ctrl.hiddenColumns[column.id]}
2003
- {@const grouped = groupingColumns.includes(column.id)}
2004
- <li class="sv-grid-tool-panel-item">
2005
- <label class="sv-grid-tool-panel-vis">
2006
- <input
2007
- type="checkbox"
2008
- checked={visible}
2009
- onchange={() => toggleColumnVisibleInPanel(column.id)}
2010
- />
2011
- <span class="sv-grid-tool-panel-name">{toolPanelHeaderLabel(column)}</span>
2012
- </label>
2013
- <span class="sv-grid-tool-panel-actions">
2014
- <button
2015
- type="button"
2016
- class="sv-grid-tool-panel-btn"
2017
- class:is-active={grouped}
2018
- aria-label={grouped ? "Ungroup" : "Group by"}
2019
- title={grouped ? "Ungroup" : "Group by this column"}
2020
- onclick={() => toggleGroupInPanel(column.id)}>⊞</button
2021
- >
2022
- <button
2023
- type="button"
2024
- class="sv-grid-tool-panel-btn"
2025
- aria-label="Move up"
2026
- disabled={i === 0}
2027
- onclick={() => moveColumnInPanel(column.id, -1)}>↑</button
2028
- >
2029
- <button
2030
- type="button"
2031
- class="sv-grid-tool-panel-btn"
2032
- aria-label="Move down"
2033
- disabled={i === toolPanelColumns.length - 1}
2034
- onclick={() => moveColumnInPanel(column.id, 1)}>↓</button
2424
+ <div class="sv-grid-tool-panel-tabs" role="tablist">
2425
+ <button
2426
+ type="button"
2427
+ class="sv-grid-tool-panel-tab"
2428
+ class:is-active={panelTab === "columns"}
2429
+ role="tab"
2430
+ aria-selected={panelTab === "columns"}
2431
+ onclick={() => (ctrl.toolPanelTab = "columns")}>Columns</button
2432
+ >
2433
+ <button
2434
+ type="button"
2435
+ class="sv-grid-tool-panel-tab"
2436
+ class:is-active={panelTab === "filters"}
2437
+ role="tab"
2438
+ aria-selected={panelTab === "filters"}
2439
+ onclick={() => (ctrl.toolPanelTab = "filters")}>Filters</button
2440
+ >
2441
+ </div>
2442
+ {#if panelTab === "columns"}
2443
+ <ul class="sv-grid-tool-panel-list">
2444
+ {#each toolPanelColumns as column, i (column.id)}
2445
+ {@const visible = !ctrl.hiddenColumns[column.id]}
2446
+ {@const grouped = groupingColumns.includes(column.id)}
2447
+ <li class="sv-grid-tool-panel-item">
2448
+ <label class="sv-grid-tool-panel-vis">
2449
+ <input
2450
+ type="checkbox"
2451
+ checked={visible}
2452
+ onchange={() => toggleColumnVisibleInPanel(column.id)}
2453
+ />
2454
+ <span class="sv-grid-tool-panel-name"
2455
+ >{toolPanelHeaderLabel(column)}</span
2456
+ >
2457
+ </label>
2458
+ <span class="sv-grid-tool-panel-actions">
2459
+ <button
2460
+ type="button"
2461
+ class="sv-grid-tool-panel-btn"
2462
+ class:is-active={grouped}
2463
+ aria-label={grouped ? "Ungroup" : "Group by"}
2464
+ title={grouped ? "Ungroup" : "Group by this column"}
2465
+ onclick={() => toggleGroupInPanel(column.id)}>⊞</button
2466
+ >
2467
+ <button
2468
+ type="button"
2469
+ class="sv-grid-tool-panel-btn"
2470
+ aria-label="Move up"
2471
+ disabled={i === 0}
2472
+ onclick={() => moveColumnInPanel(column.id, -1)}>↑</button
2473
+ >
2474
+ <button
2475
+ type="button"
2476
+ class="sv-grid-tool-panel-btn"
2477
+ aria-label="Move down"
2478
+ disabled={i === toolPanelColumns.length - 1}
2479
+ onclick={() => moveColumnInPanel(column.id, 1)}>↓</button
2480
+ >
2481
+ </span>
2482
+ </li>
2483
+ {/each}
2484
+ </ul>
2485
+ {:else}
2486
+ <!-- Filters tab: one filter control per filterable column, sharing
2487
+ the same filterMenuValues state as the column menu / filter row. -->
2488
+ <div class="sv-grid-tool-panel-filters">
2489
+ {#each toolPanelColumns as column (column.id)}
2490
+ {#if column.columnDef.field && column.columnDef.filterable !== false}
2491
+ {@const active =
2492
+ filterMenuValues[column.id]?.operator ??
2493
+ defaultOperatorFor(column)}
2494
+ {@const fType = getEditorInputType(
2495
+ column.columnDef.editorType ?? "text",
2496
+ )}
2497
+ <div
2498
+ class="sv-grid-tp-filter"
2499
+ class:is-filtered={isColumnFiltered(column.id)}
2035
2500
  >
2036
- </span>
2037
- </li>
2038
- {/each}
2039
- </ul>
2501
+ <div class="sv-grid-tp-filter-head">
2502
+ <span class="sv-grid-tp-filter-name"
2503
+ >{toolPanelHeaderLabel(column)}</span
2504
+ >
2505
+ {#if isColumnFiltered(column.id)}
2506
+ <button
2507
+ type="button"
2508
+ class="sv-grid-tp-filter-clear"
2509
+ aria-label={`Clear ${toolPanelHeaderLabel(column)} filter`}
2510
+ title="Clear filter"
2511
+ onclick={() => clearColumnFilter(column.id)}>✕</button
2512
+ >
2513
+ {/if}
2514
+ </div>
2515
+ <select
2516
+ class="sv-grid-tp-filter-op"
2517
+ aria-label={`${toolPanelHeaderLabel(column)} filter condition`}
2518
+ value={active}
2519
+ onchange={(e) =>
2520
+ updateFilterOperator(
2521
+ column.id,
2522
+ e.currentTarget.value as FilterOperator,
2523
+ )}
2524
+ >
2525
+ {#each operatorsForColumn(column) as option (option.value)}
2526
+ <option value={option.value}
2527
+ >{operatorOption(option.value).label}</option
2528
+ >
2529
+ {/each}
2530
+ </select>
2531
+ {#if active !== "isBlank"}
2532
+ <input
2533
+ class="sv-grid-tp-filter-input"
2534
+ type={fType}
2535
+ placeholder={active === "between" ? "From" : "Filter…"}
2536
+ value={filterMenuValues[column.id]?.value ?? ""}
2537
+ oninput={(e) =>
2538
+ updateFilterMenuValue(
2539
+ column.id,
2540
+ e.currentTarget.value,
2541
+ )}
2542
+ />
2543
+ {#if active === "between"}
2544
+ <input
2545
+ class="sv-grid-tp-filter-input"
2546
+ type={fType}
2547
+ placeholder="To"
2548
+ value={filterMenuValues[column.id]?.valueTo ?? ""}
2549
+ oninput={(e) =>
2550
+ updateFilterMenuValueTo(
2551
+ column.id,
2552
+ e.currentTarget.value,
2553
+ )}
2554
+ />
2555
+ {/if}
2556
+ {/if}
2557
+ </div>
2558
+ {/if}
2559
+ {/each}
2560
+ </div>
2561
+ {/if}
2040
2562
  </aside>
2041
2563
  {/if}
2042
2564
  {/if}