@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
@@ -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);
@@ -170,10 +281,16 @@
170
281
  const focusOnMount = $derived(ctrl.focusOnMount);
171
282
  const onHeaderSortClick = $derived(ctrl.onHeaderSortClick);
172
283
  const onGridKeyDown = $derived(ctrl.onGridKeyDown);
284
+ const onGridPaste = $derived(ctrl.onGridPaste);
173
285
  const changePage = $derived(ctrl.changePage);
174
286
  const goToPage = $derived(ctrl.goToPage);
175
287
  const setPageSize = $derived(ctrl.setPageSize);
176
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);
177
294
  const toggleCheckboxWithKeyboard = $derived(ctrl.toggleCheckboxWithKeyboard);
178
295
  const operatorOption = $derived(ctrl.operatorOption);
179
296
  const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
@@ -441,7 +558,10 @@
441
558
  <!-- Custom editor slot. The columnDef provides a snippet that
442
559
  receives the editor context (value + commit + cancel) so the
443
560
  consumer fully owns the in-cell UI. -->
444
- {@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
+ >}
445
565
  {@render customEditor({
446
566
  cell: row.getAllCells().find((c) => c.column.id === column.id)!,
447
567
  row,
@@ -452,19 +572,23 @@
452
572
  update: (next: unknown) => {
453
573
  // Stage the draft without closing. Live-preview controls
454
574
  // (sliders, color pickers) call this on every input tick.
455
- ctrl.editingCell = ctrl.editingCell ? { ...ctrl.editingCell, value: next } : ctrl.editingCell
575
+ ctrl.editingCell = ctrl.editingCell
576
+ ? { ...ctrl.editingCell, value: next }
577
+ : ctrl.editingCell;
456
578
  },
457
579
  commit: (next?: unknown) => {
458
580
  // Write + close. If the caller passed a value, stage it
459
581
  // first; otherwise save whatever update() last wrote.
460
582
  if (next !== undefined) {
461
- ctrl.editingCell = ctrl.editingCell ? { ...ctrl.editingCell, value: next } : ctrl.editingCell
583
+ ctrl.editingCell = ctrl.editingCell
584
+ ? { ...ctrl.editingCell, value: next }
585
+ : ctrl.editingCell;
462
586
  }
463
- saveEditingCell()
587
+ saveEditingCell();
464
588
  },
465
589
  cancel: () => {
466
- ctrl.editingCell = null
467
- ctrl.gridRootEl?.focus({ preventScroll: true })
590
+ ctrl.editingCell = null;
591
+ ctrl.gridRootEl?.focus({ preventScroll: true });
468
592
  },
469
593
  })}
470
594
  {:else if ctrl.editingCell?.editorType === "checkbox"}
@@ -518,12 +642,11 @@
518
642
  {@const selectedArr = toValueArray(ctrl.editingCell?.value)}
519
643
  {@render chipsEditor(opts, multi, selectedArr)}
520
644
  {:else if ctrl.editingCell?.editorType === "rating"}
521
- {@const ratingVal = Math.max(0, Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)))}
522
- <span
523
- class="sv-grid-rating-editor"
524
- role="radiogroup"
525
- aria-label="Rating"
526
- >
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">
527
650
  {#each [1, 2, 3, 4, 5] as n (n)}
528
651
  <button
529
652
  type="button"
@@ -539,8 +662,8 @@
539
662
  : ctrl.editingCell;
540
663
  saveEditingCell();
541
664
  }}
542
- onkeydown={onEditorKeyDown}
543
- >★</button>
665
+ onkeydown={onEditorKeyDown}>★</button
666
+ >
544
667
  {/each}
545
668
  <button
546
669
  type="button"
@@ -553,8 +676,8 @@
553
676
  ? { ...ctrl.editingCell, value: 0 }
554
677
  : ctrl.editingCell;
555
678
  saveEditingCell();
556
- }}
557
- >×</button>
679
+ }}>×</button
680
+ >
558
681
  </span>
559
682
  {:else if ctrl.editingCell?.editorType === "select"}
560
683
  <!-- Custom dropdown: opens a themed popover identical in feel to
@@ -612,18 +735,21 @@
612
735
  (event.currentTarget as HTMLTextAreaElement).value,
613
736
  )}
614
737
  onkeydown={(event) => {
615
- event.stopPropagation()
738
+ event.stopPropagation();
616
739
  if (event.key === "Escape") {
617
- event.preventDefault()
618
- ctrl.editingCell = null
619
- ctrl.gridRootEl?.focus({ preventScroll: true })
620
- return
740
+ event.preventDefault();
741
+ ctrl.editingCell = null;
742
+ ctrl.gridRootEl?.focus({ preventScroll: true });
743
+ return;
621
744
  }
622
745
  // Tab and Ctrl/Cmd+Enter both commit. Plain Enter inserts a newline.
623
- if (event.key === "Tab" || (event.key === "Enter" && (event.ctrlKey || event.metaKey))) {
624
- event.preventDefault()
625
- saveEditingCell()
626
- 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 });
627
753
  }
628
754
  }}
629
755
  onblur={() => saveEditingCell()}
@@ -667,8 +793,8 @@
667
793
  ? { ...ctrl.editingCell, value: opt.value }
668
794
  : ctrl.editingCell;
669
795
  saveEditingCell();
670
- }}
671
- >{opt.label}</button>
796
+ }}>{opt.label}</button
797
+ >
672
798
  {/each}
673
799
  </div>
674
800
  {/if}
@@ -716,6 +842,63 @@
716
842
  {/if}
717
843
  {/snippet}
718
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
+
719
902
  {#snippet chipsEditor(
720
903
  opts: CellEditorOption[],
721
904
  multi: boolean,
@@ -899,7 +1082,11 @@
899
1082
  inline editing, no row-selection checkbox, no fill handle.
900
1083
  Position-sticky CSS keeps it anchored to the top of the body or
901
1084
  the bottom of the viewport while the rest scrolls. -->
902
- {#snippet pinnedRowBody(rowData: TData, where: "top" | "bottom", index: number)}
1085
+ {#snippet pinnedRowBody(
1086
+ rowData: TData,
1087
+ where: "top" | "bottom",
1088
+ index: number,
1089
+ )}
903
1090
  <tr
904
1091
  class={`sv-grid-row sv-grid-pinned-row sv-grid-pinned-row-${where}`}
905
1092
  data-pinned-row={where}
@@ -909,7 +1096,8 @@
909
1096
  <td
910
1097
  class="sv-grid-cell sv-grid-row-number-cell"
911
1098
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
912
- >{where === "top" ? "↑" : "↓"}</td>
1099
+ >{where === "top" ? "↑" : "↓"}</td
1100
+ >
913
1101
  {/if}
914
1102
  {#if showRowSelectionEffective}
915
1103
  <td
@@ -926,13 +1114,17 @@
926
1114
  {/if}
927
1115
  {#each renderedColumns as rendered (rendered.column.id)}
928
1116
  {@const value = getPinnedCellValue(rowData, rendered.column)}
929
- {@const userCellClass = computePinnedCellClass(rowData, rendered.column)}
1117
+ {@const userCellClass = computePinnedCellClass(
1118
+ rowData,
1119
+ rendered.column,
1120
+ )}
930
1121
  <td
931
1122
  class={`sv-grid-cell ${userCellClass}`}
932
1123
  data-col-id={rendered.column.id}
933
1124
  data-pinned={isColumnPinned(rendered.column.id) ?? undefined}
934
1125
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
935
- >{formatPinnedValue(rendered.column, value)}</td>
1126
+ >{formatPinnedValue(rendered.column, value)}</td
1127
+ >
936
1128
  {/each}
937
1129
  {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
938
1130
  <td
@@ -955,13 +1147,45 @@
955
1147
  </label>
956
1148
  {/if}
957
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
+
958
1182
  <div
959
1183
  class="sv-grid-shell"
960
1184
  style={`height: ${
961
1185
  typeof props.containerHeight === "string"
962
1186
  ? props.containerHeight
963
1187
  : `${props.containerHeight ?? 520}px`
964
- }; --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;`}
965
1189
  >
966
1190
  <div
967
1191
  class="sv-grid-container sv-grid-container-custom-scrollbars"
@@ -978,6 +1202,7 @@
978
1202
  colCount: allColumns.length,
979
1203
  })}
980
1204
  onkeydown={onGridKeyDown}
1205
+ onpaste={onGridPaste}
981
1206
  style={`min-width: ${totalColumnWidth}px;`}
982
1207
  >
983
1208
  <!-- svelte-ignore a11y_no_redundant_roles -->
@@ -1016,7 +1241,43 @@
1016
1241
  style={`width: ${cell.widthPx}px; min-width: ${cell.widthPx}px; max-width: ${cell.widthPx}px;`}
1017
1242
  >
1018
1243
  {#if !cell.isPlaceholder}
1019
- <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}
1020
1281
  {/if}
1021
1282
  </th>
1022
1283
  {/each}
@@ -1077,14 +1338,18 @@
1077
1338
  )}
1078
1339
  <th
1079
1340
  class="sv-grid-column"
1080
- class:is-drag-target-before={colDropOnId === header.column.id && colDropSide === "before"}
1081
- 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"}
1082
1345
  class:is-dragging={colDragId === header.column.id}
1083
1346
  data-svgrid-header-col={header.column.id}
1084
1347
  data-align={getColumnAlign(rendered.column)}
1085
1348
  data-pinned={isColumnPinned(rendered.column.id) ??
1086
1349
  undefined}
1087
- draggable={(props.enableColumnReorder ?? false) ? true : undefined}
1350
+ draggable={(props.enableColumnReorder ?? false)
1351
+ ? true
1352
+ : undefined}
1088
1353
  ondragstart={(e) =>
1089
1354
  (props.enableColumnReorder ?? false) &&
1090
1355
  onColumnHeaderDragStart(e, header.column.id)}
@@ -1114,7 +1379,9 @@
1114
1379
  {#if !header.isPlaceholder}
1115
1380
  <div class="sv-grid-header-cell">
1116
1381
  {#if typeof header.column.columnDef.header === "function"}
1117
- {@const rendered = header.column.columnDef.header(header.getContext())}
1382
+ {@const rendered = header.column.columnDef.header(
1383
+ header.getContext(),
1384
+ )}
1118
1385
  <!-- Custom header (snippet/component): rendered
1119
1386
  OUTSIDE the sort button so the consumer's
1120
1387
  own interactive elements (menu buttons,
@@ -1127,20 +1394,30 @@
1127
1394
  role="button"
1128
1395
  tabindex="-1"
1129
1396
  onclick={(event) => {
1130
- if (!header.column.getCanSort()) return
1397
+ if (!header.column.getCanSort()) return;
1131
1398
  // If the click landed on an interactive
1132
1399
  // element inside the custom header, let
1133
1400
  // that element handle it.
1134
- const t = event.target as HTMLElement | null
1135
- if (t && t.closest('button, a, input, select, textarea, [role="button"], [role="menuitem"]') &&
1136
- !(t.classList.contains('sv-grid-header-custom'))) return
1137
- 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);
1138
1411
  }}
1139
1412
  onkeydown={(event) => {
1140
- if (event.key !== 'Enter' && event.key !== ' ') return
1141
- if (!header.column.getCanSort()) return
1142
- event.preventDefault()
1143
- 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
+ );
1144
1421
  }}
1145
1422
  >
1146
1423
  {#if rendered instanceof RenderSnippetConfig}
@@ -1155,10 +1432,12 @@
1155
1432
  {#if header.column.getCanSort()}
1156
1433
  {#if sortDirection === "asc"}
1157
1434
  <span class="sv-grid-header-icon"
1158
- >{@render icon("sort-asc")}</span>
1435
+ >{@render icon("sort-asc")}</span
1436
+ >
1159
1437
  {:else if sortDirection === "desc"}
1160
1438
  <span class="sv-grid-header-icon"
1161
- >{@render icon("sort-desc")}</span>
1439
+ >{@render icon("sort-desc")}</span
1440
+ >
1162
1441
  {/if}
1163
1442
  {/if}
1164
1443
  </div>
@@ -1170,7 +1449,8 @@
1170
1449
  onHeaderSortClick(event, header.column.id)}
1171
1450
  >
1172
1451
  <span class="sv-grid-header-label">
1173
- {typeof header.column.columnDef.header === "string"
1452
+ {typeof header.column.columnDef.header ===
1453
+ "string"
1174
1454
  ? header.column.columnDef.header
1175
1455
  : header.id}
1176
1456
  </span>
@@ -1325,9 +1605,15 @@
1325
1605
  >
1326
1606
  </button>
1327
1607
  {#if activeOperator !== "isBlank"}
1608
+ {@const frType = getEditorInputType(
1609
+ rendered.column.columnDef.editorType ?? "text",
1610
+ )}
1328
1611
  <input
1329
1612
  class="sv-grid-filter-value"
1330
- placeholder="Filter rows"
1613
+ type={frType}
1614
+ placeholder={activeOperator === "between"
1615
+ ? "From"
1616
+ : "Filter…"}
1331
1617
  data-svgrid-filter-col={rendered.column.id}
1332
1618
  value={filterRowValues[rendered.column.id] ?? ""}
1333
1619
  oninput={(event) =>
@@ -1336,6 +1622,21 @@
1336
1622
  (event.currentTarget as HTMLInputElement).value,
1337
1623
  )}
1338
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}
1339
1640
  {/if}
1340
1641
  </div>
1341
1642
  </th>
@@ -1353,14 +1654,24 @@
1353
1654
  </thead>
1354
1655
  {#if props.pinnedTopRows && props.pinnedTopRows.length > 0}
1355
1656
  <!-- svelte-ignore a11y_no_redundant_roles -->
1356
- <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
+ >
1357
1661
  {#each props.pinnedTopRows as r, i (i)}
1358
1662
  {@render pinnedRowBody(r, "top", i)}
1359
1663
  {/each}
1360
1664
  </tbody>
1361
1665
  {/if}
1362
1666
  <!-- svelte-ignore a11y_no_redundant_roles -->
1363
- <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
+ >
1364
1675
  {#if !allRows.length && !(props.loading && props.loadingOverlay)}
1365
1676
  <tr class="sv-grid-row sv-grid-empty-row">
1366
1677
  <td
@@ -1414,9 +1725,13 @@
1414
1725
  {:else}
1415
1726
  {@const userRowClass = computeRowClass(row, rowIndex)}
1416
1727
  <tr
1417
- class={`sv-grid-row ${userRowClass}`}
1728
+ class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1418
1729
  class:sv-grid-row-selected={isRowSelected(row.id)}
1730
+ class:sv-grid-row-alt={props.zebraRows &&
1731
+ rowIndex % 2 === 1}
1732
+ class:sv-grid-row-draggable={rowDragManagedEffective}
1419
1733
  {...getGridRowA11yProps(rowIndex + 1)}
1734
+ {...rowDragAttrs(rowIndex)}
1420
1735
  style={`height: ${rowItem.size}px;`}
1421
1736
  >
1422
1737
  {#if showRowNumbersEffective}
@@ -1471,6 +1786,10 @@
1471
1786
  {@const isEditing =
1472
1787
  ctrl.editingCell?.rowId === row.id &&
1473
1788
  ctrl.editingCell?.columnId === rendered.column.id}
1789
+ {@const inRowEdit =
1790
+ !!fullRowEdit &&
1791
+ fullRowEdit.rowId === row.id &&
1792
+ rendered.column.id in fullRowEdit.draft}
1474
1793
  {@const rangeEdges = getCellRangeEdges(
1475
1794
  rowIndex,
1476
1795
  colIndex,
@@ -1479,12 +1798,21 @@
1479
1798
  fillHandleCell &&
1480
1799
  fillHandleCell.rowIndex === rowIndex &&
1481
1800
  fillHandleCell.colIndex === colIndex}
1482
- {@const userCellClass = computeCellClass(row, rendered.column)}
1483
- {@const cellTooltip = computeCellTooltip(row, rendered.column)}
1484
- {@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
+ )}
1485
1813
  <td
1486
1814
  class={`sv-grid-cell ${userCellClass}`}
1487
- class:sv-grid-cell-editing={isEditing}
1815
+ class:sv-grid-cell-editing={isEditing || inRowEdit}
1488
1816
  class:sv-grid-cell-active={activeCell.rowIndex ===
1489
1817
  rowIndex && activeCell.colIndex === colIndex}
1490
1818
  class:sv-grid-cell-has-fill-handle={hasFillHandle}
@@ -1519,7 +1847,20 @@
1519
1847
  emitCellDoubleClick(rowIndex, colIndex)}
1520
1848
  onclick={() => onCellClick(rowIndex, colIndex)}
1521
1849
  oncontextmenu={(event) =>
1522
- 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
+ }}
1523
1864
  {...getGridCellA11yProps({
1524
1865
  id: getGridCellDomId("svgrid", rowIndex, colIndex),
1525
1866
  rowIndex: rowIndex + 1,
@@ -1527,10 +1868,16 @@
1527
1868
  selected: isRowSelected(row.id),
1528
1869
  })}
1529
1870
  >
1530
- {#if isEditing}
1871
+ {#if inRowEdit}
1872
+ {@render fullRowEditor(rendered.column, row)}
1873
+ {:else if isEditing}
1531
1874
  {@render editorBody(rendered.column, row)}
1532
1875
  {:else}
1533
- {@render cellBodyWithFormat(row, rendered.column, cellValue)}
1876
+ {@render cellBodyWithFormat(
1877
+ row,
1878
+ rendered.column,
1879
+ cellValue,
1880
+ )}
1534
1881
  {/if}
1535
1882
  {#if !isEditing && fillHandleCell && fillHandleCell.rowIndex === rowIndex && fillHandleCell.colIndex === colIndex}
1536
1883
  <!-- Excel-style fill handle: drag down/right to
@@ -1551,12 +1898,15 @@
1551
1898
  class="sv-grid-cell-note-corner"
1552
1899
  aria-label="Note"
1553
1900
  onpointerenter={(event) => {
1554
- event.stopPropagation()
1555
- showTooltipFor(event.currentTarget as HTMLElement, cellNote)
1901
+ event.stopPropagation();
1902
+ showTooltipFor(
1903
+ event.currentTarget as HTMLElement,
1904
+ cellNote,
1905
+ );
1556
1906
  }}
1557
1907
  onpointerleave={(event) => {
1558
- event.stopPropagation()
1559
- hideTooltip()
1908
+ event.stopPropagation();
1909
+ hideTooltip();
1560
1910
  }}
1561
1911
  ></span>
1562
1912
  {/if}
@@ -1611,9 +1961,13 @@
1611
1961
  {:else}
1612
1962
  {@const userRowClass = computeRowClass(row, rowIndex)}
1613
1963
  <tr
1614
- class={`sv-grid-row ${userRowClass}`}
1964
+ class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1615
1965
  class:sv-grid-row-selected={isRowSelected(row.id)}
1966
+ class:sv-grid-row-alt={props.zebraRows &&
1967
+ rowIndex % 2 === 1}
1968
+ class:sv-grid-row-draggable={rowDragManagedEffective}
1616
1969
  {...getGridRowA11yProps(rowIndex + 1)}
1970
+ {...rowDragAttrs(rowIndex)}
1617
1971
  >
1618
1972
  {#if showRowNumbersEffective}
1619
1973
  <td
@@ -1667,16 +2021,26 @@
1667
2021
  {@const isEditing =
1668
2022
  ctrl.editingCell?.rowId === row.id &&
1669
2023
  ctrl.editingCell?.columnId === rendered.column.id}
2024
+ {@const inRowEdit =
2025
+ !!fullRowEdit &&
2026
+ fullRowEdit.rowId === row.id &&
2027
+ rendered.column.id in fullRowEdit.draft}
1670
2028
  {@const rangeEdges = getCellRangeEdges(
1671
2029
  rowIndex,
1672
2030
  colIndex,
1673
2031
  )}
1674
- {@const userCellClass = computeCellClass(row, rendered.column)}
1675
- {@const cellTooltip = computeCellTooltip(row, rendered.column)}
1676
- {@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)}
1677
2041
  <td
1678
2042
  class={`sv-grid-cell ${userCellClass}`}
1679
- class:sv-grid-cell-editing={isEditing}
2043
+ class:sv-grid-cell-editing={isEditing || inRowEdit}
1680
2044
  class:sv-grid-cell-active={activeCell.rowIndex ===
1681
2045
  rowIndex && activeCell.colIndex === colIndex}
1682
2046
  class:sv-grid-cell-cf={hasConditionalFormats}
@@ -1699,18 +2063,36 @@
1699
2063
  onpointerdown={(event) =>
1700
2064
  onCellPointerDown(rowIndex, colIndex, event)}
1701
2065
  onpointerenter={(event) => {
1702
- onCellPointerEnter(rowIndex, colIndex)
2066
+ onCellPointerEnter(rowIndex, colIndex);
1703
2067
  // Column tooltip fires on whole-cell hover.
1704
2068
  // Per-cell notes are gated on the corner hot-
1705
2069
  // zone below (Excel-style: hover the small
1706
2070
  // triangle to read the note).
1707
- if (cellTooltip) showTooltipFor(event.currentTarget as HTMLElement, cellTooltip)
2071
+ if (cellTooltip)
2072
+ showTooltipFor(
2073
+ event.currentTarget as HTMLElement,
2074
+ cellTooltip,
2075
+ );
1708
2076
  }}
1709
2077
  onpointerleave={hideTooltip}
1710
- ondblclick={() => emitCellDoubleClick(rowIndex, colIndex)}
2078
+ ondblclick={() =>
2079
+ emitCellDoubleClick(rowIndex, colIndex)}
1711
2080
  onclick={() => onCellClick(rowIndex, colIndex)}
1712
2081
  oncontextmenu={(event) =>
1713
- 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
+ }}
1714
2096
  {...getGridCellA11yProps({
1715
2097
  id: getGridCellDomId("svgrid", rowIndex, colIndex),
1716
2098
  rowIndex: rowIndex + 1,
@@ -1718,10 +2100,16 @@
1718
2100
  selected: isRowSelected(row.id),
1719
2101
  })}
1720
2102
  >
1721
- {#if isEditing}
2103
+ {#if inRowEdit}
2104
+ {@render fullRowEditor(rendered.column, row)}
2105
+ {:else if isEditing}
1722
2106
  {@render editorBody(rendered.column, row)}
1723
2107
  {:else}
1724
- {@render cellBodyWithFormat(row, rendered.column, cellValue)}
2108
+ {@render cellBodyWithFormat(
2109
+ row,
2110
+ rendered.column,
2111
+ cellValue,
2112
+ )}
1725
2113
  {/if}
1726
2114
  {#if cellNote != null && !isEditing}
1727
2115
  <!-- Excel-style per-cell note indicator. The
@@ -1735,12 +2123,15 @@
1735
2123
  class="sv-grid-cell-note-corner"
1736
2124
  aria-label="Note"
1737
2125
  onpointerenter={(event) => {
1738
- event.stopPropagation()
1739
- showTooltipFor(event.currentTarget as HTMLElement, cellNote)
2126
+ event.stopPropagation();
2127
+ showTooltipFor(
2128
+ event.currentTarget as HTMLElement,
2129
+ cellNote,
2130
+ );
1740
2131
  }}
1741
2132
  onpointerleave={(event) => {
1742
- event.stopPropagation()
1743
- hideTooltip()
2133
+ event.stopPropagation();
2134
+ hideTooltip();
1744
2135
  }}
1745
2136
  ></span>
1746
2137
  {/if}
@@ -1760,7 +2151,10 @@
1760
2151
  </tbody>
1761
2152
  {#if props.pinnedBottomRows && props.pinnedBottomRows.length > 0}
1762
2153
  <!-- svelte-ignore a11y_no_redundant_roles -->
1763
- <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
+ >
1764
2158
  {#each props.pinnedBottomRows as r, i (i)}
1765
2159
  {@render pinnedRowBody(r, "bottom", i)}
1766
2160
  {/each}
@@ -1862,7 +2256,7 @@
1862
2256
  content-size={scrollMetrics.scrollHeight ||
1863
2257
  rowDomTotalSize + headerHeight}
1864
2258
  value={scrollMetrics.scrollTop}
1865
- step={typeof props.rowHeight === 'number' ? props.rowHeight : 30}
2259
+ step={typeof props.rowHeight === "number" ? props.rowHeight : 30}
1866
2260
  style={`top: ${headerHeight}px; height: calc(100% - ${headerHeight + (hasHorizontalOverflow ? 16 : 0)}px);`}
1867
2261
  ></sv-grid-scrollbar>
1868
2262
  {/if}
@@ -1895,8 +2289,23 @@
1895
2289
  Enter cycles to the next hit; Esc closes. -->
1896
2290
  <div class="sv-grid-find" role="search" aria-label="Find in grid">
1897
2291
  <svg class="sv-grid-find-icon" viewBox="0 0 16 16" aria-hidden="true">
1898
- <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/>
1899
- <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
+ />
1900
2309
  </svg>
1901
2310
  <input
1902
2311
  class="sv-grid-find-input"
@@ -1906,40 +2315,73 @@
1906
2315
  bind:value={ctrl.findQuery}
1907
2316
  oninput={() => (ctrl.findHitIndex = 0)}
1908
2317
  onkeydown={(event) => {
1909
- event.stopPropagation()
1910
- if (event.key === 'Enter') {
1911
- event.preventDefault()
1912
- if (findHits.length === 0) return
1913
- ctrl.findHitIndex = (ctrl.findHitIndex + (event.shiftKey ? -1 : 1) + findHits.length) % findHits.length
1914
- const hit = findHits[ctrl.findHitIndex]
1915
- 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 = "";
1916
2337
  }
1917
- if (event.key === 'Escape') { event.preventDefault(); ctrl.findOpen = false; ctrl.findQuery = '' }
1918
2338
  }}
1919
2339
  />
1920
2340
  <span class="sv-grid-find-count">
1921
2341
  {findHits.length === 0 && ctrl.findQuery.trim()
1922
- ? 'No matches'
2342
+ ? "No matches"
1923
2343
  : findHits.length === 0
1924
- ? ''
2344
+ ? ""
1925
2345
  : `${ctrl.findHitIndex + 1} of ${findHits.length}`}
1926
2346
  </span>
1927
- <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"
1928
2351
  disabled={findHits.length === 0}
1929
2352
  onclick={() => {
1930
- ctrl.findHitIndex = (ctrl.findHitIndex - 1 + findHits.length) % findHits.length
1931
- const hit = findHits[ctrl.findHitIndex]
1932
- if (hit) { setActiveCell(hit.rowIndex, hit.colIndex); scrollActiveCellIntoView(hit.rowIndex, hit.colIndex) }
1933
- }}>↑</button>
1934
- <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"
1935
2366
  disabled={findHits.length === 0}
1936
2367
  onclick={() => {
1937
- ctrl.findHitIndex = (ctrl.findHitIndex + 1) % findHits.length
1938
- const hit = findHits[ctrl.findHitIndex]
1939
- if (hit) { setActiveCell(hit.rowIndex, hit.colIndex); scrollActiveCellIntoView(hit.rowIndex, hit.colIndex) }
1940
- }}>↓</button>
1941
- <button type="button" class="sv-grid-find-close" aria-label="Close find"
1942
- 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
+ >
1943
2385
  </div>
1944
2386
  {/if}
1945
2387
 
@@ -1967,25 +2409,11 @@
1967
2409
  {/if}
1968
2410
 
1969
2411
  {#if toolPanelEnabled}
1970
- <button
1971
- type="button"
1972
- class="sv-grid-tool-panel-toggle"
1973
- class:is-open={ctrl.toolPanelOpen}
1974
- aria-label={ctrl.toolPanelOpen ? "Close columns panel" : "Open columns panel"}
1975
- aria-expanded={ctrl.toolPanelOpen}
1976
- onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1977
- title="Columns"
1978
- >
1979
- <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">
1980
- <rect x="3" y="4" width="6" height="16" rx="1" />
1981
- <rect x="11" y="4" width="4" height="16" rx="1" />
1982
- <rect x="17" y="4" width="4" height="16" rx="1" />
1983
- </svg>
1984
- </button>
1985
2412
  {#if ctrl.toolPanelOpen}
1986
- <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">
1987
2415
  <div class="sv-grid-tool-panel-head">
1988
- <span>Columns</span>
2416
+ <span>{panelTab === "filters" ? "Filters" : "Columns"}</span>
1989
2417
  <button
1990
2418
  type="button"
1991
2419
  class="sv-grid-tool-panel-close"
@@ -1993,46 +2421,144 @@
1993
2421
  onclick={() => (ctrl.toolPanelOpen = false)}>✕</button
1994
2422
  >
1995
2423
  </div>
1996
- <ul class="sv-grid-tool-panel-list">
1997
- {#each toolPanelColumns as column, i (column.id)}
1998
- {@const visible = !ctrl.hiddenColumns[column.id]}
1999
- {@const grouped = groupingColumns.includes(column.id)}
2000
- <li class="sv-grid-tool-panel-item">
2001
- <label class="sv-grid-tool-panel-vis">
2002
- <input
2003
- type="checkbox"
2004
- checked={visible}
2005
- onchange={() => toggleColumnVisibleInPanel(column.id)}
2006
- />
2007
- <span class="sv-grid-tool-panel-name">{toolPanelHeaderLabel(column)}</span>
2008
- </label>
2009
- <span class="sv-grid-tool-panel-actions">
2010
- <button
2011
- type="button"
2012
- class="sv-grid-tool-panel-btn"
2013
- class:is-active={grouped}
2014
- aria-label={grouped ? "Ungroup" : "Group by"}
2015
- title={grouped ? "Ungroup" : "Group by this column"}
2016
- onclick={() => toggleGroupInPanel(column.id)}>⊞</button
2017
- >
2018
- <button
2019
- type="button"
2020
- class="sv-grid-tool-panel-btn"
2021
- aria-label="Move up"
2022
- disabled={i === 0}
2023
- onclick={() => moveColumnInPanel(column.id, -1)}>↑</button
2024
- >
2025
- <button
2026
- type="button"
2027
- class="sv-grid-tool-panel-btn"
2028
- aria-label="Move down"
2029
- disabled={i === toolPanelColumns.length - 1}
2030
- 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)}
2031
2500
  >
2032
- </span>
2033
- </li>
2034
- {/each}
2035
- </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}
2036
2562
  </aside>
2037
2563
  {/if}
2038
2564
  {/if}