@svgrid/grid 1.1.1 → 1.2.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 (188) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/SvGrid.controller.svelte.js +17 -6
  3. package/dist/SvGrid.css +2012 -2012
  4. package/dist/SvGrid.svelte +2571 -2346
  5. package/dist/SvGridDropdown.svelte +7 -3
  6. package/dist/build-api.js +2 -2
  7. package/dist/cdn/svgrid.js +13504 -0
  8. package/dist/cdn/svgrid.svelte-external.js +11188 -0
  9. package/dist/cell-values.d.ts +1 -1
  10. package/dist/cell-values.js +7 -7
  11. package/dist/column-types.js +1 -1
  12. package/dist/core.d.ts +1 -1
  13. package/dist/core.js +2 -2
  14. package/dist/summaries.js +4 -4
  15. package/package.json +12 -2
  16. package/src/FlexRender.svelte +96 -96
  17. package/src/SvGrid.controller.svelte.ts +2363 -2352
  18. package/src/SvGrid.css +2012 -2012
  19. package/src/SvGrid.svelte +2571 -2346
  20. package/src/SvGrid.types.ts +537 -537
  21. package/src/SvGridDropdown.svelte +7 -3
  22. package/src/a11y.contract.test.ts +49 -49
  23. package/src/a11y.test.ts +59 -59
  24. package/src/a11y.ts +59 -59
  25. package/src/build-api.ts +683 -683
  26. package/src/cell-formatting.ts +169 -169
  27. package/src/cell-values.ts +4 -4
  28. package/src/column-types.ts +1 -1
  29. package/src/core.performance.test.ts +30 -30
  30. package/src/core.ts +1077 -1077
  31. package/src/createGrid.svelte.ts +42 -42
  32. package/src/createGrid.test.ts +10 -10
  33. package/src/createGridState.svelte.ts +17 -17
  34. package/src/editing.ts +669 -669
  35. package/src/flex-render.ts +3 -3
  36. package/src/index.ts +208 -208
  37. package/src/keyboard.test.ts +59 -59
  38. package/src/keyboard.ts +97 -97
  39. package/src/merge-objects.ts +48 -48
  40. package/src/render-component.ts +28 -28
  41. package/src/spreadsheet.test.ts +489 -489
  42. package/src/spreadsheet.ts +304 -304
  43. package/src/static-functions.ts +11 -11
  44. package/src/subscribe.ts +38 -38
  45. package/src/summaries.ts +4 -4
  46. package/src/svgrid-wrapper.types.ts +412 -412
  47. package/src/svgrid.features.test.ts +157 -157
  48. package/src/svgrid.wrapper.test.ts +40 -40
  49. package/src/virtualization/column-virtualizer.test.ts +27 -27
  50. package/src/virtualization/column-virtualizer.ts +30 -30
  51. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  52. package/src/virtualization/types.ts +30 -30
  53. package/src/virtualization/virtualizer.test.ts +47 -47
  54. package/src/virtualization/virtualizer.ts +296 -296
  55. package/dist/SvGrid.helpers.test.d.ts +0 -1
  56. package/dist/SvGrid.helpers.test.js +0 -298
  57. package/dist/SvGridChart.test.d.ts +0 -1
  58. package/dist/SvGridChart.test.js +0 -62
  59. package/dist/a11y.contract.test.d.ts +0 -1
  60. package/dist/a11y.contract.test.js +0 -39
  61. package/dist/a11y.test.d.ts +0 -1
  62. package/dist/a11y.test.js +0 -44
  63. package/dist/aligned-grids.test.d.ts +0 -1
  64. package/dist/aligned-grids.test.js +0 -75
  65. package/dist/build-api.coverage.test.d.ts +0 -20
  66. package/dist/build-api.coverage.test.js +0 -505
  67. package/dist/cell-formatting.test.d.ts +0 -1
  68. package/dist/cell-formatting.test.js +0 -234
  69. package/dist/cell-render.test.d.ts +0 -1
  70. package/dist/cell-render.test.js +0 -338
  71. package/dist/chart-export.test.d.ts +0 -1
  72. package/dist/chart-export.test.js +0 -302
  73. package/dist/chart.coverage.test.d.ts +0 -1
  74. package/dist/chart.coverage.test.js +0 -748
  75. package/dist/chart.test.d.ts +0 -1
  76. package/dist/chart.test.js +0 -289
  77. package/dist/clipboard.test.d.ts +0 -1
  78. package/dist/clipboard.test.js +0 -700
  79. package/dist/collaboration.coverage.test.d.ts +0 -1
  80. package/dist/collaboration.coverage.test.js +0 -200
  81. package/dist/collaboration.test.d.ts +0 -1
  82. package/dist/collaboration.test.js +0 -65
  83. package/dist/column-groups.test.d.ts +0 -1
  84. package/dist/column-groups.test.js +0 -56
  85. package/dist/column-types.test.d.ts +0 -1
  86. package/dist/column-types.test.js +0 -62
  87. package/dist/columns.test.d.ts +0 -1
  88. package/dist/columns.test.js +0 -625
  89. package/dist/conditional-formatting.test.d.ts +0 -1
  90. package/dist/conditional-formatting.test.js +0 -87
  91. package/dist/core.coverage.test.d.ts +0 -1
  92. package/dist/core.coverage.test.js +0 -186
  93. package/dist/core.performance.test.d.ts +0 -1
  94. package/dist/core.performance.test.js +0 -29
  95. package/dist/createGrid.test.d.ts +0 -1
  96. package/dist/createGrid.test.js +0 -9
  97. package/dist/editing.test.d.ts +0 -1
  98. package/dist/editing.test.js +0 -732
  99. package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
  100. package/dist/editors/cell-editors.coverage.test.js +0 -139
  101. package/dist/editors/cell-editors.test.d.ts +0 -1
  102. package/dist/editors/cell-editors.test.js +0 -75
  103. package/dist/facet-buckets.test.d.ts +0 -1
  104. package/dist/facet-buckets.test.js +0 -296
  105. package/dist/fill-patterns.test.d.ts +0 -1
  106. package/dist/fill-patterns.test.js +0 -81
  107. package/dist/filter-operators.test.d.ts +0 -1
  108. package/dist/filter-operators.test.js +0 -135
  109. package/dist/filtering/excel-filters.test.d.ts +0 -1
  110. package/dist/filtering/excel-filters.test.js +0 -116
  111. package/dist/filtering/locale-filter.test.d.ts +0 -13
  112. package/dist/filtering/locale-filter.test.js +0 -189
  113. package/dist/flex-render.test.d.ts +0 -1
  114. package/dist/flex-render.test.js +0 -142
  115. package/dist/group-aggregate.test.d.ts +0 -1
  116. package/dist/group-aggregate.test.js +0 -32
  117. package/dist/hyperformula-adapter.test.d.ts +0 -1
  118. package/dist/hyperformula-adapter.test.js +0 -205
  119. package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
  120. package/dist/keyboard-handlers.coverage.test.js +0 -495
  121. package/dist/keyboard.test.d.ts +0 -1
  122. package/dist/keyboard.test.js +0 -27
  123. package/dist/menus.test.d.ts +0 -1
  124. package/dist/menus.test.js +0 -560
  125. package/dist/named-views.coverage.test.d.ts +0 -1
  126. package/dist/named-views.coverage.test.js +0 -180
  127. package/dist/named-views.test.d.ts +0 -1
  128. package/dist/named-views.test.js +0 -57
  129. package/dist/new-features.test.d.ts +0 -1
  130. package/dist/new-features.test.js +0 -183
  131. package/dist/render-component.test.d.ts +0 -1
  132. package/dist/render-component.test.js +0 -41
  133. package/dist/row-drag.test.d.ts +0 -1
  134. package/dist/row-drag.test.js +0 -142
  135. package/dist/row-resize.test.d.ts +0 -1
  136. package/dist/row-resize.test.js +0 -329
  137. package/dist/scroll-sync.test.d.ts +0 -1
  138. package/dist/scroll-sync.test.js +0 -290
  139. package/dist/selection.multi-range.test.d.ts +0 -1
  140. package/dist/selection.multi-range.test.js +0 -55
  141. package/dist/selection.test.d.ts +0 -1
  142. package/dist/selection.test.js +0 -647
  143. package/dist/server-data-source.coverage.test.d.ts +0 -1
  144. package/dist/server-data-source.coverage.test.js +0 -154
  145. package/dist/server-data-source.test.d.ts +0 -1
  146. package/dist/server-data-source.test.js +0 -81
  147. package/dist/sparkline.test.d.ts +0 -1
  148. package/dist/sparkline.test.js +0 -52
  149. package/dist/spreadsheet.test.d.ts +0 -1
  150. package/dist/spreadsheet.test.js +0 -446
  151. package/dist/subscribe.test.d.ts +0 -1
  152. package/dist/subscribe.test.js +0 -93
  153. package/dist/svgrid.api-extensions.test.d.ts +0 -14
  154. package/dist/svgrid.api-extensions.test.js +0 -334
  155. package/dist/svgrid.api.test.d.ts +0 -15
  156. package/dist/svgrid.api.test.js +0 -169
  157. package/dist/svgrid.behavior.test.d.ts +0 -15
  158. package/dist/svgrid.behavior.test.js +0 -625
  159. package/dist/svgrid.column-reorder.test.d.ts +0 -15
  160. package/dist/svgrid.column-reorder.test.js +0 -224
  161. package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
  162. package/dist/svgrid.comments-autocomplete.test.js +0 -96
  163. package/dist/svgrid.context-menu.test.d.ts +0 -1
  164. package/dist/svgrid.context-menu.test.js +0 -102
  165. package/dist/svgrid.features.test.d.ts +0 -1
  166. package/dist/svgrid.features.test.js +0 -129
  167. package/dist/svgrid.interaction.test.d.ts +0 -9
  168. package/dist/svgrid.interaction.test.js +0 -349
  169. package/dist/svgrid.locale-filtering.test.d.ts +0 -15
  170. package/dist/svgrid.locale-filtering.test.js +0 -261
  171. package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
  172. package/dist/svgrid.new-features.wrapper.test.js +0 -194
  173. package/dist/svgrid.row-pinning.test.d.ts +0 -14
  174. package/dist/svgrid.row-pinning.test.js +0 -250
  175. package/dist/svgrid.set-filter.test.d.ts +0 -17
  176. package/dist/svgrid.set-filter.test.js +0 -268
  177. package/dist/svgrid.wrapper.test.d.ts +0 -1
  178. package/dist/svgrid.wrapper.test.js +0 -59
  179. package/dist/test-setup.d.ts +0 -1
  180. package/dist/test-setup.js +0 -35
  181. package/dist/transaction.test.d.ts +0 -1
  182. package/dist/transaction.test.js +0 -89
  183. package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
  184. package/dist/virtualization/column-virtualizer.test.js +0 -25
  185. package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
  186. package/dist/virtualization/scroll-scaling.test.js +0 -127
  187. package/dist/virtualization/virtualizer.test.d.ts +0 -1
  188. package/dist/virtualization/virtualizer.test.js +0 -41
@@ -1,700 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { createClipboard } from './clipboard';
3
- function makeCtx(opts = {}) {
4
- const columns = opts.columns ?? [
5
- { id: 'a', field: 'a', editable: true, editorType: 'text' },
6
- { id: 'b', field: 'b', editable: true, editorType: 'text' },
7
- ];
8
- const data = (opts.data ?? [
9
- { a: 'a0', b: 'b0' },
10
- { a: 'a1', b: 'b1' },
11
- ]).map((d) => ({ ...d }));
12
- const allColumns = columns.map((c) => ({
13
- id: c.id,
14
- columnDef: {
15
- field: c.field,
16
- editable: c.editable,
17
- editorType: c.editorType,
18
- },
19
- }));
20
- // allRows mirror internalData by index; row.id === index as a string.
21
- const buildRows = () => data.map((row, index) => ({
22
- id: String(index),
23
- original: row,
24
- getCanExpand: () => opts.groupRows?.has(index) ?? false,
25
- getCellValueByColumnId: (colId) => row[colId],
26
- }));
27
- let activeCell = null;
28
- const ctx = {
29
- internalData: data,
30
- allColumns,
31
- get allRows() {
32
- // Recompute from current internalData so writeCellRaw's
33
- // `r.original === row` lookup stays consistent with internalData.
34
- return ctx._rows;
35
- },
36
- _rows: buildRows(),
37
- selectionRange: { anchor: null, focus: null },
38
- selectionRanges: [],
39
- // Mirrors the real controller's getSelectionRects: committed ranges + active.
40
- getSelectionRects() {
41
- const rects = [];
42
- const norm = (r) => r?.anchor && r?.focus
43
- ? {
44
- minRow: Math.min(r.anchor.rowIndex, r.focus.rowIndex),
45
- maxRow: Math.max(r.anchor.rowIndex, r.focus.rowIndex),
46
- minCol: Math.min(r.anchor.colIndex, r.focus.colIndex),
47
- maxCol: Math.max(r.anchor.colIndex, r.focus.colIndex),
48
- }
49
- : null;
50
- for (const r of ctx.selectionRanges) {
51
- const n = norm(r);
52
- if (n)
53
- rects.push(n);
54
- }
55
- const active = norm(ctx.selectionRange);
56
- if (active)
57
- rects.push(active);
58
- return rects;
59
- },
60
- fillDrag: null,
61
- editedCellValues: {},
62
- userHasActivatedCell: false,
63
- gridRootEl: null,
64
- props: {},
65
- findColumnById: (id) => allColumns.find((c) => c.id === id),
66
- isCellEditableAt: opts.editableAt ?? (() => true),
67
- getCellDisplayValue: vi.fn((_rowId, _colId, base) => base),
68
- grid: {
69
- getState: () => ({ activeCell }),
70
- store: { setState: vi.fn() },
71
- },
72
- _setActiveCell: (c) => {
73
- activeCell = c;
74
- },
75
- };
76
- // Keep _rows pointing at the live internalData objects whenever it is
77
- // reassigned by writeCellRaw / clearSelectedCells.
78
- let backing = data;
79
- Object.defineProperty(ctx, 'internalData', {
80
- get: () => backing,
81
- set: (next) => {
82
- backing = next;
83
- ctx._rows = next.map((row, index) => ({
84
- id: String(index),
85
- original: row,
86
- getCanExpand: () => opts.groupRows?.has(index) ?? false,
87
- getCellValueByColumnId: (colId) => row[colId],
88
- }));
89
- },
90
- });
91
- return ctx;
92
- }
93
- describe('readCellRaw', () => {
94
- it('reads the underlying field value', () => {
95
- const ctx = makeCtx();
96
- const cb = createClipboard(ctx);
97
- expect(cb.readCellRaw(0, 'a')).toBe('a0');
98
- expect(cb.readCellRaw(1, 'b')).toBe('b1');
99
- });
100
- it('returns undefined when the row is missing', () => {
101
- const ctx = makeCtx();
102
- const cb = createClipboard(ctx);
103
- expect(cb.readCellRaw(99, 'a')).toBeUndefined();
104
- });
105
- it('returns undefined when the column has no field', () => {
106
- const ctx = makeCtx({
107
- columns: [{ id: 'noField' }],
108
- data: [{ a: 1 }],
109
- });
110
- const cb = createClipboard(ctx);
111
- expect(cb.readCellRaw(0, 'noField')).toBeUndefined();
112
- });
113
- });
114
- describe('writeCellRaw', () => {
115
- it('writes a new value and fires onCellValueChange', () => {
116
- const onCellValueChange = vi.fn();
117
- const ctx = makeCtx();
118
- ctx.props.onCellValueChange = onCellValueChange;
119
- const cb = createClipboard(ctx);
120
- cb.writeCellRaw(0, 'a', 'X');
121
- expect(ctx.internalData[0].a).toBe('X');
122
- // Original array object was replaced (immutable update).
123
- expect(onCellValueChange).toHaveBeenCalledWith(expect.objectContaining({
124
- rowIndex: 0,
125
- columnId: 'a',
126
- oldValue: 'a0',
127
- newValue: 'X',
128
- }));
129
- });
130
- it('records the edit in editedCellValues keyed by rowId:colId', () => {
131
- const ctx = makeCtx();
132
- const cb = createClipboard(ctx);
133
- cb.writeCellRaw(1, 'b', 'Y');
134
- expect(ctx.editedCellValues['1:b']).toBe('Y');
135
- });
136
- it('is a no-op when the value is unchanged', () => {
137
- const onCellValueChange = vi.fn();
138
- const ctx = makeCtx();
139
- ctx.props.onCellValueChange = onCellValueChange;
140
- const cb = createClipboard(ctx);
141
- cb.writeCellRaw(0, 'a', 'a0');
142
- expect(onCellValueChange).not.toHaveBeenCalled();
143
- });
144
- it('does nothing when the row is missing', () => {
145
- const ctx = makeCtx();
146
- const cb = createClipboard(ctx);
147
- cb.writeCellRaw(99, 'a', 'Z');
148
- expect(ctx.internalData).toHaveLength(2);
149
- });
150
- it('does nothing when the column has no field', () => {
151
- const ctx = makeCtx({
152
- columns: [{ id: 'noField' }],
153
- data: [{ a: 1 }],
154
- });
155
- const cb = createClipboard(ctx);
156
- cb.writeCellRaw(0, 'noField', 5);
157
- expect(ctx.internalData[0]).toEqual({ a: 1 });
158
- });
159
- });
160
- describe('clearSelectedCellValues', () => {
161
- it('nulls every editable cell in the selection range', () => {
162
- const ctx = makeCtx();
163
- ctx.selectionRange = {
164
- anchor: { rowIndex: 0, colIndex: 0 },
165
- focus: { rowIndex: 1, colIndex: 1 },
166
- };
167
- const cb = createClipboard(ctx);
168
- cb.clearSelectedCellValues();
169
- expect(ctx.internalData[0]).toEqual({ a: null, b: null });
170
- expect(ctx.internalData[1]).toEqual({ a: null, b: null });
171
- });
172
- it('skips non-editable cells', () => {
173
- const ctx = makeCtx({ editableAt: (r, c) => !(r === 0 && c === 0) });
174
- ctx.selectionRange = {
175
- anchor: { rowIndex: 0, colIndex: 0 },
176
- focus: { rowIndex: 0, colIndex: 1 },
177
- };
178
- const cb = createClipboard(ctx);
179
- cb.clearSelectedCellValues();
180
- expect(ctx.internalData[0].a).toBe('a0'); // untouched
181
- expect(ctx.internalData[0].b).toBeNull();
182
- });
183
- it('clears just the active cell when no range is selected', () => {
184
- const ctx = makeCtx();
185
- ctx.userHasActivatedCell = true;
186
- ctx._setActiveCell({ rowIndex: 1, colIndex: 1 });
187
- const cb = createClipboard(ctx);
188
- cb.clearSelectedCellValues();
189
- expect(ctx.internalData[1].b).toBeNull();
190
- expect(ctx.internalData[0].a).toBe('a0');
191
- });
192
- it('does nothing for the active cell when not activated', () => {
193
- const ctx = makeCtx();
194
- ctx.userHasActivatedCell = false;
195
- ctx._setActiveCell({ rowIndex: 1, colIndex: 1 });
196
- const cb = createClipboard(ctx);
197
- cb.clearSelectedCellValues();
198
- expect(ctx.internalData[1].b).toBe('b1');
199
- });
200
- });
201
- describe('startFillDrag', () => {
202
- function fakeEvent() {
203
- return {
204
- stopPropagation: vi.fn(),
205
- preventDefault: vi.fn(),
206
- };
207
- }
208
- it('seeds the drag from the selection range', () => {
209
- const ctx = makeCtx();
210
- ctx.selectionRange = {
211
- anchor: { rowIndex: 0, colIndex: 0 },
212
- focus: { rowIndex: 1, colIndex: 1 },
213
- };
214
- const cb = createClipboard(ctx);
215
- cb.startFillDrag(fakeEvent(), 2, 1);
216
- expect(ctx.fillDrag).toEqual({
217
- sourceMinRow: 0,
218
- sourceMaxRow: 1,
219
- sourceMinCol: 0,
220
- sourceMaxCol: 1,
221
- targetRow: 2,
222
- targetCol: 1,
223
- });
224
- });
225
- it('seeds a 1x1 drag from the active cell when no range', () => {
226
- const ctx = makeCtx();
227
- const cb = createClipboard(ctx);
228
- const event = fakeEvent();
229
- cb.startFillDrag(event, 3, 4);
230
- expect(ctx.fillDrag).toEqual({
231
- sourceMinRow: 3,
232
- sourceMaxRow: 3,
233
- sourceMinCol: 4,
234
- sourceMaxCol: 4,
235
- targetRow: 3,
236
- targetCol: 4,
237
- });
238
- expect(event.stopPropagation).toHaveBeenCalled();
239
- expect(event.preventDefault).toHaveBeenCalled();
240
- });
241
- });
242
- describe('onFillPointerMove', () => {
243
- it('does nothing when there is no active fillDrag', () => {
244
- const ctx = makeCtx();
245
- const cb = createClipboard(ctx);
246
- cb.onFillPointerMove({ clientX: 0, clientY: 0 });
247
- expect(ctx.fillDrag).toBeNull();
248
- });
249
- it('updates the target when a cell is found under the pointer', () => {
250
- const ctx = makeCtx();
251
- ctx.fillDrag = {
252
- sourceMinRow: 0,
253
- sourceMaxRow: 0,
254
- sourceMinCol: 0,
255
- sourceMaxCol: 0,
256
- targetRow: 0,
257
- targetCol: 0,
258
- };
259
- const cb = createClipboard(ctx);
260
- const td = document.createElement('td');
261
- td.setAttribute('data-svgrid-row', '2');
262
- td.setAttribute('data-svgrid-col', '1');
263
- document.elementFromPoint = () => td;
264
- cb.onFillPointerMove({ clientX: 5, clientY: 5 });
265
- expect(ctx.fillDrag.targetRow).toBe(2);
266
- expect(ctx.fillDrag.targetCol).toBe(1);
267
- });
268
- it('does not re-set the drag when hovering the same target', () => {
269
- const ctx = makeCtx();
270
- const original = {
271
- sourceMinRow: 0,
272
- sourceMaxRow: 0,
273
- sourceMinCol: 0,
274
- sourceMaxCol: 0,
275
- targetRow: 1,
276
- targetCol: 1,
277
- };
278
- ctx.fillDrag = original;
279
- const cb = createClipboard(ctx);
280
- const td = document.createElement('td');
281
- td.setAttribute('data-svgrid-row', '1');
282
- td.setAttribute('data-svgrid-col', '1');
283
- document.elementFromPoint = () => td;
284
- cb.onFillPointerMove({ clientX: 5, clientY: 5 });
285
- expect(ctx.fillDrag).toBe(original); // unchanged reference
286
- });
287
- it('ignores a pointer that is not over a grid cell', () => {
288
- const ctx = makeCtx();
289
- const original = {
290
- sourceMinRow: 0,
291
- sourceMaxRow: 0,
292
- sourceMinCol: 0,
293
- sourceMaxCol: 0,
294
- targetRow: 0,
295
- targetCol: 0,
296
- };
297
- ctx.fillDrag = original;
298
- const cb = createClipboard(ctx);
299
- document.elementFromPoint = () => document.createElement('div');
300
- cb.onFillPointerMove({ clientX: 5, clientY: 5 });
301
- expect(ctx.fillDrag).toBe(original);
302
- });
303
- });
304
- describe('onFillPointerUp / applyFillPattern', () => {
305
- it('does nothing when there is no fillDrag', () => {
306
- const ctx = makeCtx();
307
- const cb = createClipboard(ctx);
308
- cb.onFillPointerUp();
309
- expect(ctx.selectionRange).toEqual({ anchor: null, focus: null });
310
- });
311
- it('fills downward from a numeric source pattern', () => {
312
- const ctx = makeCtx({
313
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
314
- data: [{ a: 1 }, { a: 2 }, { a: 0 }, { a: 0 }],
315
- });
316
- ctx.fillDrag = {
317
- sourceMinRow: 0,
318
- sourceMaxRow: 1,
319
- sourceMinCol: 0,
320
- sourceMaxCol: 0,
321
- targetRow: 3,
322
- targetCol: 0,
323
- };
324
- const cb = createClipboard(ctx);
325
- cb.onFillPointerUp();
326
- // 1,2 -> linear pattern continues 3,4
327
- expect(ctx.internalData[2].a).toBe(3);
328
- expect(ctx.internalData[3].a).toBe(4);
329
- expect(ctx.fillDrag).toBeNull();
330
- expect(ctx.selectionRange.focus).toEqual({ rowIndex: 3, colIndex: 0 });
331
- });
332
- it('fills upward, reverse-extrapolating the pattern', () => {
333
- const ctx = makeCtx({
334
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
335
- data: [{ a: 0 }, { a: 0 }, { a: 10 }, { a: 20 }],
336
- });
337
- ctx.fillDrag = {
338
- sourceMinRow: 2,
339
- sourceMaxRow: 3,
340
- sourceMinCol: 0,
341
- sourceMaxCol: 0,
342
- targetRow: 0,
343
- targetCol: 0,
344
- };
345
- const cb = createClipboard(ctx);
346
- cb.applyFillPattern();
347
- // reversed source 20,10 -> continues 0,-10 going up; row1=0, row0=-10
348
- expect(ctx.internalData[1].a).toBe(0);
349
- expect(ctx.internalData[0].a).toBe(-10);
350
- });
351
- it('skips columns that are not editable', () => {
352
- const ctx = makeCtx({
353
- columns: [{ id: 'a', field: 'a', editable: false, editorType: 'number' }],
354
- data: [{ a: 1 }, { a: 2 }, { a: 0 }],
355
- });
356
- ctx.fillDrag = {
357
- sourceMinRow: 0,
358
- sourceMaxRow: 1,
359
- sourceMinCol: 0,
360
- sourceMaxCol: 0,
361
- targetRow: 2,
362
- targetCol: 0,
363
- };
364
- const cb = createClipboard(ctx);
365
- cb.applyFillPattern();
366
- expect(ctx.internalData[2].a).toBe(0); // untouched
367
- });
368
- it('fills horizontally to the right', () => {
369
- const ctx = makeCtx({
370
- columns: [
371
- { id: 'a', field: 'a', editable: true, editorType: 'number' },
372
- { id: 'b', field: 'b', editable: true, editorType: 'number' },
373
- { id: 'c', field: 'c', editable: true, editorType: 'number' },
374
- ],
375
- data: [{ a: 2, b: 4, c: 0 }],
376
- });
377
- ctx.fillDrag = {
378
- sourceMinRow: 0,
379
- sourceMaxRow: 0,
380
- sourceMinCol: 0,
381
- sourceMaxCol: 1,
382
- targetRow: 0,
383
- targetCol: 2,
384
- };
385
- const cb = createClipboard(ctx);
386
- cb.applyFillPattern();
387
- // 2,4 -> next is 6
388
- expect(ctx.internalData[0].c).toBe(6);
389
- });
390
- it('fills horizontally to the left', () => {
391
- const ctx = makeCtx({
392
- columns: [
393
- { id: 'a', field: 'a', editable: true, editorType: 'number' },
394
- { id: 'b', field: 'b', editable: true, editorType: 'number' },
395
- { id: 'c', field: 'c', editable: true, editorType: 'number' },
396
- ],
397
- data: [{ a: 0, b: 10, c: 20 }],
398
- });
399
- ctx.fillDrag = {
400
- sourceMinRow: 0,
401
- sourceMaxRow: 0,
402
- sourceMinCol: 1,
403
- sourceMaxCol: 2,
404
- targetRow: 0,
405
- targetCol: 0,
406
- };
407
- const cb = createClipboard(ctx);
408
- cb.applyFillPattern();
409
- // reversed 20,10 -> next going left is 0
410
- expect(ctx.internalData[0].a).toBe(0);
411
- });
412
- });
413
- describe('toggleBooleanCell', () => {
414
- it('flips a falsy cell to true and records the edit', () => {
415
- const ctx = makeCtx({
416
- columns: [{ id: 'a', field: 'a', editable: true }],
417
- data: [{ a: false }],
418
- });
419
- const cb = createClipboard(ctx);
420
- cb.toggleBooleanCell(0, 0);
421
- expect(ctx.internalData[0].a).toBe(true);
422
- expect(ctx.editedCellValues['0:a']).toBe(true);
423
- expect(ctx.grid.store.setState).toHaveBeenCalled();
424
- });
425
- it('flips a truthy cell to false', () => {
426
- const ctx = makeCtx({
427
- columns: [{ id: 'a', field: 'a', editable: true }],
428
- data: [{ a: true }],
429
- });
430
- const cb = createClipboard(ctx);
431
- cb.toggleBooleanCell(0, 0);
432
- expect(ctx.internalData[0].a).toBe(false);
433
- });
434
- it('does nothing when the row or column is missing', () => {
435
- const ctx = makeCtx({
436
- columns: [{ id: 'a', field: 'a' }],
437
- data: [{ a: true }],
438
- });
439
- const cb = createClipboard(ctx);
440
- cb.toggleBooleanCell(99, 0);
441
- cb.toggleBooleanCell(0, 99);
442
- expect(ctx.internalData[0].a).toBe(true);
443
- });
444
- it('does nothing when the column has no field', () => {
445
- const ctx = makeCtx({
446
- columns: [{ id: 'a' }],
447
- data: [{ a: true }],
448
- });
449
- const cb = createClipboard(ctx);
450
- cb.toggleBooleanCell(0, 0);
451
- expect(ctx.grid.store.setState).not.toHaveBeenCalled();
452
- });
453
- });
454
- describe('copySelectionToClipboard', () => {
455
- let writeText;
456
- beforeEach(() => {
457
- writeText = vi.fn().mockResolvedValue(undefined);
458
- Object.defineProperty(navigator, 'clipboard', {
459
- value: { writeText },
460
- configurable: true,
461
- });
462
- });
463
- it('does nothing when there is no selection', () => {
464
- const ctx = makeCtx();
465
- const cb = createClipboard(ctx);
466
- cb.copySelectionToClipboard();
467
- expect(writeText).not.toHaveBeenCalled();
468
- });
469
- it('writes the selection as tab/newline separated text', () => {
470
- const ctx = makeCtx({
471
- data: [
472
- { a: 'a0', b: 'b0' },
473
- { a: 'a1', b: 'b1' },
474
- ],
475
- });
476
- ctx.selectionRange = {
477
- anchor: { rowIndex: 0, colIndex: 0 },
478
- focus: { rowIndex: 1, colIndex: 1 },
479
- };
480
- const cb = createClipboard(ctx);
481
- cb.copySelectionToClipboard();
482
- expect(writeText).toHaveBeenCalledWith('a0\tb0\na1\tb1');
483
- });
484
- it('prepends a header row when copyHeadersToClipboard is set', () => {
485
- const ctx = makeCtx({
486
- data: [
487
- { a: 'a0', b: 'b0' },
488
- { a: 'a1', b: 'b1' },
489
- ],
490
- });
491
- ctx.props.copyHeadersToClipboard = true;
492
- ctx.selectionRange = {
493
- anchor: { rowIndex: 0, colIndex: 0 },
494
- focus: { rowIndex: 1, colIndex: 1 },
495
- };
496
- const cb = createClipboard(ctx);
497
- cb.copySelectionToClipboard();
498
- expect(writeText).toHaveBeenCalledWith('a\tb\na0\tb0\na1\tb1');
499
- });
500
- it('runs each value through processCellForClipboard', () => {
501
- const ctx = makeCtx({
502
- data: [{ a: 'a0', b: 'b0' }],
503
- });
504
- ctx.props.processCellForClipboard = ({ value }) => `[${value}]`;
505
- ctx.selectionRange = {
506
- anchor: { rowIndex: 0, colIndex: 0 },
507
- focus: { rowIndex: 0, colIndex: 1 },
508
- };
509
- const cb = createClipboard(ctx);
510
- cb.copySelectionToClipboard();
511
- expect(writeText).toHaveBeenCalledWith('[a0]\t[b0]');
512
- });
513
- it('emits empty strings for missing columns and nullish values', () => {
514
- const ctx = makeCtx({
515
- columns: [{ id: 'a', field: 'a' }],
516
- data: [{ a: null }],
517
- });
518
- ctx.selectionRange = {
519
- anchor: { rowIndex: 0, colIndex: 0 },
520
- focus: { rowIndex: 0, colIndex: 1 }, // col 1 doesn't exist
521
- };
522
- const cb = createClipboard(ctx);
523
- cb.copySelectionToClipboard();
524
- expect(writeText).toHaveBeenCalledWith('\t');
525
- });
526
- it('skips group rows', () => {
527
- const ctx = makeCtx({
528
- data: [
529
- { a: 'g', b: 'g' },
530
- { a: 'a1', b: 'b1' },
531
- ],
532
- groupRows: new Set([0]),
533
- });
534
- ctx.selectionRange = {
535
- anchor: { rowIndex: 0, colIndex: 0 },
536
- focus: { rowIndex: 1, colIndex: 1 },
537
- };
538
- const cb = createClipboard(ctx);
539
- cb.copySelectionToClipboard();
540
- expect(writeText).toHaveBeenCalledWith('a1\tb1');
541
- });
542
- it('falls back to legacy copy when clipboard API is absent', () => {
543
- Object.defineProperty(navigator, 'clipboard', {
544
- value: undefined,
545
- configurable: true,
546
- });
547
- const execCommand = vi.fn().mockReturnValue(true);
548
- // @ts-expect-error - jsdom lacks execCommand by default
549
- document.execCommand = execCommand;
550
- const ctx = makeCtx();
551
- ctx.selectionRange = {
552
- anchor: { rowIndex: 0, colIndex: 0 },
553
- focus: { rowIndex: 0, colIndex: 0 },
554
- };
555
- const cb = createClipboard(ctx);
556
- cb.copySelectionToClipboard();
557
- expect(execCommand).toHaveBeenCalledWith('copy');
558
- });
559
- it('swallows an execCommand error in the legacy path', () => {
560
- Object.defineProperty(navigator, 'clipboard', {
561
- value: undefined,
562
- configurable: true,
563
- });
564
- // @ts-expect-error - override execCommand to throw
565
- document.execCommand = () => {
566
- throw new Error('blocked');
567
- };
568
- const ctx = makeCtx();
569
- ctx.selectionRange = {
570
- anchor: { rowIndex: 0, colIndex: 0 },
571
- focus: { rowIndex: 0, colIndex: 0 },
572
- };
573
- const cb = createClipboard(ctx);
574
- // Must not throw even though execCommand blows up.
575
- expect(() => cb.copySelectionToClipboard()).not.toThrow();
576
- });
577
- });
578
- describe('clearSelectedCells', () => {
579
- it('returns false when there is no anchor or active cell', () => {
580
- const ctx = makeCtx();
581
- const cb = createClipboard(ctx);
582
- expect(cb.clearSelectedCells()).toBe(false);
583
- });
584
- it('clears a text cell to empty string and returns true', () => {
585
- const ctx = makeCtx({
586
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
587
- data: [{ a: 'hello' }],
588
- });
589
- ctx.selectionRange = {
590
- anchor: { rowIndex: 0, colIndex: 0 },
591
- focus: { rowIndex: 0, colIndex: 0 },
592
- };
593
- const cb = createClipboard(ctx);
594
- expect(cb.clearSelectedCells()).toBe(true);
595
- expect(ctx.internalData[0].a).toBe('');
596
- expect(ctx.grid.store.setState).toHaveBeenCalled();
597
- });
598
- it('clears a number cell via parseEditorValue coercion', () => {
599
- const ctx = makeCtx({
600
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
601
- data: [{ a: 5 }],
602
- });
603
- ctx.selectionRange = {
604
- anchor: { rowIndex: 0, colIndex: 0 },
605
- focus: { rowIndex: 0, colIndex: 0 },
606
- };
607
- const cb = createClipboard(ctx);
608
- cb.clearSelectedCells();
609
- // parseEditorValue('number', '') coerces '' -> 0 (Number('') is finite).
610
- expect(ctx.internalData[0].a).toBe(0);
611
- });
612
- it('falls back to the active cell when no selection range', () => {
613
- const ctx = makeCtx({
614
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
615
- data: [{ a: 'x' }],
616
- });
617
- ctx._setActiveCell({ rowIndex: 0, colIndex: 0 });
618
- const cb = createClipboard(ctx);
619
- expect(cb.clearSelectedCells()).toBe(true);
620
- expect(ctx.internalData[0].a).toBe('');
621
- });
622
- it('skips group rows and non-editable cells, returning false when nothing changed', () => {
623
- const ctx = makeCtx({
624
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
625
- data: [{ a: 'g' }],
626
- groupRows: new Set([0]),
627
- });
628
- ctx.selectionRange = {
629
- anchor: { rowIndex: 0, colIndex: 0 },
630
- focus: { rowIndex: 0, colIndex: 0 },
631
- };
632
- const cb = createClipboard(ctx);
633
- expect(cb.clearSelectedCells()).toBe(false);
634
- expect(ctx.internalData[0].a).toBe('g');
635
- });
636
- it('skips cells with no field', () => {
637
- const ctx = makeCtx({
638
- columns: [{ id: 'a' }],
639
- data: [{ a: 'x' }],
640
- });
641
- ctx.selectionRange = {
642
- anchor: { rowIndex: 0, colIndex: 0 },
643
- focus: { rowIndex: 0, colIndex: 0 },
644
- };
645
- const cb = createClipboard(ctx);
646
- expect(cb.clearSelectedCells()).toBe(false);
647
- });
648
- it('skips non-editable cells', () => {
649
- const ctx = makeCtx({
650
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
651
- data: [{ a: 'x' }],
652
- editableAt: () => false,
653
- });
654
- ctx.selectionRange = {
655
- anchor: { rowIndex: 0, colIndex: 0 },
656
- focus: { rowIndex: 0, colIndex: 0 },
657
- };
658
- const cb = createClipboard(ctx);
659
- expect(cb.clearSelectedCells()).toBe(false);
660
- expect(ctx.internalData[0].a).toBe('x');
661
- });
662
- it('skips rows whose id maps outside the data bounds', () => {
663
- const ctx = makeCtx({
664
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
665
- data: [{ a: 'x' }],
666
- });
667
- // Force the row's id to a non-numeric value so Number(id) is NaN
668
- // and the integer/bounds guard skips it.
669
- ctx._rows[0].id = 'not-a-number';
670
- ctx.selectionRange = {
671
- anchor: { rowIndex: 0, colIndex: 0 },
672
- focus: { rowIndex: 0, colIndex: 0 },
673
- };
674
- const cb = createClipboard(ctx);
675
- expect(cb.clearSelectedCells()).toBe(false);
676
- expect(ctx.internalData[0].a).toBe('x');
677
- });
678
- });
679
- describe('cutSelectionToClipboard', () => {
680
- beforeEach(() => {
681
- Object.defineProperty(navigator, 'clipboard', {
682
- value: { writeText: vi.fn().mockResolvedValue(undefined) },
683
- configurable: true,
684
- });
685
- });
686
- it('copies then clears the selection', async () => {
687
- const ctx = makeCtx({
688
- columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
689
- data: [{ a: 'hello' }],
690
- });
691
- ctx.selectionRange = {
692
- anchor: { rowIndex: 0, colIndex: 0 },
693
- focus: { rowIndex: 0, colIndex: 0 },
694
- };
695
- const cb = createClipboard(ctx);
696
- await cb.cutSelectionToClipboard();
697
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('hello');
698
- expect(ctx.internalData[0].a).toBe('');
699
- });
700
- });
@@ -1 +0,0 @@
1
- export {};