@svgrid/grid 2.2.28 → 2.2.29

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 (60) hide show
  1. package/dist/GridMenus.svelte +148 -100
  2. package/dist/SvGrid.controller.svelte.d.ts +14 -1
  3. package/dist/SvGrid.controller.svelte.js +112 -28
  4. package/dist/SvGrid.css +15 -34
  5. package/dist/SvGrid.svelte +49 -21
  6. package/dist/SvGrid.types.d.ts +20 -5
  7. package/dist/SvListBox.svelte +42 -3
  8. package/dist/SvListBox.svelte.d.ts +10 -1
  9. package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
  10. package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
  11. package/dist/cdn/{src-BxNEslEo.js → src-BLJfdyL0.js} +4014 -3869
  12. package/dist/cdn/{src-C4yKQZ5t.js → src-DYDCiC0D.js} +7036 -6891
  13. package/dist/cdn/svgrid.js +8 -8
  14. package/dist/cdn/svgrid.svelte-external.js +8 -8
  15. package/dist/column-groups.js +8 -10
  16. package/dist/column-id.d.ts +9 -0
  17. package/dist/column-id.js +25 -0
  18. package/dist/core.js +2 -1
  19. package/dist/createListbox.svelte.d.ts +15 -2
  20. package/dist/createListbox.svelte.js +27 -6
  21. package/dist/editing.d.ts +1 -0
  22. package/dist/editing.js +45 -10
  23. package/dist/filtering/excel-filters.d.ts +30 -10
  24. package/dist/filtering/excel-filters.js +96 -15
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/menus.d.ts +2 -0
  28. package/dist/menus.js +25 -0
  29. package/dist/row-resize.js +40 -0
  30. package/dist/selection.js +1 -1
  31. package/package.json +1 -1
  32. package/src/GridMenus.svelte +148 -100
  33. package/src/SvGrid.controller.svelte.ts +123 -27
  34. package/src/SvGrid.css +15 -34
  35. package/src/SvGrid.svelte +49 -21
  36. package/src/SvGrid.types.ts +20 -5
  37. package/src/SvListBox.svelte +42 -3
  38. package/src/column-groups.test.ts +48 -0
  39. package/src/column-groups.ts +8 -10
  40. package/src/column-id.ts +30 -0
  41. package/src/core.ts +2 -1
  42. package/src/createListbox.svelte.ts +39 -7
  43. package/src/editing.test.ts +90 -0
  44. package/src/editing.ts +46 -12
  45. package/src/filtering/excel-filters.test.ts +44 -1
  46. package/src/filtering/excel-filters.ts +91 -14
  47. package/src/index.ts +2 -0
  48. package/src/menus.test.ts +40 -4
  49. package/src/menus.ts +27 -0
  50. package/src/row-resize.test.ts +100 -0
  51. package/src/row-resize.ts +36 -0
  52. package/src/selection.ts +1 -1
  53. package/src/svgrid.api-extensions.test.ts +8 -3
  54. package/src/svgrid.cell-dom-ids.test.ts +96 -0
  55. package/src/svgrid.conditional-stat-scope.test.ts +111 -0
  56. package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
  57. package/src/svgrid.filter-menu-scroll.test.ts +3 -1
  58. package/src/svgrid.group-header-ids.test.ts +121 -0
  59. package/dist/cdn/GridMenus-BbzEvTYO.js +0 -421
  60. package/dist/cdn/GridMenus-E220X2kM.js +0 -417
@@ -3,6 +3,7 @@
3
3
  applyExcelFilter,
4
4
  normalizeForFilter,
5
5
  splitInTokens,
6
+ trailingInToken,
6
7
  createColumnVirtualizer,
7
8
  createCoreRowModel,
8
9
  createExpandedRowModel,
@@ -61,6 +62,7 @@
61
62
  type ResolvedCellFormat,
62
63
  } from "./conditional-formatting";
63
64
  import SvGridDropdown from "./SvGridDropdown.svelte";
65
+ import SvListBox from "./SvListBox.svelte";
64
66
  import type {
65
67
  Props,
66
68
  SelectionPoint,
@@ -129,9 +131,6 @@
129
131
  const filterRowValues = $derived(ctrl.filterRowValues);
130
132
  const inSuggestFor = $derived(ctrl.inSuggestFor);
131
133
  const inSuggestPos = $derived(ctrl.inSuggestPos);
132
- const inSuggestValues = $derived(ctrl.inSuggestValues);
133
- const toggleFilterToken = $derived(ctrl.toggleFilterToken);
134
- const facetValuesForColumn = $derived(ctrl.facetValuesForColumn);
135
134
 
136
135
  // Patch a column's menu filter (used for the optional 2nd condition + join
137
136
  // of multi-condition filtering). Merges into the existing entry, creating a
@@ -174,10 +173,60 @@
174
173
  const clearGroupingFromMenu = $derived(ctrl.clearGroupingFromMenu);
175
174
  const columnMenuFacetValues = $derived(ctrl.columnMenuFacetValues);
176
175
  const columnMenuVisibleFacets = $derived(ctrl.columnMenuVisibleFacets);
177
- const isFacetChecked = $derived(ctrl.isFacetChecked);
178
- const toggleFacetValue = $derived(ctrl.toggleFacetValue);
176
+ const columnMenuFacetOptions = $derived(ctrl.columnMenuFacetOptions);
177
+ const columnMenuSelectedFacets = $derived(ctrl.columnMenuSelectedFacets);
178
+ const setFacetSelection = $derived(ctrl.setFacetSelection);
179
+ const setFilterTokens = $derived(ctrl.setFilterTokens);
180
+ const inSuggestOptions = $derived(ctrl.inSuggestOptions);
181
+ const openInSuggest = $derived(ctrl.openInSuggest);
182
+ const closeInSuggest = $derived(ctrl.closeInSuggest);
179
183
  const isAllFacetsChecked = $derived(ctrl.isAllFacetsChecked);
180
184
  const toggleAllFacets = $derived(ctrl.toggleAllFacets);
185
+
186
+ /**
187
+ * Above this many rows the checklists switch to the windowed listbox. Short
188
+ * lists stay fully rendered so the popover hugs its content instead of
189
+ * reserving a fixed `rows`-tall box.
190
+ */
191
+ const FACET_VIRTUAL_THRESHOLD = 40;
192
+
193
+ // The filter panel's `in` / `notIn` box holds the WHOLE token list
194
+ // ("AAPL, MSFT"), so the suggestion query is the trailing fragment being
195
+ // typed - not the full value, which would match nothing.
196
+ function onSetOpFilterFocus(input: HTMLInputElement, columnId: string) {
197
+ openInSuggest(input, columnId);
198
+ ctrl.inSuggestQuery = trailingInToken(input.value);
199
+ }
200
+
201
+ function onSetOpFilterInput(input: HTMLInputElement, columnId: string) {
202
+ ctrl.inSuggestQuery = trailingInToken(input.value);
203
+ if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
204
+ }
205
+
206
+ /** Columns as checklist options, labelled by their header when it's a string. */
207
+ function columnVisibilityOptions(columns: Array<Column<TData>>) {
208
+ return columns.map((column) => ({
209
+ value: column.id,
210
+ label: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
211
+ }));
212
+ }
213
+
214
+ /** Currently visible column ids - the checklist's selection set. */
215
+ function visibleColumnIds(columns: Array<Column<TData>>) {
216
+ const set = new Set<string | number>();
217
+ for (const column of columns) if (!ctrl.hiddenColumns[column.id]) set.add(column.id);
218
+ return set;
219
+ }
220
+
221
+ /** Write a checklist selection back as the complementary `hiddenColumns` map. */
222
+ function applyColumnVisibility(columns: Array<Column<TData>>, visible: Set<string>) {
223
+ const next: Record<string, boolean> = { ...ctrl.hiddenColumns };
224
+ for (const column of columns) {
225
+ if (visible.has(column.id)) delete next[column.id];
226
+ else next[column.id] = true;
227
+ }
228
+ ctrl.hiddenColumns = next;
229
+ }
181
230
  const clearColumnFilter = $derived(ctrl.clearColumnFilter);
182
231
  const contextMenuFor = $derived(ctrl.contextMenuFor);
183
232
  const contextMenuPos = $derived(ctrl.contextMenuPos);
@@ -234,10 +283,12 @@
234
283
  {#if menuActiveOperator !== "isBlank" && menuActiveOperator !== "isNotBlank"}
235
284
  {@const isSetOp =
236
285
  menuActiveOperator === "in" || menuActiveOperator === "notIn"}
286
+ <!-- in/notIn drives the shared value-suggestions dropdown (a windowed
287
+ checklist) rather than a native datalist, which capped at 200
288
+ values and couldn't show which are already selected. -->
237
289
  <input
238
290
  class="sv-grid-menu-condition-value"
239
291
  type={menuActiveOperator === "regex" ? "text" : menuInputType}
240
- list={isSetOp ? `sv-in-list-${colId}` : undefined}
241
292
  value={filterMenuValues[colId]?.value ?? ""}
242
293
  placeholder={menuActiveOperator === "between"
243
294
  ? "From"
@@ -246,15 +297,16 @@
246
297
  : menuActiveOperator === "regex"
247
298
  ? "Pattern..."
248
299
  : "Filter value..."}
249
- oninput={(event) => updateFilterMenuValue(colId, (event.currentTarget as HTMLInputElement).value)}
300
+ onfocus={isSetOp
301
+ ? (event) => onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId)
302
+ : undefined}
303
+ onblur={isSetOp ? () => closeInSuggest() : undefined}
304
+ oninput={(event) => {
305
+ const input = event.currentTarget as HTMLInputElement;
306
+ updateFilterMenuValue(colId, input.value);
307
+ if (isSetOp) onSetOpFilterInput(input, colId);
308
+ }}
250
309
  />
251
- {#if isSetOp}
252
- <datalist id={`sv-in-list-${colId}`}>
253
- {#each facetValuesForColumn(colId).slice(0, 200) as v (v)}
254
- <option value={v}></option>
255
- {/each}
256
- </datalist>
257
- {/if}
258
310
  {#if menuActiveOperator === "between"}
259
311
  <input
260
312
  class="sv-grid-menu-condition-value"
@@ -323,16 +375,27 @@
323
375
  <input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
324
376
  <span class="sv-grid-facet-label">(Select all)</span>
325
377
  </label>
326
- <div class="sv-grid-facet-list">
327
- {#each columnMenuVisibleFacets as value (value)}
328
- <label class="sv-grid-facet">
329
- <input type="checkbox" checked={isFacetChecked(colId, value)} onchange={() => toggleFacetValue(colId, value)} />
330
- <span class="sv-grid-facet-label">{value === "" ? "(Blanks)" : value}</span>
331
- </label>
332
- {:else}
333
- <div class="sv-grid-facet-empty">No values</div>
334
- {/each}
335
- </div>
378
+ <!-- Windowed checklist. A high-cardinality column (10k distinct symbols
379
+ on a live feed) would otherwise mount a label + checkbox per value -
380
+ and 10k focusable checkboxes are 10k tab stops. SvListBox renders
381
+ only the visible rows and is one roving-tabindex stop. -->
382
+ {#if columnMenuFacetOptions.length}
383
+ <SvListBox
384
+ block
385
+ checkbox
386
+ multiple
387
+ virtual={columnMenuFacetOptions.length > FACET_VIRTUAL_THRESHOLD}
388
+ rows={7}
389
+ rowHeight={24}
390
+ size="sm"
391
+ ariaLabel="Filter values"
392
+ options={columnMenuFacetOptions}
393
+ value={columnMenuSelectedFacets}
394
+ onChange={(next) => setFacetSelection(colId, next as Set<string>)}
395
+ />
396
+ {:else}
397
+ <div class="sv-grid-facet-empty">No values</div>
398
+ {/if}
336
399
  {#if columnMenuFacetValues.length > columnMenuVisibleFacets.length}
337
400
  <div class="sv-grid-facet-note">
338
401
  Showing {columnMenuVisibleFacets.length} of {columnMenuFacetValues.length}
@@ -383,30 +446,21 @@
383
446
  {#if columnMenuTabsEnabled && menuTab === "filter" && menuCanFilter && showColumnFiltersEffective}
384
447
  {@render filterPanelBody(menuColumnId)}
385
448
  {:else if columnMenuTabsEnabled && menuTab === "columns"}
449
+ {@const tabColumns = grid.getAllColumns()}
386
450
  <div class="sv-grid-menu-filter-head">Visible columns</div>
387
- <div class="sv-grid-facet-list">
388
- {#each grid.getAllColumns() as column (column.id)}
389
- <label class="sv-grid-facet">
390
- <input
391
- type="checkbox"
392
- checked={!ctrl.hiddenColumns[column.id]}
393
- onchange={(event) => {
394
- const visible = (event.currentTarget as HTMLInputElement).checked;
395
- if (visible) {
396
- const next = { ...ctrl.hiddenColumns };
397
- delete next[column.id];
398
- ctrl.hiddenColumns = next;
399
- } else {
400
- ctrl.hiddenColumns = { ...ctrl.hiddenColumns, [column.id]: true };
401
- }
402
- }}
403
- />
404
- <span class="sv-grid-facet-label"
405
- >{typeof column.columnDef.header === "string" ? column.columnDef.header : column.id}</span
406
- >
407
- </label>
408
- {/each}
409
- </div>
451
+ <SvListBox
452
+ block
453
+ checkbox
454
+ multiple
455
+ virtual={tabColumns.length > FACET_VIRTUAL_THRESHOLD}
456
+ rows={9}
457
+ rowHeight={24}
458
+ size="sm"
459
+ ariaLabel="Visible columns"
460
+ options={columnVisibilityOptions(tabColumns)}
461
+ value={visibleColumnIds(tabColumns)}
462
+ onChange={(next) => applyColumnVisibility(tabColumns, next as Set<string>)}
463
+ />
410
464
  {:else}
411
465
  {#if menuCanSort}
412
466
  <button
@@ -570,32 +624,19 @@
570
624
  style={`left: ${chooseColumnsPos.x}px; top: ${chooseColumnsPos.y}px;`}
571
625
  >
572
626
  <div class="sv-grid-menu-filter-head">Visible columns</div>
573
- <div class="sv-grid-facet-list">
574
- {#each everyColumn as column (column.id)}
575
- <label class="sv-grid-facet">
576
- <input
577
- type="checkbox"
578
- checked={!ctrl.hiddenColumns[column.id]}
579
- onchange={(event) => {
580
- const visible = (event.currentTarget as HTMLInputElement)
581
- .checked;
582
- if (visible) {
583
- const next = { ...ctrl.hiddenColumns };
584
- delete next[column.id];
585
- ctrl.hiddenColumns = next;
586
- } else {
587
- ctrl.hiddenColumns = { ...ctrl.hiddenColumns, [column.id]: true };
588
- }
589
- }}
590
- />
591
- <span class="sv-grid-facet-label"
592
- >{typeof column.columnDef.header === "string"
593
- ? column.columnDef.header
594
- : column.id}</span
595
- >
596
- </label>
597
- {/each}
598
- </div>
627
+ <SvListBox
628
+ block
629
+ checkbox
630
+ multiple
631
+ virtual={everyColumn.length > FACET_VIRTUAL_THRESHOLD}
632
+ rows={11}
633
+ rowHeight={24}
634
+ size="sm"
635
+ ariaLabel="Visible columns"
636
+ options={columnVisibilityOptions(everyColumn)}
637
+ value={visibleColumnIds(everyColumn)}
638
+ onChange={(next) => applyColumnVisibility(everyColumn, next as Set<string>)}
639
+ />
599
640
  </div>
600
641
  {/if}
601
642
 
@@ -632,42 +673,49 @@
632
673
 
633
674
  {#if inSuggestFor}
634
675
  {@const suggestColId = inSuggestFor}
635
- {@const selectedTokens = new Set(
676
+ {@const selectedTokens = new Set<string | number>(
636
677
  splitInTokens(filterRowValues[suggestColId] ?? ""),
637
678
  )}
638
- <!-- Value suggestions for an `in` / `notIn` chip input. Items use
639
- `onmousedown` (preventDefault) so clicking one keeps focus in the
640
- chip input and the dropdown stays open for multi-select; the input's
641
- own blur handler closes it when focus truly leaves. -->
679
+ <!-- Value suggestions for an `in` / `notIn` input. The wrapper swallows
680
+ `mousedown` so clicking a row keeps focus in the input and the
681
+ dropdown stays open for multi-select; the input's own blur handler
682
+ closes it when focus truly leaves. The list still picks on `click`.
683
+ Tokens the query is currently hiding ride along in the set, so a
684
+ click never silently drops them. -->
685
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
642
686
  <div
643
687
  class="sv-grid-menu sv-grid-in-suggest"
644
- role="listbox"
645
- aria-multiselectable="true"
646
- aria-label="Filter values"
647
688
  style={`left: ${inSuggestPos.x}px; top: ${inSuggestPos.y}px;`}
689
+ onmousedown={(event) => event.preventDefault()}
648
690
  >
649
- {#each inSuggestValues as value (value)}
650
- {@const checked = selectedTokens.has(value)}
651
- <button
652
- type="button"
653
- class="sv-grid-menu-item sv-grid-in-suggest-item"
654
- role="option"
655
- aria-selected={checked}
656
- onmousedown={(event) => {
657
- event.preventDefault();
658
- toggleFilterToken(suggestColId, value);
691
+ {#if inSuggestOptions.length}
692
+ <SvListBox
693
+ block
694
+ checkbox
695
+ multiple
696
+ virtual={inSuggestOptions.length > FACET_VIRTUAL_THRESHOLD}
697
+ rows={8}
698
+ rowHeight={26}
699
+ size="sm"
700
+ ariaLabel="Filter values"
701
+ options={inSuggestOptions}
702
+ value={selectedTokens}
703
+ onChange={(next) => {
704
+ const picked = new Set(next as Set<string>);
705
+ // Boxes that hold the whole list ("AAPL, MS") leave a half-typed
706
+ // fragment in the token set. The pick REPLACES that fragment
707
+ // rather than landing beside it as a stray filter value. The chip
708
+ // input keeps its fragment out of `filterRowValues`, so this is a
709
+ // no-op there.
710
+ const fragment = trailingInToken(filterRowValues[suggestColId] ?? "");
711
+ if (fragment) picked.delete(fragment);
712
+ setFilterTokens(suggestColId, [...picked]);
713
+ ctrl.inSuggestQuery = "";
659
714
  }}
660
- >
661
- <span class="sv-grid-in-suggest-check" aria-hidden="true"
662
- >{checked ? "✓" : ""}</span
663
- >
664
- <span class="sv-grid-in-suggest-label"
665
- >{value === "" ? "(Blanks)" : value}</span
666
- >
667
- </button>
715
+ />
668
716
  {:else}
669
717
  <div class="sv-grid-facet-empty">No values</div>
670
- {/each}
718
+ {/if}
671
719
  </div>
672
720
  {/if}
673
721
 
@@ -12,7 +12,7 @@ import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNume
12
12
  * the component lifecycle) and consumed through the returned getters.
13
13
  */
14
14
  export type SvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = ReturnType<typeof createSvGridController<TFeatures, TData>>;
15
- export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(rawProps: Props<TFeatures, TData>): {
15
+ export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(rawProps: Props<TFeatures, TData>, domIdBase?: string): {
16
16
  readonly props: Props<TFeatures, TData>;
17
17
  optionOverrides: Record<string, unknown>;
18
18
  readonly editingEnabled: boolean;
@@ -126,6 +126,10 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
126
126
  inSuggestPos: MenuPosition;
127
127
  inSuggestQuery: string;
128
128
  readonly inSuggestValues: string[];
129
+ readonly inSuggestOptions: {
130
+ value: string;
131
+ label: string;
132
+ }[];
129
133
  readonly facetValuesForColumn: (columnId: string) => Array<string>;
130
134
  chooseColumnsPos: MenuPosition | null;
131
135
  contextMenuFor: ContextMenuTarget<TData> | null;
@@ -446,6 +450,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
446
450
  readonly columnWindowRightSpacer: number;
447
451
  readonly activeCell: any;
448
452
  readonly activeDescendantId: string | null;
453
+ readonly gridDomId: string;
449
454
  readonly formatSummaryNumeric: typeof formatSummaryNumeric;
450
455
  readonly computeSummaries: (rows: readonly Row<TData>[], columns: readonly Column<TData>[]) => Record<string, string>;
451
456
  readonly SUMMARY_DEFER_CELL_LIMIT: number;
@@ -549,6 +554,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
549
554
  }, direction: "undo" | "redo") => void;
550
555
  readonly updateEditingCellValue: (value: unknown) => void;
551
556
  readonly onEditorKeyDown: (event: KeyboardEvent) => void;
557
+ readonly commitAndMoveByTab: (shiftKey: boolean) => void;
552
558
  readonly focusOnMount: (node: HTMLInputElement | HTMLTextAreaElement) => void;
553
559
  readonly onHeaderSortClick: (event: MouseEvent, columnId: string) => void;
554
560
  readonly onGridKeyDown: (event: KeyboardEvent) => void;
@@ -594,6 +600,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
594
600
  readonly addFilterToken: (columnId: string, token: string) => void;
595
601
  readonly removeFilterToken: (columnId: string, token: string) => void;
596
602
  readonly toggleFilterToken: (columnId: string, token: string) => void;
603
+ readonly setFilterTokens: (columnId: string, tokens: Iterable<string>) => void;
597
604
  readonly sortColumnFromMenu: (columnId: string, desc: boolean) => void;
598
605
  readonly clearColumnSort: (columnId: string) => void;
599
606
  readonly groupByColumnFromMenu: (columnId: string) => void;
@@ -605,8 +612,14 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
605
612
  readonly serverFacetLoading: string | null;
606
613
  readonly columnMenuFacetValues: string[];
607
614
  readonly columnMenuVisibleFacets: string[];
615
+ readonly columnMenuFacetOptions: {
616
+ value: string;
617
+ label: string;
618
+ }[];
619
+ readonly columnMenuSelectedFacets: ReadonlySet<string>;
608
620
  readonly isFacetChecked: (columnId: string, value: string) => any;
609
621
  readonly toggleFacetValue: (columnId: string, value: string) => void;
622
+ readonly setFacetSelection: (columnId: string, next: ReadonlySet<string>) => void;
610
623
  readonly isAllFacetsChecked: (columnId: string) => boolean;
611
624
  readonly toggleAllFacets: (columnId: string) => void;
612
625
  readonly clearColumnFilter: (columnId: string) => void;