@svgrid/grid 2.6.20 → 2.6.22

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 (110) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +22 -0
  3. package/dist/GridMenus.svelte +17 -12
  4. package/dist/SvGrid.controller.svelte.d.ts +14 -8
  5. package/dist/SvGrid.controller.svelte.js +214 -74
  6. package/dist/SvGrid.css +1 -1
  7. package/dist/SvGrid.svelte +112 -57
  8. package/dist/SvGrid.types.d.ts +191 -1
  9. package/dist/ai.d.ts +28 -0
  10. package/dist/ai.js +6 -0
  11. package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BuoBPqxx.js} +137 -132
  12. package/dist/cdn/GridMenus-n4llxoOI.js +494 -0
  13. package/dist/cdn/column-resize-DsfNXMom.js +102 -0
  14. package/dist/cdn/row-resize-BRcimkUT.js +95 -0
  15. package/dist/cdn/{src-Cd0tearp.js → src-C9Hihx1W.js} +3497 -3487
  16. package/dist/cdn/{src-B1TdiyS8.js → src-D1lXwq1l.js} +8258 -8248
  17. package/dist/cdn/svgrid.js +10 -8
  18. package/dist/cdn/svgrid.svelte-external.js +10 -8
  19. package/dist/cdn/validate-_CDJzgIo.js +75 -0
  20. package/dist/cell-formatting.d.ts +2 -0
  21. package/dist/cell-formatting.js +2 -0
  22. package/dist/chart-export.d.ts +1 -0
  23. package/dist/chart.d.ts +31 -5
  24. package/dist/chart.js +9 -3
  25. package/dist/column-groups.js +1 -1
  26. package/dist/column-resize.d.ts +46 -0
  27. package/dist/column-resize.js +205 -0
  28. package/dist/columns.d.ts +0 -3
  29. package/dist/columns.js +0 -57
  30. package/dist/core.d.ts +216 -4
  31. package/dist/core.js +532 -119
  32. package/dist/createTree.svelte.d.ts +3 -0
  33. package/dist/createTree.svelte.js +1 -0
  34. package/dist/datetime/date-core.d.ts +2 -0
  35. package/dist/datetime/date-restrict.d.ts +1 -0
  36. package/dist/datetime/timezone.d.ts +1 -0
  37. package/dist/dock-manager-model.d.ts +3 -0
  38. package/dist/dock-manager-model.js +1 -0
  39. package/dist/dock-model.d.ts +6 -0
  40. package/dist/dock-model.js +3 -0
  41. package/dist/editor-contract.d.ts +1 -0
  42. package/dist/filtering/excel-filters.js +28 -0
  43. package/dist/group-display.d.ts +1 -1
  44. package/dist/index.d.ts +2 -1
  45. package/dist/index.js +6 -0
  46. package/dist/list-option.d.ts +1 -0
  47. package/dist/menus.js +1 -1
  48. package/dist/positioning.d.ts +2 -0
  49. package/dist/row-resize.d.ts +11 -0
  50. package/dist/row-resize.js +7 -1
  51. package/dist/scheduler-ical.d.ts +1 -0
  52. package/dist/scheduler-model.d.ts +1 -0
  53. package/dist/selection.js +9 -0
  54. package/dist/spreadsheet.d.ts +1 -1
  55. package/dist/spreadsheet.js +1 -1
  56. package/dist/summaries.js +22 -0
  57. package/dist/svgrid-wrapper.types.d.ts +5 -0
  58. package/dist/toast-store.svelte.d.ts +4 -0
  59. package/dist/validate.d.ts +50 -0
  60. package/dist/validate.js +187 -0
  61. package/package.json +4 -1
  62. package/src/GridMenus.svelte +17 -12
  63. package/src/SvGrid.controller.svelte.ts +223 -76
  64. package/src/SvGrid.css +1 -1
  65. package/src/SvGrid.svelte +112 -57
  66. package/src/SvGrid.types.ts +191 -1
  67. package/src/ai.ts +28 -0
  68. package/src/cell-formatting.ts +2 -0
  69. package/src/chart-export.ts +1 -0
  70. package/src/chart.ts +31 -5
  71. package/src/column-groups.ts +1 -1
  72. package/src/column-resize.test.ts +381 -0
  73. package/src/column-resize.ts +227 -0
  74. package/src/columns.test.ts +0 -103
  75. package/src/columns.ts +0 -58
  76. package/src/core.aggregate.test.ts +134 -0
  77. package/src/core.filter.test.ts +156 -0
  78. package/src/core.grouping.test.ts +146 -0
  79. package/src/core.row-shape.test.ts +119 -0
  80. package/src/core.rowmodel-cache.test.ts +121 -0
  81. package/src/core.sort.test.ts +293 -0
  82. package/src/core.ts +723 -119
  83. package/src/createTree.svelte.ts +3 -0
  84. package/src/datetime/date-core.ts +2 -0
  85. package/src/datetime/date-restrict.ts +1 -0
  86. package/src/datetime/timezone.ts +1 -0
  87. package/src/dock-manager-model.ts +3 -0
  88. package/src/dock-model.ts +6 -0
  89. package/src/editor-contract.ts +1 -0
  90. package/src/filtering/excel-filters.ts +30 -0
  91. package/src/filtering/normalize-fast-path.test.ts +104 -0
  92. package/src/group-display.ts +1 -1
  93. package/src/index.ts +12 -1
  94. package/src/list-option.ts +1 -0
  95. package/src/menus.ts +1 -1
  96. package/src/positioning.ts +2 -0
  97. package/src/resize-props.test.ts +361 -0
  98. package/src/row-resize.test.ts +31 -0
  99. package/src/row-resize.ts +21 -3
  100. package/src/scheduler-ical.ts +1 -0
  101. package/src/scheduler-model.ts +1 -0
  102. package/src/selection.ts +9 -0
  103. package/src/spreadsheet.ts +1 -1
  104. package/src/summaries.ts +21 -0
  105. package/src/svgrid-wrapper.types.ts +5 -0
  106. package/src/svgrid.summaries.test.ts +217 -0
  107. package/src/toast-store.svelte.ts +4 -0
  108. package/src/validate.test.ts +207 -0
  109. package/src/validate.ts +269 -0
  110. package/dist/cdn/GridMenus-IHK_l7m6.js +0 -489
@@ -31,6 +31,7 @@ export type TreeNode = {
31
31
  lazy?: boolean
32
32
  }
33
33
 
34
+ /** A node's checkbox state. `'indeterminate'` means some but not all descendants are checked. */
34
35
  export type CheckState = 'checked' | 'indeterminate' | 'unchecked'
35
36
 
36
37
  /** A single visible (flattened) tree row. */
@@ -145,6 +146,7 @@ export type TreeConfig = {
145
146
  filter?: () => string | undefined
146
147
  }
147
148
 
149
+ /** Build the headless tree model: expansion, selection and checkbox cascading, with no markup. */
148
150
  export function createTree(config: TreeConfig) {
149
151
  const nodes = () => config.nodes()
150
152
  const checkable = () => config.checkable?.() ?? false
@@ -316,4 +318,5 @@ export function createTree(config: TreeConfig) {
316
318
  }
317
319
  }
318
320
 
321
+ /** The headless tree instance returned by {@link createTree}. */
319
322
  export type Tree = ReturnType<typeof createTree>
@@ -10,6 +10,7 @@
10
10
  * `firstDayOfWeek`.
11
11
  */
12
12
 
13
+ /** Anything the date helpers accept: a `Date`, epoch milliseconds, or a parseable string. */
13
14
  export type DateLike = Date | number | string
14
15
 
15
16
  /** Coerce a Date | epoch-ms | parseable string to a Date, or null if invalid. */
@@ -137,6 +138,7 @@ export function centuryRange(year: number): { start: number; end: number } {
137
138
  return { start, end: start + 99 }
138
139
  }
139
140
 
141
+ /** One cell of a month grid, including the leading and trailing days from adjacent months. */
140
142
  export type MonthMatrixCell = {
141
143
  date: Date
142
144
  /** In the displayed month (vs. leading/trailing days of adjacent months). */
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { compareDay, isSameDay, toDate, type DateLike } from './date-core'
7
7
 
8
+ /** Which dates a picker allows: bounds, an explicit disabled set, and disabled weekdays. */
8
9
  export type RestrictOptions = {
9
10
  min?: DateLike | null
10
11
  max?: DateLike | null
@@ -12,6 +12,7 @@
12
12
  * headless contexts that forbid `Date.now()`).
13
13
  */
14
14
 
15
+ /** A wall-clock time broken into fields, as it reads in a specific time zone. */
15
16
  export type ZoneParts = {
16
17
  year: number
17
18
  month: number // 1-12
@@ -31,6 +31,7 @@ import {
31
31
  type IdGen,
32
32
  } from './dock-model'
33
33
 
34
+ /** Which edge a pane docks against when dropped. */
34
35
  export type DockSide = Exclude<DockZone, 'center'>
35
36
 
36
37
  /** A floating window: one tabs leaf shown in a movable/resizable frame. */
@@ -58,6 +59,7 @@ export type AutoHideEntry = {
58
59
  size: number
59
60
  }
60
61
 
62
+ /** The whole dock manager: its layout tree plus floating and pinned panes. */
61
63
  export type DockManagerState = {
62
64
  main: DockNode | null
63
65
  floating: FloatWindow[]
@@ -74,6 +76,7 @@ export type PaneLocation =
74
76
  | { kind: 'floating'; windowId: string }
75
77
  | { kind: 'autoHide'; entryId: string }
76
78
 
79
+ /** Find a pane in the layout tree by id, returning it with its parent for mutation. */
77
80
  export function locatePane(state: DockManagerState, paneId: string): PaneLocation | null {
78
81
  if (state.main && findTabsWithPane(state.main, paneId)) return { kind: 'main' }
79
82
  for (const w of state.floating) {
package/src/dock-model.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * without a browser. Modelled on Smart's `smart-layout` group/item structure.
14
14
  */
15
15
 
16
+ /** One dockable pane: its id, title, and the state a tab needs to render. */
16
17
  export type DockPane = {
17
18
  id: string
18
19
  title: string
@@ -40,15 +41,18 @@ export type DockGroup = {
40
41
  sizes: number[]
41
42
  }
42
43
 
44
+ /** A node in the layout tree - either a split group or a tabbed leaf. */
43
45
  export type DockNode = DockGroup | DockTabs
44
46
 
45
47
  /** Where a dragged pane lands relative to a target leaf. */
46
48
  export type DockZone = 'left' | 'right' | 'top' | 'bottom' | 'center'
47
49
 
50
+ /** Supplies ids for newly created nodes, so layouts stay deterministic in tests. */
48
51
  export type IdGen = () => string
49
52
 
50
53
  // ---- construction helpers -------------------------------------------------
51
54
 
55
+ /** Build a {@link DockPane}. */
52
56
  export function pane(id: string, title: string, closable = true): DockPane {
53
57
  return { id, title, closable }
54
58
  }
@@ -58,10 +62,12 @@ export function leafMinSize(node: DockTabs): number {
58
62
  return node.panes.reduce((m, p) => Math.max(m, p.minSize ?? 0), 0)
59
63
  }
60
64
 
65
+ /** Build a tabbed leaf holding the given panes. */
61
66
  export function tabs(genId: IdGen, panes: DockPane[], active = 0): DockTabs {
62
67
  return { type: 'tabs', id: genId(), panes, active: clampIndex(active, panes.length) }
63
68
  }
64
69
 
70
+ /** Build a split group: panes or nested groups laid out in a row or column. */
65
71
  export function group(
66
72
  genId: IdGen,
67
73
  direction: 'row' | 'column',
@@ -10,6 +10,7 @@
10
10
  * Framework-free (no Svelte, no DOM) so it can be unit-tested and reused.
11
11
  */
12
12
 
13
+ /** How much room a cell editor asks for, so the grid can size its overlay. */
13
14
  export type EditorSize = 'sm' | 'md' | 'lg'
14
15
 
15
16
  /**
@@ -49,12 +49,42 @@ export function normalizeForFilter(
49
49
  locale?: string | ReadonlyArray<string>,
50
50
  ): string {
51
51
  if (!s) return ''
52
+
53
+ // Fast path for pure-ASCII input, which is the overwhelming majority of what
54
+ // a grid filters. Both of the expensive steps are provably no-ops there:
55
+ // ASCII is already NFD-normalised, and the combining-marks block this strips
56
+ // (U+0300..U+036F) contains no ASCII. So lowercasing alone is the same
57
+ // answer, and `normalize-fast-path.test.ts` checks that exhaustively over
58
+ // every ASCII code point rather than taking the argument on trust.
59
+ //
60
+ // Worth the branch: this runs once per cell per filter - 100,000 times for a
61
+ // one-column filter over a 100k grid - and a CPU profile of a browser filter
62
+ // put more time in this function than in any other.
63
+ //
64
+ // Note the locale still routes through toLocaleLowerCase even on the fast
65
+ // path: 'I'.toLocaleLowerCase('tr') is 'ı', so ASCII does NOT imply the
66
+ // locale can be ignored.
67
+ if (isAsciiOnly(s)) {
68
+ return locale ? s.toLocaleLowerCase(locale as string | string[]) : s.toLowerCase()
69
+ }
70
+
52
71
  const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
53
72
  return locale
54
73
  ? stripped.toLocaleLowerCase(locale as string | string[])
55
74
  : stripped.toLowerCase()
56
75
  }
57
76
 
77
+ /**
78
+ * True when every code unit is ASCII. A plain char-code scan, which is far
79
+ * cheaper than the `normalize()` + regex pass it lets us skip.
80
+ */
81
+ function isAsciiOnly(s: string): boolean {
82
+ for (let i = 0; i < s.length; i++) {
83
+ if (s.charCodeAt(i) > 127) return false
84
+ }
85
+ return true
86
+ }
87
+
58
88
  /**
59
89
  * Delimiter used to serialise the `in` / `notIn` value list into the single
60
90
  * `value` string that the filter model carries.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Equivalence tests for `normalizeForFilter`'s ASCII fast path.
3
+ *
4
+ * The function NFD-decomposes, strips combining marks, then lowercases. It runs
5
+ * once per cell per filter - 100,000 times for a one-column filter on a 100k
6
+ * grid - and a CPU profile of a browser filter put 15.5% of the whole operation
7
+ * inside it, more than any other single function.
8
+ *
9
+ * For a pure-ASCII string the first two steps are provably no-ops: ASCII is
10
+ * already in NFD, and the combining-marks block (U+0300..U+036F) contains no
11
+ * ASCII. So the fast path returns `lowercase(s)` directly.
12
+ *
13
+ * "Provably" is doing a lot of work in that sentence, so the first test checks
14
+ * it exhaustively over every ASCII code point rather than trusting the argument.
15
+ */
16
+ import { describe, expect, it } from 'vitest'
17
+ import { normalizeForFilter } from './excel-filters'
18
+
19
+ /** The original implementation, verbatim, as the oracle. */
20
+ const DIACRITIC_RE = /[̀-ͯ]/g
21
+ function reference(s: string, locale?: string | ReadonlyArray<string>): string {
22
+ if (!s) return ''
23
+ const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
24
+ return locale
25
+ ? stripped.toLocaleLowerCase(locale as string | string[])
26
+ : stripped.toLowerCase()
27
+ }
28
+
29
+ describe('normalizeForFilter - ASCII fast path equivalence', () => {
30
+ it('matches on every single ASCII code point', () => {
31
+ for (let code = 0; code < 128; code++) {
32
+ const ch = String.fromCharCode(code)
33
+ expect(normalizeForFilter(ch), `code point ${code}`).toBe(reference(ch))
34
+ }
35
+ })
36
+
37
+ it('matches on every ASCII code point under a Turkish locale', () => {
38
+ // Turkish is the locale that makes ASCII lowercasing locale-dependent:
39
+ // 'I'.toLocaleLowerCase('tr') is 'ı', not 'i'. The fast path must still
40
+ // route through toLocaleLowerCase rather than assuming ASCII is safe.
41
+ for (let code = 0; code < 128; code++) {
42
+ const ch = String.fromCharCode(code)
43
+ expect(normalizeForFilter(ch, 'tr'), `code point ${code}`).toBe(reference(ch, 'tr'))
44
+ }
45
+ })
46
+
47
+ const CASES: Array<[string, string]> = [
48
+ ['empty', ''],
49
+ ['plain ascii', 'Hello World'],
50
+ ['ascii with digits and punctuation', 'ABC-123_x.y,z!'],
51
+ ['precomposed accents', 'café CAFÉ Café'],
52
+ ['decomposed accents', 'café'],
53
+ ['mixed ascii and accents', 'naive naïve NAÏVE'],
54
+ ['german sharp s', 'straße STRASSE'],
55
+ ['greek', 'ΣΊΣΥΦΟΣ σίσυφος'],
56
+ ['cyrillic', 'Привет МИР'],
57
+ ['cjk', '日本語テキスト'],
58
+ ['emoji', 'grid 🚀 test'],
59
+ ['combining marks alone', '̀́̂'],
60
+ ['ascii then combining mark', 'áb'],
61
+ ['turkish dotted capital', 'İstanbul ISTANBUL'],
62
+ ['ligature', 'file flow'],
63
+ ['fullwidth', 'ABC'],
64
+ ['nbsp and control chars', 'a b\tc\nd'],
65
+ ['long ascii', 'x'.repeat(500)],
66
+ ['long with one non-ascii at the end', 'x'.repeat(500) + 'é'],
67
+ ]
68
+
69
+ for (const [name, input] of CASES) {
70
+ it(`matches: ${name}`, () => {
71
+ expect(normalizeForFilter(input)).toBe(reference(input))
72
+ })
73
+ it(`matches with locale: ${name}`, () => {
74
+ expect(normalizeForFilter(input, 'tr')).toBe(reference(input, 'tr'))
75
+ expect(normalizeForFilter(input, 'de')).toBe(reference(input, 'de'))
76
+ })
77
+ }
78
+
79
+ it('matches across randomised mixed-script strings', () => {
80
+ let seed = 7
81
+ const rand = () => {
82
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff
83
+ return seed / 0x7fffffff
84
+ }
85
+ const pools = [
86
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_.',
87
+ 'áéíóúàèìòùâêîôûäëïöüñçÁÉÍÓÚÑÇ',
88
+ '̀́̂̃̈',
89
+ 'ΑΒΓΔαβγδ',
90
+ 'абвгдАБВГД',
91
+ '日本語中文한국어',
92
+ ]
93
+ for (let trial = 0; trial < 300; trial++) {
94
+ let s = ''
95
+ const len = 1 + Math.floor(rand() * 20)
96
+ for (let i = 0; i < len; i++) {
97
+ const pool = pools[Math.floor(rand() * pools.length)]!
98
+ s += pool[Math.floor(rand() * pool.length)]
99
+ }
100
+ expect(normalizeForFilter(s), s).toBe(reference(s))
101
+ expect(normalizeForFilter(s, 'tr'), s).toBe(reference(s, 'tr'))
102
+ }
103
+ })
104
+ })
@@ -14,7 +14,7 @@
14
14
  * they are trivially unit-testable and carry no dependency on the grid runtime.
15
15
  */
16
16
 
17
- /** AG-Grid-style grouping display variants. */
17
+ /** Grouping display variants. */
18
18
  export type GroupDisplayType = 'groupRows' | 'singleColumn' | 'multipleColumns'
19
19
 
20
20
  export type GroupFooterOptions<R> = {
package/src/index.ts CHANGED
@@ -588,7 +588,13 @@ export {
588
588
  type CellBorderSpec,
589
589
  type BorderSpec,
590
590
  } from './spreadsheet'
591
+ // The two resize actions, exported as a matched pair. `<SvGrid>` drives both
592
+ // through the `rowResize` / `columnResize` props - both default OFF, both are
593
+ // turned on by setting them true, and the grid `import()`s the module itself so
594
+ // a grid that leaves them off carries neither. Import them directly only for a
595
+ // headless table, which renders its own rows and headers.
591
596
  export { rowResize, type RowResizeOptions } from './row-resize'
597
+ export { columnResize, type ColumnResizeOptions } from './column-resize'
592
598
  export { rowDropZone, type RowDragEndEvent, type RowDropZoneOptions } from './row-drag'
593
599
  export { resolveColumnTypes, inferCellDataType, type CellDataType } from './column-types'
594
600
  export {
@@ -690,7 +696,12 @@ export { createVirtualizer } from './virtualization/virtualizer'
690
696
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte'
691
697
  export { createColumnVirtualizer } from './virtualization/column-virtualizer'
692
698
  export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types'
693
- export type { SvGridApi, SvGridFilterOperator, SvGridWrapperProps } from './svgrid-wrapper.types'
699
+ export type {
700
+ SvGridApi,
701
+ SvGridFilterOperator,
702
+ SvGridViewState,
703
+ SvGridWrapperProps,
704
+ } from './svgrid-wrapper.types'
694
705
  export {
695
706
  parseEditorValue,
696
707
  normalizeEditorOptions,
@@ -71,6 +71,7 @@ export type VirtualListRow =
71
71
  | { type: 'group'; label: string; size: number }
72
72
  | { type: 'option'; opt: IndexedOption; size: number }
73
73
 
74
+ /** A flattened option list plus its measurements, for virtualizing long dropdowns. */
74
75
  export type FlatVirtualModel = {
75
76
  /** Group headings + options in render order, each with its px height. */
76
77
  entries: VirtualListRow[]
package/src/menus.ts CHANGED
@@ -151,7 +151,7 @@ export function createMenus<
151
151
  function openChooseColumns(event: MouseEvent) {
152
152
  event.stopPropagation();
153
153
  // Submenu behavior: keep the parent operations menu open and float the
154
- // Choose Columns panel out to the right of it (AG-Grid style). Falls
154
+ // Choose Columns panel out to the right of it, submenu-style. Falls
155
155
  // back to below the item if the popover would clip the viewport.
156
156
  const trigger = event.currentTarget as HTMLElement;
157
157
  const menuEl = trigger.closest(".sv-grid-menu") as HTMLElement | null;
@@ -35,6 +35,7 @@ export type Rect = { x: number; y: number; width: number; height: number }
35
35
  /** The available viewport (defaults to `window` in the browser). */
36
36
  export type Viewport = { width: number; height: number }
37
37
 
38
+ /** Where to put a floating element: its preferred side, offsets, and collision behaviour. */
38
39
  export type ComputePositionOptions = {
39
40
  /** Preferred placement. Default `'bottom-start'`. */
40
41
  placement?: Placement
@@ -58,6 +59,7 @@ export type ComputePositionOptions = {
58
59
  minMainAxis?: number
59
60
  }
60
61
 
62
+ /** The resolved position, including the side actually used after collision handling. */
61
63
  export type ComputePositionResult = {
62
64
  /** Floating left, in viewport (fixed-position) coordinates. */
63
65
  x: number