@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
@@ -1,8 +1,14 @@
1
1
  // Vitest setup. jsdom doesn't ship ResizeObserver, IntersectionObserver,
2
2
  // or the layout APIs SvGrid touches inside its mount effects. Provide
3
3
  // minimal no-op stubs so component mounting completes without crashing.
4
+ //
5
+ // These assign to globals/prototypes the jsdom env doesn't fully type, so
6
+ // the targets are cast to `any`. We deliberately avoid expect-error
7
+ // suppression directives: when a stub happens to line up with the lib types,
8
+ // such a directive becomes "unused" and svelte-check fails the build (an
9
+ // unused expect-error directive is itself an error).
4
10
  if (typeof globalThis.ResizeObserver === 'undefined') {
5
- // @ts-expect-error - assigning to a globalThis property the env doesn't ship
11
+ ;
6
12
  globalThis.ResizeObserver = class {
7
13
  observe() { }
8
14
  unobserve() { }
@@ -10,7 +16,7 @@ if (typeof globalThis.ResizeObserver === 'undefined') {
10
16
  };
11
17
  }
12
18
  if (typeof globalThis.IntersectionObserver === 'undefined') {
13
- // @ts-expect-error - same
19
+ ;
14
20
  globalThis.IntersectionObserver = class {
15
21
  observe() { }
16
22
  unobserve() { }
@@ -23,7 +29,7 @@ if (typeof globalThis.IntersectionObserver === 'undefined') {
23
29
  // jsdom's HTMLElement.scrollIntoView is a no-op; some grid code calls it
24
30
  // during the first effect. Make sure the method exists on every element.
25
31
  if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
26
- // @ts-expect-error - patching a missing DOM method
32
+ ;
27
33
  Element.prototype.scrollIntoView = function () { };
28
34
  }
29
35
  export {};
@@ -8,6 +8,23 @@ export type RowScrollScaling = {
8
8
  /** Map a virtualizer logical offset back into a DOM scrollTop. */
9
9
  logicalToDom(logical: number): number;
10
10
  };
11
+ /**
12
+ * Resolve the usable max element height from the two raw DOM signals.
13
+ *
14
+ * `layoutCap` is a tall probe's clamped `offsetHeight` - the height layout
15
+ * assigns the element. `scrollCap` is the `scrollHeight` a real `overflow:auto`
16
+ * container exposes for that same probe - the height the user can actually
17
+ * scroll through. They differ on mobile / high-DPR engines, which report a
18
+ * generous `offsetHeight` but then expose a SMALLER scrollable range (the
19
+ * physical limit is in device px, so a 3x-DPR phone has ~1/3 the CSS-px scroll
20
+ * cap). Trusting `offsetHeight` alone is exactly what strands the last rows of
21
+ * a huge grid on a phone, so we take the smaller of the two.
22
+ *
23
+ * A junk reading (<= the sanity floor, or so large the browser clearly didn't
24
+ * clamp) is dropped; if neither signal survives, the caller's conservative
25
+ * `fallback` is returned unchanged.
26
+ */
27
+ export declare function resolveMaxDomHeight(layoutCap: number, scrollCap: number, fallback: number): number;
11
28
  /**
12
29
  * Build the scaling mapping for one axis.
13
30
  *
@@ -21,6 +21,41 @@ function clamp01(value) {
21
21
  return 1;
22
22
  return value;
23
23
  }
24
+ /** Below this a reading is junk (0 from jsdom, a sub-cap layout quirk, etc). */
25
+ const MAX_DOM_HEIGHT_SANITY_FLOOR = 100000;
26
+ /** At/above this the browser didn't clamp (returned ~the 1e9 we asked for). */
27
+ const MAX_DOM_HEIGHT_SANITY_CEIL = 900000000;
28
+ /**
29
+ * Shave a hair off the detected cap so the spacer never sits at the exact
30
+ * physical edge, where sub-pixel rounding could leave the final row a touch
31
+ * out of reach. 0.5% is invisible at desktop caps (~33.5M -> ~33.3M) yet
32
+ * keeps a safety gap proportional to the cap.
33
+ */
34
+ const MAX_DOM_HEIGHT_SAFETY = 0.995;
35
+ /**
36
+ * Resolve the usable max element height from the two raw DOM signals.
37
+ *
38
+ * `layoutCap` is a tall probe's clamped `offsetHeight` - the height layout
39
+ * assigns the element. `scrollCap` is the `scrollHeight` a real `overflow:auto`
40
+ * container exposes for that same probe - the height the user can actually
41
+ * scroll through. They differ on mobile / high-DPR engines, which report a
42
+ * generous `offsetHeight` but then expose a SMALLER scrollable range (the
43
+ * physical limit is in device px, so a 3x-DPR phone has ~1/3 the CSS-px scroll
44
+ * cap). Trusting `offsetHeight` alone is exactly what strands the last rows of
45
+ * a huge grid on a phone, so we take the smaller of the two.
46
+ *
47
+ * A junk reading (<= the sanity floor, or so large the browser clearly didn't
48
+ * clamp) is dropped; if neither signal survives, the caller's conservative
49
+ * `fallback` is returned unchanged.
50
+ */
51
+ export function resolveMaxDomHeight(layoutCap, scrollCap, fallback) {
52
+ const usable = [layoutCap, scrollCap].filter((v) => Number.isFinite(v) &&
53
+ v > MAX_DOM_HEIGHT_SANITY_FLOOR &&
54
+ v < MAX_DOM_HEIGHT_SANITY_CEIL);
55
+ if (usable.length === 0)
56
+ return fallback;
57
+ return Math.floor(Math.min(...usable) * MAX_DOM_HEIGHT_SAFETY);
58
+ }
24
59
  /**
25
60
  * Build the scaling mapping for one axis.
26
61
  *
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { createRowScrollScaling } from './scroll-scaling';
2
+ import { createRowScrollScaling, resolveMaxDomHeight } from './scroll-scaling';
3
3
  const VIEWPORT = 600;
4
+ const FALLBACK = 8000000;
4
5
  describe('createRowScrollScaling - inert (content fits under the cap)', () => {
5
6
  // 1M rows * 18px = 18M, under a 33.5M (Chrome) cap -> no scaling.
6
7
  const s = createRowScrollScaling(18000000, 33554400, VIEWPORT);
@@ -62,6 +63,46 @@ describe('createRowScrollScaling - active (content exceeds the cap)', () => {
62
63
  }
63
64
  });
64
65
  });
66
+ describe('resolveMaxDomHeight - picks the smaller, real scrollable cap', () => {
67
+ it('keeps the desktop cap when both signals agree', () => {
68
+ // Chrome ~33.5M; offsetHeight and scrollHeight match. 0.5% safety shave.
69
+ const cap = resolveMaxDomHeight(33554400, 33554400, FALLBACK);
70
+ expect(cap).toBeLessThan(33554400);
71
+ expect(cap).toBeGreaterThan(33000000);
72
+ });
73
+ it('uses the SCROLL cap when a phone over-reports offsetHeight', () => {
74
+ // High-DPR mobile: layout says 24M, but the container only scrolls ~6M.
75
+ // Trusting offsetHeight is exactly what stranded the last rows.
76
+ const cap = resolveMaxDomHeight(24000000, 6000000, FALLBACK);
77
+ expect(cap).toBeLessThanOrEqual(6000000);
78
+ expect(cap).toBeGreaterThan(5900000);
79
+ });
80
+ it('shaves a safety margin so the spacer never sits at the exact edge', () => {
81
+ const cap = resolveMaxDomHeight(10000000, 10000000, FALLBACK);
82
+ expect(cap).toBeLessThan(10000000);
83
+ });
84
+ it('falls back when both readings are junk (jsdom returns 0)', () => {
85
+ expect(resolveMaxDomHeight(0, 0, FALLBACK)).toBe(FALLBACK);
86
+ });
87
+ it('falls back when the browser did not clamp (returned ~1e9)', () => {
88
+ expect(resolveMaxDomHeight(1000000000, 1000000000, FALLBACK)).toBe(FALLBACK);
89
+ });
90
+ it('ignores a junk signal but trusts the valid one', () => {
91
+ // offsetHeight unclamped/garbage, scrollHeight is the real cap.
92
+ const cap = resolveMaxDomHeight(1000000000, 5000000, FALLBACK);
93
+ expect(cap).toBeLessThanOrEqual(5000000);
94
+ expect(cap).toBeGreaterThan(4900000);
95
+ });
96
+ it('feeds straight into the scaler so the last row stays reachable', () => {
97
+ const maxDom = resolveMaxDomHeight(24000000, 6000000, FALLBACK);
98
+ const trueTotal = 500000 * 32; // 16M px of rows, over the phone cap
99
+ const s = createRowScrollScaling(trueTotal, maxDom, VIEWPORT);
100
+ expect(s.active).toBe(true);
101
+ expect(s.domTotal).toBeLessThanOrEqual(maxDom);
102
+ const domMax = s.domTotal - VIEWPORT;
103
+ expect(s.domToLogical(domMax)).toBeCloseTo(trueTotal - VIEWPORT, 0);
104
+ });
105
+ });
65
106
  describe('createRowScrollScaling - edge cases', () => {
66
107
  it('handles an empty grid without dividing by zero', () => {
67
108
  const s = createRowScrollScaling(0, 17895697, VIEWPORT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "SvGrid - a headless-first, Svelte 5-native data grid engine and render component.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -60,6 +60,7 @@
60
60
  "@vitest/coverage-v8": "^4.1.5",
61
61
  "eslint-plugin-svelte": "^3.17.1",
62
62
  "jsdom": "^29.1.1",
63
+ "publint": "^0.3.21",
63
64
  "svelte": "^5.55.5",
64
65
  "svelte-check": "^4.4.6"
65
66
  },
@@ -1,96 +1,96 @@
1
- <script
2
- lang="ts"
3
- generics="TData extends RowData"
4
- >
5
- import { isFunction } from './core'
6
- import {
7
- RenderComponentConfig,
8
- RenderSnippetConfig,
9
- } from './render-component'
10
- import type {
11
- Cell,
12
- CellContext,
13
- ColumnDefTemplate,
14
- Header,
15
- HeaderContext,
16
- RowData,
17
- } from './core'
18
-
19
- type Props =
20
- | {
21
- content?: ColumnDefTemplate<
22
- | HeaderContext<TData>
23
- | CellContext<TData>
24
- >
25
- context:
26
- | HeaderContext<TData>
27
- | CellContext<TData>
28
- cell?: never
29
- header?: never
30
- footer?: never
31
- }
32
- | {
33
- cell: Cell<TData>
34
- content?: never
35
- context?: never
36
- header?: never
37
- footer?: never
38
- }
39
- | {
40
- header: Header<TData>
41
- content?: never
42
- context?: never
43
- cell?: never
44
- footer?: never
45
- }
46
- | {
47
- footer: Header<TData>
48
- content?: never
49
- context?: never
50
- cell?: never
51
- header?: never
52
- }
53
-
54
- let props: Props = $props()
55
-
56
- const resolved = $derived.by(() => {
57
- if ('cell' in props && props.cell) {
58
- return {
59
- content: props.cell.column.columnDef.cell,
60
- context: props.cell.getContext(),
61
- }
62
- }
63
- if ('header' in props && props.header) {
64
- return {
65
- content: props.header.column.columnDef.header,
66
- context: props.header.getContext(),
67
- }
68
- }
69
- if ('footer' in props && props.footer) {
70
- return {
71
- content: props.footer.column.columnDef.footer,
72
- context: props.footer.getContext(),
73
- }
74
- }
75
- return {
76
- content: props.content,
77
- context: props.context,
78
- }
79
- })
80
-
81
- const result = $derived(
82
- isFunction(resolved.content)
83
- ? resolved.content(resolved.context as any)
84
- : undefined,
85
- )
86
- </script>
87
-
88
- {#if typeof resolved.content === 'string'}
89
- {resolved.content}
90
- {:else if result instanceof RenderComponentConfig}
91
- <result.component {...result.props} />
92
- {:else if result instanceof RenderSnippetConfig}
93
- {@render result.snippet(result.params)}
94
- {:else if result !== undefined}
95
- {result}
96
- {/if}
1
+ <script
2
+ lang="ts"
3
+ generics="TData extends RowData"
4
+ >
5
+ import { isFunction } from './core'
6
+ import {
7
+ RenderComponentConfig,
8
+ RenderSnippetConfig,
9
+ } from './render-component'
10
+ import type {
11
+ Cell,
12
+ CellContext,
13
+ ColumnDefTemplate,
14
+ Header,
15
+ HeaderContext,
16
+ RowData,
17
+ } from './core'
18
+
19
+ type Props =
20
+ | {
21
+ content?: ColumnDefTemplate<
22
+ | HeaderContext<TData>
23
+ | CellContext<TData>
24
+ >
25
+ context:
26
+ | HeaderContext<TData>
27
+ | CellContext<TData>
28
+ cell?: never
29
+ header?: never
30
+ footer?: never
31
+ }
32
+ | {
33
+ cell: Cell<TData>
34
+ content?: never
35
+ context?: never
36
+ header?: never
37
+ footer?: never
38
+ }
39
+ | {
40
+ header: Header<TData>
41
+ content?: never
42
+ context?: never
43
+ cell?: never
44
+ footer?: never
45
+ }
46
+ | {
47
+ footer: Header<TData>
48
+ content?: never
49
+ context?: never
50
+ cell?: never
51
+ header?: never
52
+ }
53
+
54
+ let props: Props = $props()
55
+
56
+ const resolved = $derived.by(() => {
57
+ if ('cell' in props && props.cell) {
58
+ return {
59
+ content: props.cell.column.columnDef.cell,
60
+ context: props.cell.getContext(),
61
+ }
62
+ }
63
+ if ('header' in props && props.header) {
64
+ return {
65
+ content: props.header.column.columnDef.header,
66
+ context: props.header.getContext(),
67
+ }
68
+ }
69
+ if ('footer' in props && props.footer) {
70
+ return {
71
+ content: props.footer.column.columnDef.footer,
72
+ context: props.footer.getContext(),
73
+ }
74
+ }
75
+ return {
76
+ content: props.content,
77
+ context: props.context,
78
+ }
79
+ })
80
+
81
+ const result = $derived(
82
+ isFunction(resolved.content)
83
+ ? resolved.content(resolved.context as any)
84
+ : undefined,
85
+ )
86
+ </script>
87
+
88
+ {#if typeof resolved.content === 'string'}
89
+ {resolved.content}
90
+ {:else if result instanceof RenderComponentConfig}
91
+ <result.component {...result.props} />
92
+ {:else if result instanceof RenderSnippetConfig}
93
+ {@render result.snippet(result.params)}
94
+ {:else if result !== undefined}
95
+ {result}
96
+ {/if}