@svgrid/grid 2.1.21 → 2.1.23

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 (219) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. package/src/virtual.test.ts +6 -4
@@ -52,19 +52,26 @@
52
52
  colorfulChipStyle,
53
53
  getEditorClass,
54
54
  } from "./SvGrid.helpers";
55
+ import {
56
+ splitInTokens,
57
+ joinInTokens,
58
+ } from "./filtering/excel-filters";
55
59
  import { createSvGridController } from "./SvGrid.controller.svelte";
56
60
  import GridMenus from "./GridMenus.svelte";
57
61
  import SvGridChartPanel from "./SvGridChartPanel.svelte";
58
62
  import GridFooter from "./GridFooter.svelte";
59
63
  import SvGridBoard from "./SvGridBoard.svelte";
64
+ import { getSchedulerView } from "./scheduler-view.svelte";
60
65
  let props: Props<TFeatures, TData> = $props();
61
66
  const ctrl = createSvGridController(props);
62
67
  // Kanban board mode: when `board` is set the grid renders lanes of cards
63
68
  // instead of the table (see SvGridBoard). Narrowed derived so the template
64
69
  // branch can pass it non-null.
65
70
  const boardConfig = $derived(props.board);
66
- // The board renders the grid's FILTERED + SORTED rows (not raw data), so the
67
- // search box / column filters / sort all flow through to the lanes.
71
+ // Scheduler / calendar mode: same "view of the grid" seam as the board.
72
+ const schedulerConfig = $derived(props.scheduler);
73
+ // The board / scheduler render the grid's FILTERED + SORTED rows (not raw
74
+ // data), so the search box / column filters / sort all flow through to them.
68
75
  const boardData = $derived(
69
76
  ctrl.allRowsBeforePagination
70
77
  .filter((r) => !ctrl.isGroupRow(r))
@@ -101,7 +108,7 @@
101
108
  const grid = $derived(ctrl.grid);
102
109
  const allColumns = $derived(ctrl.allColumns);
103
110
  const headerGroups = $derived(ctrl.headerGroups);
104
- const groupHeaderRows = $derived(ctrl.groupHeaderRows);
111
+ const groupHeaderRows = $derived(ctrl.groupHeaderRowsWindowed);
105
112
  const cellPinStyle = $derived(ctrl.cellPinStyle);
106
113
  const isColumnPinned = $derived(ctrl.isColumnPinned);
107
114
  const colDragId = $derived(ctrl.colDragId);
@@ -172,6 +179,13 @@
172
179
  const toggleGroupInPanel = $derived(ctrl.toggleGroupInPanel);
173
180
  const virtualizer = $derived(ctrl.virtualizer);
174
181
  const rowVirtualizationEnabled = $derived(ctrl.rowVirtualizationEnabled);
182
+ // The fixed row height, matching what the virtualized path takes from the virtualizer.
183
+ // The non-virtualized (`virtualization={false}`) body must apply this too, else its rows
184
+ // fall back to content height and look shorter than a virtualized grid's.
185
+ const rowSizePx = (i: number): number => {
186
+ const rh = props.rowHeight;
187
+ return typeof rh === "function" ? rh(i) : (rh ?? 30);
188
+ };
175
189
  const columnVirtualizationEnabled = $derived(
176
190
  ctrl.columnVirtualizationEnabled,
177
191
  );
@@ -367,7 +381,149 @@
367
381
  const openColumnMenu = $derived(ctrl.openColumnMenu);
368
382
  const openFilterMenu = $derived(ctrl.openFilterMenu);
369
383
  const openOperatorMenu = $derived(ctrl.openOperatorMenu);
384
+ const openInSuggest = $derived(ctrl.openInSuggest);
385
+ const closeInSuggest = $derived(ctrl.closeInSuggest);
386
+ const addFilterToken = $derived(ctrl.addFilterToken);
387
+ const removeFilterToken = $derived(ctrl.removeFilterToken);
388
+ const facetValuesForColumn = $derived(ctrl.facetValuesForColumn);
370
389
  const onWindowKeydown = $derived(ctrl.onWindowKeydown);
390
+
391
+ // True when a `regex` filter's pattern won't compile - used to flag the
392
+ // input so the user sees a half-typed pattern isn't being applied.
393
+ function isInvalidRegex(pattern: string): boolean {
394
+ if (!pattern) return false;
395
+ try {
396
+ new RegExp(pattern);
397
+ return false;
398
+ } catch {
399
+ return true;
400
+ }
401
+ }
402
+
403
+ // Commit the token currently typed in an `in` / `notIn` chip input,
404
+ // appending it to the column's token list and clearing the box.
405
+ function commitFilterChip(columnId: string, input: HTMLInputElement): void {
406
+ const raw = input.value.trim();
407
+ if (!raw) return;
408
+ addFilterToken(columnId, raw);
409
+ input.value = "";
410
+ ctrl.inSuggestQuery = "";
411
+ }
412
+
413
+ function removeFilterChip(columnId: string, token: string): void {
414
+ removeFilterToken(columnId, token);
415
+ }
416
+
417
+ // Focus opens the value-suggestions dropdown under this input; typing
418
+ // narrows it via `inSuggestQuery`.
419
+ function onFilterChipFocus(
420
+ event: FocusEvent,
421
+ columnId: string,
422
+ ): void {
423
+ openInSuggest(event.currentTarget as HTMLInputElement, columnId);
424
+ }
425
+
426
+ function onFilterChipInput(
427
+ event: Event,
428
+ columnId: string,
429
+ ): void {
430
+ const input = event.currentTarget as HTMLInputElement;
431
+ ctrl.inSuggestQuery = input.value;
432
+ // Reopen the dropdown if a prior selection/blur had closed it.
433
+ if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
434
+ }
435
+
436
+ // Overflow layout for the `in` / `notIn` chip row: render every chip, then
437
+ // measure how many fit on one line and collapse the rest into the "+N" pill.
438
+ // A guess-by-width heuristic can't work (labels vary), so we measure real
439
+ // element widths and re-run on column resize (ResizeObserver) and whenever
440
+ // the token set or width changes (action param -> update()).
441
+ function fitChips(node: HTMLElement, _param?: string) {
442
+ const INPUT_MIN = 14; // small sliver kept clickable to focus + type
443
+ const recompute = () => {
444
+ const chips = Array.from(
445
+ node.querySelectorAll<HTMLElement>("[data-chip]"),
446
+ );
447
+ const more = node.querySelector<HTMLElement>("[data-more]");
448
+ if (!more) return;
449
+ // Reset to the all-visible baseline before measuring.
450
+ for (const chip of chips) chip.style.display = "";
451
+ more.style.display = "none";
452
+ if (!chips.length || node.clientWidth === 0) return;
453
+
454
+ const style = getComputedStyle(node);
455
+ const padX =
456
+ parseFloat(style.paddingLeft || "0") +
457
+ parseFloat(style.paddingRight || "0");
458
+ const gap = parseFloat(style.columnGap || style.gap || "0") || 3;
459
+ const avail = node.clientWidth - padX - INPUT_MIN;
460
+
461
+ // Do all chips fit without needing a "+N" pill?
462
+ let used = 0;
463
+ let fit = 0;
464
+ for (const chip of chips) {
465
+ const w = chip.offsetWidth + gap;
466
+ if (used + w <= avail) {
467
+ used += w;
468
+ fit++;
469
+ } else break;
470
+ }
471
+ if (fit >= chips.length) return; // everything fits
472
+
473
+ // Reserve space for the "+N" pill, then re-fit.
474
+ more.style.display = "";
475
+ more.textContent = `+${chips.length}`; // widest label, for measuring
476
+ const budget = avail - (more.offsetWidth + gap);
477
+ used = 0;
478
+ fit = 0;
479
+ for (const chip of chips) {
480
+ const w = chip.offsetWidth + gap;
481
+ if (used + w <= budget) {
482
+ used += w;
483
+ fit++;
484
+ } else break;
485
+ }
486
+ if (fit < 1) fit = 1;
487
+ for (const chip of chips.slice(fit)) chip.style.display = "none";
488
+ const hidden = chips.length - fit;
489
+ more.textContent = `+${hidden}`;
490
+ more.title = chips
491
+ .slice(fit)
492
+ .map((c) => c.textContent?.replace(/×$/, "").trim() ?? "")
493
+ .join(", ");
494
+ };
495
+ // Measure after layout settles.
496
+ const schedule = () =>
497
+ typeof requestAnimationFrame === "function"
498
+ ? requestAnimationFrame(recompute)
499
+ : recompute();
500
+ schedule();
501
+ const ro =
502
+ typeof ResizeObserver !== "undefined"
503
+ ? new ResizeObserver(schedule)
504
+ : null;
505
+ ro?.observe(node);
506
+ return {
507
+ update: schedule,
508
+ destroy: () => ro?.disconnect(),
509
+ };
510
+ }
511
+
512
+ function onFilterChipKeydown(
513
+ event: KeyboardEvent,
514
+ columnId: string,
515
+ ): void {
516
+ const input = event.currentTarget as HTMLInputElement;
517
+ if (event.key === "Enter" || event.key === ",") {
518
+ event.preventDefault();
519
+ commitFilterChip(columnId, input);
520
+ } else if (event.key === "Escape") {
521
+ closeInSuggest();
522
+ } else if (event.key === "Backspace" && input.value === "") {
523
+ const tokens = splitInTokens(filterRowValues[columnId] ?? "");
524
+ if (tokens.length) removeFilterChip(columnId, tokens[tokens.length - 1]!);
525
+ }
526
+ }
371
527
  </script>
372
528
 
373
529
  <svelte:window
@@ -422,6 +578,53 @@
422
578
  />
423
579
  </div>
424
580
  </div>
581
+ {:else if schedulerConfig}
582
+ <div
583
+ class="sv-grid-root sv-grid-scheduler-root"
584
+ class:sv-grid-root-fill={props.containerHeight === "100%"}
585
+ style={`height: ${
586
+ typeof props.containerHeight === "string"
587
+ ? props.containerHeight
588
+ : `${props.containerHeight ?? 520}px`
589
+ }; display: flex; flex-direction: column;`}
590
+ >
591
+ {#if schedulerConfig.searchable !== false}
592
+ <label class="sv-grid-board-search">
593
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="14" height="14">
594
+ <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
595
+ <line x1="10.2" y1="10.2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
596
+ </svg>
597
+ <input
598
+ type="search"
599
+ placeholder={schedulerConfig.searchPlaceholder ?? "Search events..."}
600
+ bind:value={ctrl.globalFilter}
601
+ aria-label="Search events"
602
+ />
603
+ </label>
604
+ {/if}
605
+ <div style="flex: 1 1 auto; min-height: 0;">
606
+ {#if getSchedulerView()}
607
+ {@const SchedulerView = getSchedulerView()}
608
+ <SchedulerView
609
+ data={boardData}
610
+ columns={props.columns}
611
+ scheduler={schedulerConfig}
612
+ getRowId={props.getRowId}
613
+ />
614
+ {:else}
615
+ <!-- The scheduler renderer ships in @svgrid/enterprise. Call
616
+ `enableSchedulerView()` (or `installEnterprise(api)`) to register it. -->
617
+ <div class="sv-grid-scheduler-upsell" role="note">
618
+ <strong>Scheduler view</strong>
619
+ <p>
620
+ The calendar / scheduler view is an Enterprise feature. Install
621
+ <code>@svgrid/enterprise</code> and call <code>enableSchedulerView()</code>
622
+ to render it.
623
+ </p>
624
+ </div>
625
+ {/if}
626
+ </div>
627
+ </div>
425
628
  {:else}
426
629
  {#snippet icon(name: string)}
427
630
  <svg
@@ -473,6 +676,34 @@
473
676
  {:else if name === "op-isBlank"}
474
677
  <circle cx="12" cy="12" r="8" />
475
678
  <path d="M6.5 6.5l11 11" />
679
+ {:else if name === "op-isNotBlank"}
680
+ <circle cx="12" cy="12" r="8" />
681
+ <circle cx="12" cy="12" r="3.2" fill="currentColor" stroke="none" />
682
+ {:else if name === "op-notContains"}
683
+ <circle cx="11" cy="11" r="6" />
684
+ <path d="M20 20l-4.5-4.5" />
685
+ <path d="M7 11h8" />
686
+ {:else if name === "op-notEquals"}
687
+ <path d="M5 9.5h14" />
688
+ <path d="M5 14.5h14" />
689
+ <path d="M16 5l-8 14" />
690
+ {:else if name === "op-endsWith"}
691
+ <path d="M19 5v14" />
692
+ <path d="M15 9H5" />
693
+ <path d="M15 15H8" />
694
+ {:else if name === "op-regex"}
695
+ <path d="M12 5v9" />
696
+ <path d="M8.1 7.4l7.8 4.5" />
697
+ <path d="M15.9 7.4l-7.8 4.5" />
698
+ <circle cx="6" cy="18" r="1.4" fill="currentColor" stroke="none" />
699
+ {:else if name === "op-in"}
700
+ <path d="M14 5a7 7 0 1 0 0 14" />
701
+ <path d="M6 12h9" />
702
+ <path d="M12 9l3 3-3 3" />
703
+ {:else if name === "op-notIn"}
704
+ <path d="M14 5a7 7 0 1 0 0 14" />
705
+ <path d="M15 12H6" />
706
+ <path d="M9 9l-3 3 3 3" />
476
707
  {:else if name === "autosize"}
477
708
  <path d="M3 12h18" />
478
709
  <path d="M3 12l4-4" />
@@ -645,10 +876,18 @@
645
876
  {#if cf.dataBar}
646
877
  <div
647
878
  class="sv-grid-cf-bar"
648
- style={`width:${cf.dataBar.percent}%;background:${cf.dataBar.color}`}
879
+ style={`width:${cf.dataBar.percent}%;background:${
880
+ cf.dataBar.gradient
881
+ ? `linear-gradient(90deg, color-mix(in srgb, ${cf.dataBar.color} 35%, transparent), ${cf.dataBar.color})`
882
+ : cf.dataBar.color
883
+ }`}
649
884
  ></div>
650
885
  {/if}
651
- <span class="sv-grid-cf-content" style={cfTextStyle(cf)}>
886
+ <span
887
+ class="sv-grid-cf-content"
888
+ style={cfTextStyle(cf)}
889
+ title={cf.title ?? undefined}
890
+ >
652
891
  {#if cf.icon}<span class="sv-grid-cf-icon">{cf.icon}</span>{/if}
653
892
  {#if !cf.iconOnly}{@render cellBody(row, column, cellValue)}{/if}
654
893
  </span>
@@ -914,6 +1153,7 @@
914
1153
  formatString="yyyy-MM-dd"
915
1154
  dropDownDisplayMode="calendar"
916
1155
  autoOpen
1156
+ block
917
1157
  onChange={(d) => updateEditingCellValue(d)}
918
1158
  onCommit={() => saveEditingCell()}
919
1159
  onCancel={() => {
@@ -927,6 +1167,7 @@
927
1167
  formatString="yyyy-MM-dd HH:mm"
928
1168
  dropDownDisplayMode="both"
929
1169
  autoOpen
1170
+ block
930
1171
  onChange={(d) => updateEditingCellValue(d)}
931
1172
  onCommit={() => saveEditingCell()}
932
1173
  onCancel={() => {
@@ -942,6 +1183,7 @@
942
1183
  formatString="HH:mm"
943
1184
  dropDownDisplayMode="time"
944
1185
  autoOpen
1186
+ block
945
1187
  onChange={(d) => updateEditingCellValue(dateToTimeString(d))}
946
1188
  onCommit={() => saveEditingCell()}
947
1189
  onCancel={() => {
@@ -1368,6 +1610,7 @@
1368
1610
  <div
1369
1611
  class="sv-grid-container sv-grid-container-custom-scrollbars"
1370
1612
  class:sv-grid-has-vscroll={hasMeasured && hasVerticalOverflow}
1613
+ class:sv-grid-has-hscroll={hasMeasured && hasHorizontalOverflow}
1371
1614
  class:sv-grid-narrow={ctrl.isNarrowResponsive}
1372
1615
  bind:this={ctrl.scrollContainer}
1373
1616
  onscroll={onBodyScroll}
@@ -1415,6 +1658,13 @@
1415
1658
  aria-hidden="true"
1416
1659
  ></th>
1417
1660
  {/if}
1661
+ {#if columnVirtualizationEnabled && columnWindowStart > 0}
1662
+ <th
1663
+ class="sv-grid-column sv-grid-column-spacer"
1664
+ aria-hidden="true"
1665
+ style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
1666
+ ></th>
1667
+ {/if}
1418
1668
  {#each row.cells as cell (cell.key)}
1419
1669
  <th
1420
1670
  class="sv-grid-column sv-grid-group-header-cell"
@@ -1463,6 +1713,13 @@
1463
1713
  {/if}
1464
1714
  </th>
1465
1715
  {/each}
1716
+ {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
1717
+ <th
1718
+ class="sv-grid-column sv-grid-column-spacer"
1719
+ aria-hidden="true"
1720
+ style={`width: ${columnWindowRightSpacer}px; min-width: ${columnWindowRightSpacer}px; max-width: ${columnWindowRightSpacer}px;`}
1721
+ ></th>
1722
+ {/if}
1466
1723
  </tr>
1467
1724
  {/each}
1468
1725
  {#each headerGroups as headerGroup (headerGroup.id)}
@@ -1789,18 +2046,84 @@
1789
2046
  >{@render icon("chevron-down")}</span
1790
2047
  >
1791
2048
  </button>
1792
- {#if activeOperator !== "isBlank"}
2049
+ {#if activeOperator === "in" || activeOperator === "notIn"}
2050
+ {@const frTokens = splitInTokens(
2051
+ filterRowValues[rendered.column.id] ?? "",
2052
+ )}
2053
+ <div
2054
+ class="sv-grid-filter-chips"
2055
+ use:fitChips={`${frTokens.length}:${rendered.item.size}`}
2056
+ >
2057
+ {#each frTokens as token (token)}
2058
+ <span class="sv-grid-filter-chip" data-chip>
2059
+ <span class="sv-grid-filter-chip-label"
2060
+ >{token}</span
2061
+ >
2062
+ <button
2063
+ type="button"
2064
+ class="sv-grid-filter-chip-x"
2065
+ aria-label={`Remove ${token}`}
2066
+ onmousedown={(event) => {
2067
+ event.preventDefault();
2068
+ removeFilterChip(rendered.column.id, token);
2069
+ }}>×</button
2070
+ >
2071
+ </span>
2072
+ {/each}
2073
+ <!-- Overflow pill: the fitChips action shows/labels
2074
+ it when chips don't fit; click opens the list. -->
2075
+ <button
2076
+ type="button"
2077
+ class="sv-grid-filter-chip sv-grid-filter-chip-more"
2078
+ data-more
2079
+ style="display: none;"
2080
+ aria-label="More selected values. Open value list"
2081
+ onclick={(event) => {
2082
+ (
2083
+ event.currentTarget as HTMLElement
2084
+ ).parentElement
2085
+ ?.querySelector<HTMLInputElement>("input")
2086
+ ?.focus();
2087
+ }}
2088
+ ></button>
2089
+ <input
2090
+ class="sv-grid-filter-value sv-grid-filter-chip-input"
2091
+ type="text"
2092
+ placeholder={frTokens.length ? "" : "Add value…"}
2093
+ data-svgrid-filter-col={rendered.column.id}
2094
+ onfocus={(event) =>
2095
+ onFilterChipFocus(event, rendered.column.id)}
2096
+ oninput={(event) =>
2097
+ onFilterChipInput(event, rendered.column.id)}
2098
+ onkeydown={(event) =>
2099
+ onFilterChipKeydown(event, rendered.column.id)}
2100
+ onblur={(event) => {
2101
+ commitFilterChip(
2102
+ rendered.column.id,
2103
+ event.currentTarget as HTMLInputElement,
2104
+ );
2105
+ closeInSuggest();
2106
+ }}
2107
+ />
2108
+ </div>
2109
+ {:else if activeOperator !== "isBlank" && activeOperator !== "isNotBlank"}
1793
2110
  {@const frType = getEditorInputType(
1794
2111
  rendered.column.columnDef.editorType ?? "text",
1795
2112
  )}
2113
+ {@const frValue =
2114
+ filterRowValues[rendered.column.id] ?? ""}
1796
2115
  <input
1797
2116
  class="sv-grid-filter-value"
1798
- type={frType}
2117
+ class:sv-grid-filter-value-invalid={activeOperator ===
2118
+ "regex" && isInvalidRegex(frValue)}
2119
+ type={activeOperator === "regex" ? "text" : frType}
1799
2120
  placeholder={activeOperator === "between"
1800
2121
  ? "From"
1801
- : "Filter…"}
2122
+ : activeOperator === "regex"
2123
+ ? "Pattern…"
2124
+ : "Filter…"}
1802
2125
  data-svgrid-filter-col={rendered.column.id}
1803
- value={filterRowValues[rendered.column.id] ?? ""}
2126
+ value={frValue}
1804
2127
  oninput={(event) =>
1805
2128
  updateFilterRow(
1806
2129
  rendered.column.id,
@@ -2145,6 +2468,7 @@
2145
2468
  aria-level={row.depth + 1}
2146
2469
  aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
2147
2470
  {...getGridRowA11yProps(rowIndex + 1)}
2471
+ style={`height: ${rowSizePx(rowIndex)}px;`}
2148
2472
  >
2149
2473
  <td
2150
2474
  class="sv-grid-cell sv-grid-group-cell"
@@ -2170,6 +2494,7 @@
2170
2494
  aria-level={sgAriaLevel(row)}
2171
2495
  aria-expanded={sgAriaExpanded(row)}
2172
2496
  {...rowDragAttrs(rowIndex)}
2497
+ style={`height: ${rowSizePx(rowIndex)}px;`}
2173
2498
  >
2174
2499
  {#if showRowNumbersEffective}
2175
2500
  <td
@@ -2255,6 +2580,7 @@
2255
2580
  data-svgrid-row={rowIndex}
2256
2581
  data-svgrid-col={colIndex}
2257
2582
  data-col-id={rendered.column.id}
2583
+ data-align={getColumnAlign(rendered.column)}
2258
2584
  data-pinned={isColumnPinned(rendered.column.id) ??
2259
2585
  undefined}
2260
2586
  data-selected-range={rangeEdges ? "true" : undefined}
@@ -2547,6 +2873,8 @@
2547
2873
  event.preventDefault();
2548
2874
  ctrl.findOpen = false;
2549
2875
  ctrl.findQuery = "";
2876
+ // Return focus to the grid instead of letting it fall to <body> (#80).
2877
+ ctrl.gridRootEl?.focus({ preventScroll: true });
2550
2878
  }
2551
2879
  }}
2552
2880
  />
@@ -2593,6 +2921,8 @@
2593
2921
  onclick={() => {
2594
2922
  ctrl.findOpen = false;
2595
2923
  ctrl.findQuery = "";
2924
+ // Return focus to the grid instead of letting it fall to <body> (#80).
2925
+ ctrl.gridRootEl?.focus({ preventScroll: true });
2596
2926
  }}>✕</button
2597
2927
  >
2598
2928
  </div>
@@ -2741,11 +3071,19 @@
2741
3071
  >
2742
3072
  {/each}
2743
3073
  </select>
2744
- {#if active !== "isBlank"}
3074
+ {#if active !== "isBlank" && active !== "isNotBlank"}
3075
+ {@const tpIsSetOp = active === "in" || active === "notIn"}
2745
3076
  <input
2746
3077
  class="sv-grid-tp-filter-input"
2747
- type={fType}
2748
- placeholder={active === "between" ? "From" : "Filter…"}
3078
+ type={active === "regex" ? "text" : fType}
3079
+ list={tpIsSetOp ? `sv-tp-in-list-${column.id}` : undefined}
3080
+ placeholder={active === "between"
3081
+ ? "From"
3082
+ : tpIsSetOp
3083
+ ? "value, value…"
3084
+ : active === "regex"
3085
+ ? "Pattern…"
3086
+ : "Filter…"}
2749
3087
  value={filterMenuValues[column.id]?.value ?? ""}
2750
3088
  oninput={(e) =>
2751
3089
  updateFilterMenuValue(
@@ -2753,6 +3091,13 @@
2753
3091
  e.currentTarget.value,
2754
3092
  )}
2755
3093
  />
3094
+ {#if tpIsSetOp}
3095
+ <datalist id={`sv-tp-in-list-${column.id}`}>
3096
+ {#each facetValuesForColumn(column.id).slice(0, 200) as v (v)}
3097
+ <option value={v}></option>
3098
+ {/each}
3099
+ </datalist>
3100
+ {/if}
2756
3101
  {#if active === "between"}
2757
3102
  <input
2758
3103
  class="sv-grid-tp-filter-input"