@svgrid/grid 1.1.2 → 1.2.3

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 (143) hide show
  1. package/dist/SvGrid.css +294 -16
  2. package/dist/SvGrid.svelte +4 -1
  3. package/dist/SvGridDropdown.svelte +7 -3
  4. package/dist/cdn/svgrid.js +13506 -0
  5. package/dist/cdn/svgrid.svelte-external.js +11190 -0
  6. package/package.json +12 -2
  7. package/src/SvGrid.css +294 -16
  8. package/src/SvGrid.svelte +4 -1
  9. package/src/SvGridDropdown.svelte +7 -3
  10. package/dist/SvGrid.helpers.test.d.ts +0 -1
  11. package/dist/SvGrid.helpers.test.js +0 -298
  12. package/dist/SvGridChart.test.d.ts +0 -1
  13. package/dist/SvGridChart.test.js +0 -62
  14. package/dist/a11y.contract.test.d.ts +0 -1
  15. package/dist/a11y.contract.test.js +0 -39
  16. package/dist/a11y.test.d.ts +0 -1
  17. package/dist/a11y.test.js +0 -44
  18. package/dist/aligned-grids.test.d.ts +0 -1
  19. package/dist/aligned-grids.test.js +0 -75
  20. package/dist/build-api.coverage.test.d.ts +0 -20
  21. package/dist/build-api.coverage.test.js +0 -505
  22. package/dist/cell-formatting.test.d.ts +0 -1
  23. package/dist/cell-formatting.test.js +0 -234
  24. package/dist/cell-render.test.d.ts +0 -1
  25. package/dist/cell-render.test.js +0 -338
  26. package/dist/chart-export.test.d.ts +0 -1
  27. package/dist/chart-export.test.js +0 -302
  28. package/dist/chart.coverage.test.d.ts +0 -1
  29. package/dist/chart.coverage.test.js +0 -748
  30. package/dist/chart.test.d.ts +0 -1
  31. package/dist/chart.test.js +0 -289
  32. package/dist/clipboard.test.d.ts +0 -1
  33. package/dist/clipboard.test.js +0 -700
  34. package/dist/collaboration.coverage.test.d.ts +0 -1
  35. package/dist/collaboration.coverage.test.js +0 -200
  36. package/dist/collaboration.test.d.ts +0 -1
  37. package/dist/collaboration.test.js +0 -65
  38. package/dist/column-groups.test.d.ts +0 -1
  39. package/dist/column-groups.test.js +0 -56
  40. package/dist/column-types.test.d.ts +0 -1
  41. package/dist/column-types.test.js +0 -62
  42. package/dist/columns.test.d.ts +0 -1
  43. package/dist/columns.test.js +0 -625
  44. package/dist/conditional-formatting.test.d.ts +0 -1
  45. package/dist/conditional-formatting.test.js +0 -87
  46. package/dist/core.coverage.test.d.ts +0 -1
  47. package/dist/core.coverage.test.js +0 -186
  48. package/dist/core.performance.test.d.ts +0 -1
  49. package/dist/core.performance.test.js +0 -29
  50. package/dist/createGrid.test.d.ts +0 -1
  51. package/dist/createGrid.test.js +0 -9
  52. package/dist/editing.test.d.ts +0 -1
  53. package/dist/editing.test.js +0 -732
  54. package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
  55. package/dist/editors/cell-editors.coverage.test.js +0 -139
  56. package/dist/editors/cell-editors.test.d.ts +0 -1
  57. package/dist/editors/cell-editors.test.js +0 -75
  58. package/dist/facet-buckets.test.d.ts +0 -1
  59. package/dist/facet-buckets.test.js +0 -296
  60. package/dist/fill-patterns.test.d.ts +0 -1
  61. package/dist/fill-patterns.test.js +0 -81
  62. package/dist/filter-operators.test.d.ts +0 -1
  63. package/dist/filter-operators.test.js +0 -135
  64. package/dist/filtering/excel-filters.test.d.ts +0 -1
  65. package/dist/filtering/excel-filters.test.js +0 -116
  66. package/dist/filtering/locale-filter.test.d.ts +0 -13
  67. package/dist/filtering/locale-filter.test.js +0 -189
  68. package/dist/flex-render.test.d.ts +0 -1
  69. package/dist/flex-render.test.js +0 -142
  70. package/dist/group-aggregate.test.d.ts +0 -1
  71. package/dist/group-aggregate.test.js +0 -32
  72. package/dist/hyperformula-adapter.test.d.ts +0 -1
  73. package/dist/hyperformula-adapter.test.js +0 -205
  74. package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
  75. package/dist/keyboard-handlers.coverage.test.js +0 -495
  76. package/dist/keyboard.test.d.ts +0 -1
  77. package/dist/keyboard.test.js +0 -27
  78. package/dist/menus.test.d.ts +0 -1
  79. package/dist/menus.test.js +0 -560
  80. package/dist/named-views.coverage.test.d.ts +0 -1
  81. package/dist/named-views.coverage.test.js +0 -180
  82. package/dist/named-views.test.d.ts +0 -1
  83. package/dist/named-views.test.js +0 -57
  84. package/dist/new-features.test.d.ts +0 -1
  85. package/dist/new-features.test.js +0 -183
  86. package/dist/render-component.test.d.ts +0 -1
  87. package/dist/render-component.test.js +0 -41
  88. package/dist/row-drag.test.d.ts +0 -1
  89. package/dist/row-drag.test.js +0 -142
  90. package/dist/row-resize.test.d.ts +0 -1
  91. package/dist/row-resize.test.js +0 -329
  92. package/dist/scroll-sync.test.d.ts +0 -1
  93. package/dist/scroll-sync.test.js +0 -290
  94. package/dist/selection.multi-range.test.d.ts +0 -1
  95. package/dist/selection.multi-range.test.js +0 -55
  96. package/dist/selection.test.d.ts +0 -1
  97. package/dist/selection.test.js +0 -647
  98. package/dist/server-data-source.coverage.test.d.ts +0 -1
  99. package/dist/server-data-source.coverage.test.js +0 -154
  100. package/dist/server-data-source.test.d.ts +0 -1
  101. package/dist/server-data-source.test.js +0 -81
  102. package/dist/sparkline.test.d.ts +0 -1
  103. package/dist/sparkline.test.js +0 -52
  104. package/dist/spreadsheet.test.d.ts +0 -1
  105. package/dist/spreadsheet.test.js +0 -446
  106. package/dist/subscribe.test.d.ts +0 -1
  107. package/dist/subscribe.test.js +0 -93
  108. package/dist/svgrid.api-extensions.test.d.ts +0 -14
  109. package/dist/svgrid.api-extensions.test.js +0 -334
  110. package/dist/svgrid.api.test.d.ts +0 -15
  111. package/dist/svgrid.api.test.js +0 -169
  112. package/dist/svgrid.behavior.test.d.ts +0 -15
  113. package/dist/svgrid.behavior.test.js +0 -625
  114. package/dist/svgrid.column-reorder.test.d.ts +0 -15
  115. package/dist/svgrid.column-reorder.test.js +0 -224
  116. package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
  117. package/dist/svgrid.comments-autocomplete.test.js +0 -96
  118. package/dist/svgrid.context-menu.test.d.ts +0 -1
  119. package/dist/svgrid.context-menu.test.js +0 -102
  120. package/dist/svgrid.features.test.d.ts +0 -1
  121. package/dist/svgrid.features.test.js +0 -129
  122. package/dist/svgrid.interaction.test.d.ts +0 -9
  123. package/dist/svgrid.interaction.test.js +0 -349
  124. package/dist/svgrid.locale-filtering.test.d.ts +0 -15
  125. package/dist/svgrid.locale-filtering.test.js +0 -261
  126. package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
  127. package/dist/svgrid.new-features.wrapper.test.js +0 -194
  128. package/dist/svgrid.row-pinning.test.d.ts +0 -14
  129. package/dist/svgrid.row-pinning.test.js +0 -250
  130. package/dist/svgrid.set-filter.test.d.ts +0 -17
  131. package/dist/svgrid.set-filter.test.js +0 -268
  132. package/dist/svgrid.wrapper.test.d.ts +0 -1
  133. package/dist/svgrid.wrapper.test.js +0 -59
  134. package/dist/test-setup.d.ts +0 -1
  135. package/dist/test-setup.js +0 -35
  136. package/dist/transaction.test.d.ts +0 -1
  137. package/dist/transaction.test.js +0 -89
  138. package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
  139. package/dist/virtualization/column-virtualizer.test.js +0 -25
  140. package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
  141. package/dist/virtualization/scroll-scaling.test.js +0 -127
  142. package/dist/virtualization/virtualizer.test.d.ts +0 -1
  143. 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 {};