@svgrid/grid 1.0.2 → 1.1.1

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 (216) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +137 -39
  3. package/dist/GridFooter.svelte +164 -0
  4. package/dist/GridFooter.svelte.d.ts +29 -0
  5. package/dist/GridMenus.svelte +648 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +472 -0
  8. package/dist/SvGrid.controller.svelte.js +1917 -0
  9. package/dist/SvGrid.css +2012 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.helpers.test.d.ts +1 -0
  13. package/dist/SvGrid.helpers.test.js +298 -0
  14. package/dist/SvGrid.svelte +2346 -8742
  15. package/dist/SvGrid.svelte.d.ts +4 -357
  16. package/dist/SvGrid.types.d.ts +523 -0
  17. package/dist/SvGrid.types.js +1 -0
  18. package/dist/SvGridChart.svelte +1060 -23
  19. package/dist/SvGridChart.svelte.d.ts +17 -0
  20. package/dist/aligned-grids.d.ts +6 -0
  21. package/dist/aligned-grids.js +84 -0
  22. package/dist/aligned-grids.test.d.ts +1 -0
  23. package/dist/aligned-grids.test.js +75 -0
  24. package/dist/build-api.coverage.test.d.ts +20 -0
  25. package/dist/build-api.coverage.test.js +505 -0
  26. package/dist/build-api.d.ts +5 -0
  27. package/dist/build-api.js +557 -0
  28. package/dist/cell-render.d.ts +15 -0
  29. package/dist/cell-render.js +246 -0
  30. package/dist/cell-render.test.d.ts +1 -0
  31. package/dist/cell-render.test.js +338 -0
  32. package/dist/cell-values.d.ts +28 -0
  33. package/dist/cell-values.js +89 -0
  34. package/dist/chart-export.test.d.ts +1 -0
  35. package/dist/chart-export.test.js +302 -0
  36. package/dist/chart.coverage.test.d.ts +1 -0
  37. package/dist/chart.coverage.test.js +748 -0
  38. package/dist/chart.d.ts +370 -3
  39. package/dist/chart.js +1135 -42
  40. package/dist/clipboard.d.ts +15 -0
  41. package/dist/clipboard.js +420 -0
  42. package/dist/clipboard.test.d.ts +1 -0
  43. package/dist/clipboard.test.js +700 -0
  44. package/dist/collaboration.coverage.test.d.ts +1 -0
  45. package/dist/collaboration.coverage.test.js +200 -0
  46. package/dist/column-groups.d.ts +19 -0
  47. package/dist/column-groups.js +62 -0
  48. package/dist/column-groups.test.d.ts +1 -0
  49. package/dist/column-groups.test.js +56 -0
  50. package/dist/column-types.d.ts +10 -0
  51. package/dist/column-types.js +63 -0
  52. package/dist/column-types.test.d.ts +1 -0
  53. package/dist/column-types.test.js +62 -0
  54. package/dist/columns.d.ts +30 -0
  55. package/dist/columns.js +277 -0
  56. package/dist/columns.test.d.ts +1 -0
  57. package/dist/columns.test.js +625 -0
  58. package/dist/core.d.ts +86 -1
  59. package/dist/css.d.ts +3 -0
  60. package/dist/editing.d.ts +31 -0
  61. package/dist/editing.js +529 -0
  62. package/dist/editing.test.d.ts +1 -0
  63. package/dist/editing.test.js +732 -0
  64. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  65. package/dist/editors/cell-editors.coverage.test.js +139 -0
  66. package/dist/editors/cell-editors.d.ts +1 -1
  67. package/dist/facet-buckets.d.ts +13 -0
  68. package/dist/facet-buckets.js +54 -0
  69. package/dist/facet-buckets.test.d.ts +1 -0
  70. package/dist/facet-buckets.test.js +296 -0
  71. package/dist/features.d.ts +5 -0
  72. package/dist/features.js +30 -0
  73. package/dist/filter-operators.d.ts +16 -0
  74. package/dist/filter-operators.js +69 -0
  75. package/dist/filter-operators.test.d.ts +1 -0
  76. package/dist/filter-operators.test.js +135 -0
  77. package/dist/hyperformula-adapter.d.ts +82 -0
  78. package/dist/hyperformula-adapter.js +73 -0
  79. package/dist/hyperformula-adapter.test.d.ts +1 -0
  80. package/dist/hyperformula-adapter.test.js +205 -0
  81. package/dist/index.d.ts +9 -3
  82. package/dist/index.js +8 -2
  83. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  84. package/dist/keyboard-handlers.coverage.test.js +495 -0
  85. package/dist/keyboard-handlers.d.ts +7 -0
  86. package/dist/keyboard-handlers.js +202 -0
  87. package/dist/menus.d.ts +40 -0
  88. package/dist/menus.js +390 -0
  89. package/dist/menus.test.d.ts +1 -0
  90. package/dist/menus.test.js +560 -0
  91. package/dist/named-views.coverage.test.d.ts +1 -0
  92. package/dist/named-views.coverage.test.js +180 -0
  93. package/dist/named-views.d.ts +27 -0
  94. package/dist/named-views.js +39 -0
  95. package/dist/row-drag.d.ts +49 -0
  96. package/dist/row-drag.js +221 -0
  97. package/dist/row-drag.test.d.ts +1 -0
  98. package/dist/row-drag.test.js +142 -0
  99. package/dist/row-resize.d.ts +43 -0
  100. package/dist/row-resize.js +158 -0
  101. package/dist/row-resize.test.d.ts +1 -0
  102. package/dist/row-resize.test.js +329 -0
  103. package/dist/scroll-sync.d.ts +9 -0
  104. package/dist/scroll-sync.js +89 -0
  105. package/dist/scroll-sync.test.d.ts +1 -0
  106. package/dist/scroll-sync.test.js +290 -0
  107. package/dist/selection.d.ts +32 -0
  108. package/dist/selection.js +427 -0
  109. package/dist/selection.multi-range.test.d.ts +1 -0
  110. package/dist/selection.multi-range.test.js +55 -0
  111. package/dist/selection.test.d.ts +1 -0
  112. package/dist/selection.test.js +647 -0
  113. package/dist/server-data-source.coverage.test.d.ts +1 -0
  114. package/dist/server-data-source.coverage.test.js +154 -0
  115. package/dist/spreadsheet.d.ts +110 -0
  116. package/dist/spreadsheet.js +242 -0
  117. package/dist/spreadsheet.test.d.ts +1 -0
  118. package/dist/spreadsheet.test.js +446 -0
  119. package/dist/summaries.d.ts +12 -0
  120. package/dist/summaries.js +65 -0
  121. package/dist/sv-grid-scrollbar.js +13 -1
  122. package/dist/svgrid-wrapper.types.d.ts +31 -1
  123. package/dist/svgrid.behavior.test.js +20 -0
  124. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  125. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  126. package/dist/svgrid.context-menu.test.d.ts +1 -0
  127. package/dist/svgrid.context-menu.test.js +102 -0
  128. package/dist/svgrid.interaction.test.js +31 -0
  129. package/dist/svgrid.new-features.wrapper.test.js +64 -6
  130. package/dist/svgrid.wrapper.test.js +27 -1
  131. package/dist/test-setup.js +9 -3
  132. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  133. package/dist/virtualization/scroll-scaling.d.ts +45 -0
  134. package/dist/virtualization/scroll-scaling.js +99 -0
  135. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  136. package/dist/virtualization/scroll-scaling.test.js +127 -0
  137. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  138. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  139. package/dist/virtualization/virtualizer.d.ts +7 -0
  140. package/dist/virtualization/virtualizer.js +30 -0
  141. package/package.json +2 -1
  142. package/src/GridFooter.svelte +164 -0
  143. package/src/GridMenus.svelte +648 -0
  144. package/src/SvGrid.controller.svelte.ts +2352 -0
  145. package/src/SvGrid.css +2012 -0
  146. package/src/SvGrid.helpers.test.ts +415 -0
  147. package/src/SvGrid.helpers.ts +185 -0
  148. package/src/SvGrid.svelte +2346 -8742
  149. package/src/SvGrid.types.ts +537 -0
  150. package/src/SvGridChart.svelte +1060 -23
  151. package/src/aligned-grids.test.ts +80 -0
  152. package/src/aligned-grids.ts +87 -0
  153. package/src/build-api.coverage.test.ts +532 -0
  154. package/src/build-api.ts +683 -0
  155. package/src/cell-render.test.ts +451 -0
  156. package/src/cell-render.ts +426 -0
  157. package/src/cell-values.ts +114 -0
  158. package/src/chart-export.test.ts +370 -0
  159. package/src/chart.coverage.test.ts +814 -0
  160. package/src/chart.ts +1352 -47
  161. package/src/clipboard.test.ts +780 -0
  162. package/src/clipboard.ts +552 -0
  163. package/src/collaboration.coverage.test.ts +220 -0
  164. package/src/column-groups.test.ts +59 -0
  165. package/src/column-groups.ts +80 -0
  166. package/src/column-types.test.ts +68 -0
  167. package/src/column-types.ts +82 -0
  168. package/src/columns.test.ts +702 -0
  169. package/src/columns.ts +419 -0
  170. package/src/core.ts +86 -0
  171. package/src/css.d.ts +3 -0
  172. package/src/editing.test.ts +837 -0
  173. package/src/editing.ts +669 -0
  174. package/src/editors/cell-editors.coverage.test.ts +156 -0
  175. package/src/editors/cell-editors.ts +1 -0
  176. package/src/facet-buckets.test.ts +353 -0
  177. package/src/facet-buckets.ts +67 -0
  178. package/src/features.ts +128 -0
  179. package/src/filter-operators.test.ts +174 -0
  180. package/src/filter-operators.ts +87 -0
  181. package/src/hyperformula-adapter.test.ts +256 -0
  182. package/src/hyperformula-adapter.ts +124 -0
  183. package/src/index.ts +49 -0
  184. package/src/keyboard-handlers.coverage.test.ts +560 -0
  185. package/src/keyboard-handlers.ts +353 -0
  186. package/src/keyboard.ts +97 -97
  187. package/src/menus.test.ts +620 -0
  188. package/src/menus.ts +555 -0
  189. package/src/named-views.coverage.test.ts +210 -0
  190. package/src/named-views.ts +48 -0
  191. package/src/row-drag.test.ts +168 -0
  192. package/src/row-drag.ts +255 -0
  193. package/src/row-resize.test.ts +369 -0
  194. package/src/row-resize.ts +171 -0
  195. package/src/scroll-sync.test.ts +330 -0
  196. package/src/scroll-sync.ts +218 -0
  197. package/src/selection.multi-range.test.ts +61 -0
  198. package/src/selection.test.ts +722 -0
  199. package/src/selection.ts +579 -0
  200. package/src/server-data-source.coverage.test.ts +180 -0
  201. package/src/spreadsheet.test.ts +489 -0
  202. package/src/spreadsheet.ts +304 -0
  203. package/src/summaries.ts +204 -0
  204. package/src/sv-grid-scrollbar.ts +13 -1
  205. package/src/svgrid-wrapper.types.ts +31 -1
  206. package/src/svgrid.behavior.test.ts +22 -0
  207. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  208. package/src/svgrid.context-menu.test.ts +126 -0
  209. package/src/svgrid.interaction.test.ts +30 -0
  210. package/src/svgrid.new-features.wrapper.test.ts +67 -6
  211. package/src/svgrid.wrapper.test.ts +27 -1
  212. package/src/test-setup.ts +9 -6
  213. package/src/virtualization/scroll-scaling.test.ts +148 -0
  214. package/src/virtualization/scroll-scaling.ts +121 -0
  215. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  216. package/src/virtualization/virtualizer.ts +26 -0
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createServerDataSource } from './server-data-source';
3
+ const flush = () => new Promise((r) => setTimeout(r, 0));
4
+ describe('createServerDataSource - request params', () => {
5
+ it('passes startRow/endRow/page/sort/filter through to getRows', async () => {
6
+ const getRows = vi.fn(async (_req) => ({ rows: [], rowCount: 0 }));
7
+ const ctl = createServerDataSource({ getRows }, { pageSize: 20, onChange: () => { } });
8
+ ctl.setFilter({ global: 'abc', columns: { name: { operator: 'eq', value: 'x' } } });
9
+ await flush();
10
+ const req = getRows.mock.calls.at(-1)[0];
11
+ expect(req).toMatchObject({
12
+ startRow: 0,
13
+ endRow: 20,
14
+ pageIndex: 0,
15
+ pageSize: 20,
16
+ filterModel: { global: 'abc', columns: { name: { operator: 'eq', value: 'x' } } },
17
+ });
18
+ });
19
+ it('setFilter resets to page 0', async () => {
20
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 100 }));
21
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
22
+ ctl.setPage(3);
23
+ await flush();
24
+ ctl.setFilter({ global: 'q' });
25
+ await flush();
26
+ expect(ctl.getState().pageIndex).toBe(0);
27
+ const req = getRows.mock.calls.at(-1)[0];
28
+ expect(req.startRow).toBe(0);
29
+ });
30
+ it('uses the default page size of 50 when unspecified', () => {
31
+ const ctl = createServerDataSource({ getRows: async () => ({ rows: [], rowCount: 0 }) }, { onChange: () => { } });
32
+ expect(ctl.getState().pageSize).toBe(50);
33
+ });
34
+ });
35
+ describe('createServerDataSource - setPage edge cases', () => {
36
+ it('clamps a negative page index to 0', async () => {
37
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 0 }));
38
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
39
+ ctl.setPage(5);
40
+ await flush();
41
+ getRows.mockClear();
42
+ ctl.setPage(-3);
43
+ await flush();
44
+ expect(ctl.getState().pageIndex).toBe(0);
45
+ expect(getRows).toHaveBeenCalledTimes(1);
46
+ });
47
+ it('does nothing when the page index is unchanged', async () => {
48
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 0 }));
49
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
50
+ await flush();
51
+ getRows.mockClear();
52
+ ctl.setPage(0); // already on page 0
53
+ await flush();
54
+ expect(getRows).not.toHaveBeenCalled();
55
+ });
56
+ });
57
+ describe('createServerDataSource - setPageSize', () => {
58
+ it('clamps page size to a minimum of 1, resets page, and re-fetches', async () => {
59
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 0 }));
60
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
61
+ ctl.setPage(2);
62
+ await flush();
63
+ getRows.mockClear();
64
+ ctl.setPageSize(0);
65
+ await flush();
66
+ expect(ctl.getState().pageSize).toBe(1);
67
+ expect(ctl.getState().pageIndex).toBe(0);
68
+ expect(getRows).toHaveBeenCalledTimes(1);
69
+ });
70
+ it('recomputes pageCount from total and page size', async () => {
71
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 95 }));
72
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
73
+ ctl.refresh();
74
+ await flush();
75
+ expect(ctl.getState().pageCount).toBe(10); // ceil(95/10)
76
+ ctl.setPageSize(20);
77
+ await flush();
78
+ expect(ctl.getState().pageCount).toBe(5); // ceil(95/20)
79
+ });
80
+ });
81
+ describe('createServerDataSource - error handling', () => {
82
+ it('captures a rejection into state.error and clears rows', async () => {
83
+ const boom = new Error('network down');
84
+ const ctl = createServerDataSource({ getRows: async () => { throw boom; } }, { pageSize: 10, onChange: () => { } });
85
+ ctl.refresh();
86
+ await flush();
87
+ const s = ctl.getState();
88
+ expect(s.error).toBe(boom);
89
+ expect(s.rows).toEqual([]);
90
+ expect(s.loading).toBe(false);
91
+ });
92
+ it('clears a prior error on the next successful fetch', async () => {
93
+ let fail = true;
94
+ const ctl = createServerDataSource({
95
+ getRows: async () => {
96
+ if (fail)
97
+ throw new Error('x');
98
+ return { rows: [{ id: 1 }], rowCount: 1 };
99
+ },
100
+ }, { pageSize: 10, onChange: () => { } });
101
+ ctl.refresh();
102
+ await flush();
103
+ expect(ctl.getState().error).toBeInstanceOf(Error);
104
+ fail = false;
105
+ ctl.refresh();
106
+ await flush();
107
+ expect(ctl.getState().error).toBe(null);
108
+ expect(ctl.getState().rows).toEqual([{ id: 1 }]);
109
+ });
110
+ it('ignores a stale REJECTION when a newer request superseded it', async () => {
111
+ let call = 0;
112
+ const src = {
113
+ getRows: vi.fn(async (req) => {
114
+ call += 1;
115
+ if (call === 1) {
116
+ await new Promise((r) => setTimeout(r, 40));
117
+ throw new Error('stale failure');
118
+ }
119
+ await new Promise((r) => setTimeout(r, 5));
120
+ return { rows: [{ id: req.startRow }], rowCount: 100 };
121
+ }),
122
+ };
123
+ const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => { } });
124
+ ctl.refresh(); // slow + throws
125
+ ctl.setPage(2); // fast + wins
126
+ await new Promise((r) => setTimeout(r, 80));
127
+ const s = ctl.getState();
128
+ expect(s.error).toBe(null); // stale rejection did not land
129
+ expect(s.rows).toEqual([{ id: 20 }]);
130
+ });
131
+ });
132
+ describe('createServerDataSource - dispose', () => {
133
+ it('refresh after dispose does not call getRows', async () => {
134
+ const getRows = vi.fn(async () => ({ rows: [], rowCount: 0 }));
135
+ const ctl = createServerDataSource({ getRows }, { pageSize: 10, onChange: () => { } });
136
+ ctl.dispose();
137
+ ctl.refresh();
138
+ await flush();
139
+ expect(getRows).not.toHaveBeenCalled();
140
+ });
141
+ it('a response resolving after dispose is ignored', async () => {
142
+ const onChange = vi.fn();
143
+ const ctl = createServerDataSource({
144
+ getRows: async () => {
145
+ await new Promise((r) => setTimeout(r, 30));
146
+ return { rows: [{ id: 1 }], rowCount: 1 };
147
+ },
148
+ }, { pageSize: 10, onChange });
149
+ ctl.refresh();
150
+ ctl.dispose();
151
+ await new Promise((r) => setTimeout(r, 60));
152
+ expect(ctl.getState().rows).toEqual([]);
153
+ });
154
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Spreadsheet-style layout helpers — cell merging + per-cell borders.
3
+ *
4
+ * These are *layout-only* augmentations on top of `<SvGrid>`. They don't
5
+ * change the grid's data model, sorting, or filtering — they just decorate
6
+ * the rendered body cells. Bind the action to the wrapper around your
7
+ * SvGrid and pass merge / border specs; the helper observes DOM mutations
8
+ * and re-applies the layout whenever the grid re-renders.
9
+ *
10
+ * Limitations (worth knowing up front):
11
+ * - Merges + borders index into the CURRENTLY-DISPLAYED rows (after
12
+ * sort / filter), via the `data-svgrid-row` index the grid sets on
13
+ * every body cell. If your data sorts or filters, recompute the
14
+ * specs against the new display order.
15
+ * - Merges use real `colspan` / `rowspan` plus `display:none` on
16
+ * covered TDs. Column widths come from the inline styles SvGrid
17
+ * emits on each TD; we don't fight those.
18
+ * - For long row-merges across a virtualised window, only rows that
19
+ * are actually rendered get the rowspan applied. Disable
20
+ * virtualisation on grids that need a continuous merge.
21
+ */
22
+ /** A single edge of a cell border. */
23
+ export type BorderSpec = {
24
+ /** Thickness in pixels. Default 2. */
25
+ width?: number;
26
+ /** CSS border-style. Default 'solid'. */
27
+ style?: 'solid' | 'dashed' | 'dotted' | 'double';
28
+ /** CSS color. Falls back to currentColor (i.e. text color). */
29
+ color?: string;
30
+ };
31
+ /** A merge declaration. The cell at (rowIndex, columnId) is the ORIGIN;
32
+ * it spans `colspan` columns to the right + `rowspan` rows downward.
33
+ * Covered cells are hidden so the origin visually fills the region. */
34
+ export type MergeSpec = {
35
+ /** Display-row index — the same value the grid puts on
36
+ * `data-svgrid-row`. After sorting/filtering, recompute the spec
37
+ * against the new display order. */
38
+ rowIndex: number;
39
+ columnId: string;
40
+ /** Default 1. */
41
+ rowspan?: number;
42
+ /** Default 1. */
43
+ colspan?: number;
44
+ };
45
+ /** A column with declarative spanning callbacks, as accepted by
46
+ * `spansToMerges`. Matches the relevant slice of `ColumnDef`. */
47
+ export type SpanColumn<TData = Record<string, unknown>> = {
48
+ id: string;
49
+ field?: string;
50
+ colSpan?: (params: {
51
+ data: TData;
52
+ rowIndex: number;
53
+ columnId: string;
54
+ value: unknown;
55
+ }) => number;
56
+ rowSpan?: (params: {
57
+ data: TData;
58
+ rowIndex: number;
59
+ columnId: string;
60
+ value: unknown;
61
+ }) => number;
62
+ };
63
+ /**
64
+ * Turn declarative per-column `colSpan` / `rowSpan` callbacks into a
65
+ * `MergeSpec[]` you can hand to `spreadsheetLayout` - so value-driven,
66
+ * AG-Grid-style spanning runs on the SAME real colspan/rowspan merge engine
67
+ * instead of a second code path. Recompute after sort/filter (indexes are
68
+ * display-row indexes). A common pattern is "merge runs of equal values":
69
+ *
70
+ * { field: 'region', rowSpan: ({ data, rowIndex }) =>
71
+ * rows.filter((r, i) => i >= rowIndex && r.region === data.region &&
72
+ * (i === rowIndex || rows[i-1].region === data.region)).length }
73
+ */
74
+ export declare function spansToMerges<TData = Record<string, unknown>>(rows: ReadonlyArray<TData>, columns: ReadonlyArray<SpanColumn<TData>>, getValue?: (row: TData, columnId: string) => unknown): MergeSpec[];
75
+ /** Borders for one cell. Edges left unset render as the default
76
+ * cell border (i.e. no override). */
77
+ export type CellBorderSpec = {
78
+ rowIndex: number;
79
+ columnId: string;
80
+ top?: BorderSpec;
81
+ right?: BorderSpec;
82
+ bottom?: BorderSpec;
83
+ left?: BorderSpec;
84
+ };
85
+ /** What the Svelte action receives. Pass new values to update; pass
86
+ * `null` / empty arrays to clear. */
87
+ export type SpreadsheetActionOptions = {
88
+ merges?: ReadonlyArray<MergeSpec> | null;
89
+ borders?: ReadonlyArray<CellBorderSpec> | null;
90
+ /** Column id order the grid uses, in left-to-right order. Required
91
+ * to translate `colspan` into the right set of covered column ids.
92
+ * Pass `columns.map((c) => c.id)` from the consumer. */
93
+ columnOrder: ReadonlyArray<string>;
94
+ };
95
+ /** Svelte action. Attach to the element that hosts your `<SvGrid>` so
96
+ * the action can watch its DOM for re-renders.
97
+ *
98
+ * ```svelte
99
+ * <div use:spreadsheetLayout={{ merges, borders, columnOrder }}>
100
+ * <SvGrid {data} {columns} ... />
101
+ * </div>
102
+ * ```
103
+ *
104
+ * The action re-applies the layout whenever the grid's body mutates
105
+ * (new rows, column reorder, virtualization scroll) and whenever the
106
+ * options change. */
107
+ export declare function spreadsheetLayout(node: HTMLElement, opts: SpreadsheetActionOptions): {
108
+ update(next: SpreadsheetActionOptions): void;
109
+ destroy(): void;
110
+ };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Spreadsheet-style layout helpers — cell merging + per-cell borders.
3
+ *
4
+ * These are *layout-only* augmentations on top of `<SvGrid>`. They don't
5
+ * change the grid's data model, sorting, or filtering — they just decorate
6
+ * the rendered body cells. Bind the action to the wrapper around your
7
+ * SvGrid and pass merge / border specs; the helper observes DOM mutations
8
+ * and re-applies the layout whenever the grid re-renders.
9
+ *
10
+ * Limitations (worth knowing up front):
11
+ * - Merges + borders index into the CURRENTLY-DISPLAYED rows (after
12
+ * sort / filter), via the `data-svgrid-row` index the grid sets on
13
+ * every body cell. If your data sorts or filters, recompute the
14
+ * specs against the new display order.
15
+ * - Merges use real `colspan` / `rowspan` plus `display:none` on
16
+ * covered TDs. Column widths come from the inline styles SvGrid
17
+ * emits on each TD; we don't fight those.
18
+ * - For long row-merges across a virtualised window, only rows that
19
+ * are actually rendered get the rowspan applied. Disable
20
+ * virtualisation on grids that need a continuous merge.
21
+ */
22
+ /**
23
+ * Turn declarative per-column `colSpan` / `rowSpan` callbacks into a
24
+ * `MergeSpec[]` you can hand to `spreadsheetLayout` - so value-driven,
25
+ * AG-Grid-style spanning runs on the SAME real colspan/rowspan merge engine
26
+ * instead of a second code path. Recompute after sort/filter (indexes are
27
+ * display-row indexes). A common pattern is "merge runs of equal values":
28
+ *
29
+ * { field: 'region', rowSpan: ({ data, rowIndex }) =>
30
+ * rows.filter((r, i) => i >= rowIndex && r.region === data.region &&
31
+ * (i === rowIndex || rows[i-1].region === data.region)).length }
32
+ */
33
+ export function spansToMerges(rows, columns, getValue) {
34
+ const merges = [];
35
+ const covered = new Set();
36
+ for (let r = 0; r < rows.length; r += 1) {
37
+ const row = rows[r];
38
+ for (let ci = 0; ci < columns.length; ci += 1) {
39
+ const col = columns[ci];
40
+ if (!col.colSpan && !col.rowSpan)
41
+ continue;
42
+ const key = `${r}:${ci}`;
43
+ if (covered.has(key))
44
+ continue;
45
+ const value = getValue
46
+ ? getValue(row, col.id)
47
+ : row[col.field ?? col.id];
48
+ const params = { data: row, rowIndex: r, columnId: col.id, value };
49
+ const cs = Math.max(1, Math.floor(col.colSpan?.(params) ?? 1));
50
+ const rs = Math.max(1, Math.floor(col.rowSpan?.(params) ?? 1));
51
+ if (cs <= 1 && rs <= 1)
52
+ continue;
53
+ merges.push({
54
+ rowIndex: r,
55
+ columnId: col.id,
56
+ colspan: cs > 1 ? cs : undefined,
57
+ rowspan: rs > 1 ? rs : undefined,
58
+ });
59
+ for (let dr = 0; dr < rs; dr += 1) {
60
+ for (let dc = 0; dc < cs; dc += 1) {
61
+ if (dr === 0 && dc === 0)
62
+ continue;
63
+ covered.add(`${r + dr}:${ci + dc}`);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return merges;
69
+ }
70
+ const BORDER_KEYS = ['top', 'right', 'bottom', 'left'];
71
+ const MARK = 'data-svgrid-sheet';
72
+ function borderCss(spec) {
73
+ if (!spec)
74
+ return null;
75
+ const w = spec.width ?? 2;
76
+ const s = spec.style ?? 'solid';
77
+ const c = spec.color ?? 'currentColor';
78
+ return `${w}px ${s} ${c}`;
79
+ }
80
+ /** Look up a body cell by its display-row + column id. Falls back to
81
+ * null if the cell isn't in the current render window (virtualisation,
82
+ * scrolled out, or filtered away). */
83
+ function findCell(root, rowIndex, columnId) {
84
+ return root.querySelector(`td[data-svgrid-row="${rowIndex}"][data-col-id="${CSS.escape(columnId)}"]`);
85
+ }
86
+ const OVERLAY_CLASS = 'sv-cell-border-overlay';
87
+ /** Apply / re-apply layout decorations. Called once on mount, again on
88
+ * every relevant DOM mutation inside the grid, and whenever the action
89
+ * options update. */
90
+ function apply(root, opts) {
91
+ // ---- 1. Clean up anything the LAST run added -----------------------
92
+ for (const td of root.querySelectorAll(`td[${MARK}]`)) {
93
+ td.removeAttribute('colspan');
94
+ td.removeAttribute('rowspan');
95
+ if (td.style.display === 'none')
96
+ td.style.display = '';
97
+ // Remove the overlay child we previously injected for borders.
98
+ const overlay = td.querySelector(`:scope > .${OVERLAY_CLASS}`);
99
+ if (overlay)
100
+ overlay.remove();
101
+ td.classList.remove('sv-merge-edge-right', 'sv-merge-edge-bottom', 'sv-merge-in-range');
102
+ td.removeAttribute(MARK);
103
+ }
104
+ const colOrder = opts.columnOrder;
105
+ const colIndexOf = new Map();
106
+ for (let i = 0; i < colOrder.length; i += 1)
107
+ colIndexOf.set(colOrder[i], i);
108
+ // ---- 2. Merges -----------------------------------------------------
109
+ for (const m of opts.merges ?? []) {
110
+ const startCol = colIndexOf.get(m.columnId);
111
+ if (startCol === undefined)
112
+ continue;
113
+ const rs = Math.max(1, m.rowspan ?? 1);
114
+ const cs = Math.max(1, m.colspan ?? 1);
115
+ const origin = findCell(root, m.rowIndex, m.columnId);
116
+ if (!origin)
117
+ continue;
118
+ if (cs > 1)
119
+ origin.setAttribute('colspan', String(cs));
120
+ if (rs > 1)
121
+ origin.setAttribute('rowspan', String(rs));
122
+ origin.setAttribute(MARK, '');
123
+ for (let dr = 0; dr < rs; dr += 1) {
124
+ for (let dc = 0; dc < cs; dc += 1) {
125
+ if (dr === 0 && dc === 0)
126
+ continue;
127
+ const colId = colOrder[startCol + dc];
128
+ if (!colId)
129
+ continue;
130
+ const td = findCell(root, m.rowIndex + dr, colId);
131
+ if (!td)
132
+ continue;
133
+ td.style.display = 'none';
134
+ td.setAttribute(MARK, '');
135
+ }
136
+ }
137
+ // Selection-edge inheritance:
138
+ // The grid sets `data-range-*` per cell. The merge's RIGHTMOST /
139
+ // BOTTOMMOST covered cells are `display:none`, so the borders
140
+ // they would draw never render. Mirror those edge flags onto the
141
+ // origin via dedicated CSS classes (so we can toggle them
142
+ // independently of Svelte's own data-range-* updates without a
143
+ // ping-pong loop).
144
+ const lastColId = colOrder[startCol + cs - 1];
145
+ const rightCell = lastColId ? findCell(root, m.rowIndex, lastColId) : null;
146
+ const bottomCell = findCell(root, m.rowIndex + rs - 1, m.columnId);
147
+ const farCell = lastColId ? findCell(root, m.rowIndex + rs - 1, lastColId) : null;
148
+ const wantRight = rightCell?.getAttribute('data-range-right') === 'true' ||
149
+ farCell?.getAttribute('data-range-right') === 'true';
150
+ const wantBottom = bottomCell?.getAttribute('data-range-bottom') === 'true' ||
151
+ farCell?.getAttribute('data-range-bottom') === 'true';
152
+ const inRange = rightCell?.getAttribute('data-selected-range') === 'true' ||
153
+ bottomCell?.getAttribute('data-selected-range') === 'true' ||
154
+ farCell?.getAttribute('data-selected-range') === 'true' ||
155
+ origin.getAttribute('data-selected-range') === 'true';
156
+ origin.classList.toggle('sv-merge-edge-right', wantRight);
157
+ origin.classList.toggle('sv-merge-edge-bottom', wantBottom);
158
+ origin.classList.toggle('sv-merge-in-range', inRange);
159
+ }
160
+ // ---- 3. Borders ---------------------------------------------------
161
+ // Use an absolute-positioned overlay div so each edge renders
162
+ // independently of the grid's own border-collapse rules. Adjacent
163
+ // TDs no longer "eat" the right / bottom edges of a bordered cell.
164
+ for (const b of opts.borders ?? []) {
165
+ const td = findCell(root, b.rowIndex, b.columnId);
166
+ if (!td)
167
+ continue;
168
+ let any = false;
169
+ const overlay = document.createElement('div');
170
+ overlay.className = OVERLAY_CLASS;
171
+ overlay.style.cssText =
172
+ 'position:absolute;inset:0;pointer-events:none;box-sizing:border-box;z-index:1;';
173
+ for (const e of BORDER_KEYS) {
174
+ const css = borderCss(b[e]);
175
+ if (!css)
176
+ continue;
177
+ overlay.style.setProperty(`border-${e}`, css);
178
+ any = true;
179
+ }
180
+ if (!any)
181
+ continue;
182
+ // Ensure the TD is a positioning context for the overlay.
183
+ if (getComputedStyle(td).position === 'static')
184
+ td.style.position = 'relative';
185
+ td.appendChild(overlay);
186
+ td.setAttribute(MARK, '');
187
+ }
188
+ }
189
+ /** Svelte action. Attach to the element that hosts your `<SvGrid>` so
190
+ * the action can watch its DOM for re-renders.
191
+ *
192
+ * ```svelte
193
+ * <div use:spreadsheetLayout={{ merges, borders, columnOrder }}>
194
+ * <SvGrid {data} {columns} ... />
195
+ * </div>
196
+ * ```
197
+ *
198
+ * The action re-applies the layout whenever the grid's body mutates
199
+ * (new rows, column reorder, virtualization scroll) and whenever the
200
+ * options change. */
201
+ export function spreadsheetLayout(node, opts) {
202
+ let current = opts;
203
+ let frame = 0;
204
+ // The MutationObserver fires for EVERY DOM change inside the grid -
205
+ // batch them into one rAF so a big virtualization scroll doesn't run
206
+ // apply() dozens of times in a tick.
207
+ function schedule() {
208
+ if (frame)
209
+ return;
210
+ frame = requestAnimationFrame(() => {
211
+ frame = 0;
212
+ apply(node, current);
213
+ });
214
+ }
215
+ const observer = new MutationObserver(schedule);
216
+ observer.observe(node, {
217
+ childList: true,
218
+ subtree: true,
219
+ attributes: true,
220
+ attributeFilter: [
221
+ 'data-svgrid-row', 'data-col-id', 'style',
222
+ // Watch selection-range attrs so we can transfer them from
223
+ // hidden covered cells to merge origins.
224
+ 'data-range-top', 'data-range-bottom', 'data-range-left',
225
+ 'data-range-right', 'data-selected-range',
226
+ ],
227
+ });
228
+ // First-paint pass: schedule the same way so we don't run before the
229
+ // grid's initial render landed.
230
+ schedule();
231
+ return {
232
+ update(next) {
233
+ current = next;
234
+ schedule();
235
+ },
236
+ destroy() {
237
+ observer.disconnect();
238
+ if (frame)
239
+ cancelAnimationFrame(frame);
240
+ },
241
+ };
242
+ }
@@ -0,0 +1 @@
1
+ export {};