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