@lapis-notes/csv 0.0.0-stage → 0.2.0

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 (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE.md +4 -0
  3. package/README.md +19 -2
  4. package/dist/app.css +210 -0
  5. package/dist/csv-settings.d.ts +13 -0
  6. package/dist/csv-settings.js +28 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +7 -0
  9. package/dist/lib/CsvSheet.svelte +58 -0
  10. package/dist/lib/CsvSheet.svelte.d.ts +11 -0
  11. package/dist/lib/CsvTable.svelte +58 -0
  12. package/dist/lib/CsvTable.svelte.d.ts +11 -0
  13. package/dist/lib/codemirror/csv-language-extensions.d.ts +3 -0
  14. package/dist/lib/codemirror/csv-language-extensions.js +11 -0
  15. package/dist/lib/codemirror/csv-language.d.ts +3 -0
  16. package/dist/lib/codemirror/csv-language.js +24 -0
  17. package/dist/lib/codemirror/csv-rainbow-theme.d.ts +3 -0
  18. package/dist/lib/codemirror/csv-rainbow-theme.js +15 -0
  19. package/dist/lib/codemirror/csv-source-extensions.d.ts +7 -0
  20. package/dist/lib/codemirror/csv-source-extensions.js +9 -0
  21. package/dist/lib/codemirror/csv.parser.d.ts +2 -0
  22. package/dist/lib/codemirror/csv.parser.js +16 -0
  23. package/dist/lib/codemirror/csv.parser.terms.d.ts +1 -0
  24. package/dist/lib/codemirror/csv.parser.terms.js +2 -0
  25. package/dist/lib/codemirror/syntax.grammar +36 -0
  26. package/dist/lib/components/csv-cell.svelte +137 -0
  27. package/dist/lib/components/csv-cell.svelte.d.ts +11 -0
  28. package/dist/lib/components/csv-data-table-column-head.svelte +76 -0
  29. package/dist/lib/components/csv-data-table-column-head.svelte.d.ts +17 -0
  30. package/dist/lib/components/csv-data-table-column-header.svelte +159 -0
  31. package/dist/lib/components/csv-data-table-column-header.svelte.d.ts +23 -0
  32. package/dist/lib/components/csv-data-table-data-cell.svelte +72 -0
  33. package/dist/lib/components/csv-data-table-data-cell.svelte.d.ts +18 -0
  34. package/dist/lib/components/csv-data-table-drag-overlay.svelte +119 -0
  35. package/dist/lib/components/csv-data-table-drag-overlay.svelte.d.ts +13 -0
  36. package/dist/lib/components/csv-data-table-row-actions.svelte +47 -0
  37. package/dist/lib/components/csv-data-table-row-actions.svelte.d.ts +8 -0
  38. package/dist/lib/components/csv-data-table-row-grip-cell.svelte +102 -0
  39. package/dist/lib/components/csv-data-table-row-grip-cell.svelte.d.ts +19 -0
  40. package/dist/lib/components/csv-data-table-row.svelte +38 -0
  41. package/dist/lib/components/csv-data-table-row.svelte.d.ts +11 -0
  42. package/dist/lib/components/csv-data-table.svelte +903 -0
  43. package/dist/lib/components/csv-data-table.svelte.d.ts +21 -0
  44. package/dist/lib/components/csv-find.svelte +382 -0
  45. package/dist/lib/components/csv-find.svelte.d.ts +24 -0
  46. package/dist/lib/components/csv-row-virtualizer.svelte.d.ts +9 -0
  47. package/dist/lib/components/csv-row-virtualizer.svelte.js +59 -0
  48. package/dist/lib/components/csv-source-view.svelte +127 -0
  49. package/dist/lib/components/csv-source-view.svelte.d.ts +10 -0
  50. package/dist/lib/components/csv-status-bar.svelte +101 -0
  51. package/dist/lib/components/csv-status-bar.svelte.d.ts +14 -0
  52. package/dist/lib/components/csv-table-dnd.d.ts +9 -0
  53. package/dist/lib/components/csv-table-dnd.js +12 -0
  54. package/dist/lib/components/csv-table-drag-measure.d.ts +7 -0
  55. package/dist/lib/components/csv-table-drag-measure.js +38 -0
  56. package/dist/lib/components/csv-table-selection.d.ts +28 -0
  57. package/dist/lib/components/csv-table-selection.js +178 -0
  58. package/dist/lib/components/csv-table-view.svelte +265 -0
  59. package/dist/lib/components/csv-table-view.svelte.d.ts +16 -0
  60. package/dist/lib/csv-plugin.d.ts +9 -0
  61. package/dist/lib/csv-plugin.js +107 -0
  62. package/dist/lib/csv-view-settings.d.ts +43 -0
  63. package/dist/lib/csv-view-settings.js +86 -0
  64. package/dist/lib/ids.d.ts +8 -0
  65. package/dist/lib/ids.js +19 -0
  66. package/dist/lib/model/csv-column-sort.d.ts +16 -0
  67. package/dist/lib/model/csv-column-sort.js +81 -0
  68. package/dist/lib/model/csv-grid.d.ts +31 -0
  69. package/dist/lib/model/csv-grid.js +161 -0
  70. package/dist/lib/model/csv-search.d.ts +38 -0
  71. package/dist/lib/model/csv-search.js +101 -0
  72. package/dist/lib/model/csv-table-model.d.ts +10 -0
  73. package/dist/lib/model/csv-table-model.js +18 -0
  74. package/dist/lib/model/parse-csv.d.ts +10 -0
  75. package/dist/lib/model/parse-csv.js +13 -0
  76. package/dist/lib/model/serialize-csv.d.ts +5 -0
  77. package/dist/lib/model/serialize-csv.js +10 -0
  78. package/dist/lib/sheet/csv-sheet-toolbar.svelte +244 -0
  79. package/dist/lib/sheet/csv-sheet-toolbar.svelte.d.ts +20 -0
  80. package/dist/lib/sheet/csv-sheet-view.svelte +444 -0
  81. package/dist/lib/sheet/csv-sheet-view.svelte.d.ts +18 -0
  82. package/dist/lib/sheet/csv-sheet.svelte +1121 -0
  83. package/dist/lib/sheet/csv-sheet.svelte.d.ts +37 -0
  84. package/dist/lib/sheet/sheet-glyph.svelte +57 -0
  85. package/dist/lib/sheet/sheet-glyph.svelte.d.ts +7 -0
  86. package/dist/lib/sheet/sheet-history.d.ts +14 -0
  87. package/dist/lib/sheet/sheet-history.js +33 -0
  88. package/dist/lib/sheet/sheet-layout.d.ts +121 -0
  89. package/dist/lib/sheet/sheet-layout.js +336 -0
  90. package/dist/lib/sheet/sheet-paint.d.ts +45 -0
  91. package/dist/lib/sheet/sheet-paint.js +274 -0
  92. package/dist/lib/sheet/sheet-theme.d.ts +2 -0
  93. package/dist/lib/sheet/sheet-theme.js +26 -0
  94. package/dist/lib/views/csv-view-mode.d.ts +3 -0
  95. package/dist/lib/views/csv-view-mode.js +6 -0
  96. package/dist/lib/views/csv-view.d.ts +24 -0
  97. package/dist/lib/views/csv-view.js +125 -0
  98. package/dist/styles/csv-find.css +313 -0
  99. package/dist/styles/csv-table.css +882 -0
  100. package/dist/styles.css +37 -0
  101. package/manifest.json +45 -0
  102. package/package.json +86 -3
  103. package/registry-content/overview.md +3 -0
  104. package/registry.json +322 -0
  105. package/styles.css +1 -0
@@ -0,0 +1,903 @@
1
+ <script lang="ts">
2
+ import { DragDropProvider } from "@dnd-kit/svelte";
3
+ import type {
4
+ DragEndEvent,
5
+ DragOverEvent,
6
+ DragStartEvent,
7
+ } from "@dnd-kit/dom";
8
+ import { onMount, untrack } from "svelte";
9
+ import { Button } from "@lapismd/design-core/shadcn/button";
10
+ import * as ContextMenu from "@lapismd/design-core/shadcn/context-menu";
11
+ import * as Table from "@lapismd/design-core/shadcn/table";
12
+ import CodeIcon from "@lucide/svelte/icons/code";
13
+ import { cn } from "@lapis-notes/ui";
14
+ import { Menu } from "@lapis-notes/api";
15
+ import { ContextMenu as MenuComponent } from "@lapis-notes/api/menu";
16
+ import {
17
+ applyCsvColumnSort,
18
+ noteCsvColumnDeleted,
19
+ noteCsvColumnInserted,
20
+ noteCsvColumnMoved,
21
+ noteCsvColumnSortEdit,
22
+ toggleCsvColumnSort,
23
+ type CsvColumnSort,
24
+ } from "../model/csv-column-sort";
25
+ import { CsvGrid } from "../model/csv-grid";
26
+ import {
27
+ headerLabelsFromGrid,
28
+ rowsFromGrid,
29
+ type CsvTableRow,
30
+ } from "../model/csv-table-model";
31
+ import {
32
+ columnSelectionIncludingHeader,
33
+ coordFromTarget,
34
+ createSelectionState,
35
+ dragSelectionRange,
36
+ fullRowsAndColumns,
37
+ getCellsBetween,
38
+ rowSelection,
39
+ selectionClassForCoord,
40
+ type CsvCellCoord,
41
+ type CsvSelectionRange,
42
+ } from "./csv-table-selection";
43
+ import type { CsvTableDragSource } from "./csv-table-dnd";
44
+ import { tableReorderSensors } from "@lapis-notes/ui/table-dnd/sensors";
45
+ import {
46
+ measureColumnOverlayMetrics,
47
+ measureRowOverlayMetrics,
48
+ type CsvDragOverlayMetrics,
49
+ } from "./csv-table-drag-measure";
50
+ import CsvCell from "./csv-cell.svelte";
51
+ import CsvDataTableColumnHead from "./csv-data-table-column-head.svelte";
52
+ import CsvDataTableColumnHeader from "./csv-data-table-column-header.svelte";
53
+ import CsvDataTableDataCell from "./csv-data-table-data-cell.svelte";
54
+ import CsvDataTableRowGripCell from "./csv-data-table-row-grip-cell.svelte";
55
+ import CsvDataTableDragOverlay from "./csv-data-table-drag-overlay.svelte";
56
+ import CsvDataTableRow from "./csv-data-table-row.svelte";
57
+ import CsvDataTableRowActions from "./csv-data-table-row-actions.svelte";
58
+ import {
59
+ createCsvRowVirtualizer,
60
+ CSV_ROW_HEIGHT,
61
+ CSV_ROW_OVERSCAN,
62
+ } from "./csv-row-virtualizer.svelte";
63
+
64
+ type Props = {
65
+ grid: CsvGrid;
66
+ headerRow: boolean;
67
+ showRowNumbers: boolean;
68
+ scrollViewport?: HTMLElement | null;
69
+ /** Grid row indexes to keep. Null shows every body row. */
70
+ rowFilter?: ReadonlySet<number> | null;
71
+ searchCell?: { row: number; col: number } | null;
72
+ onChange: (grid: CsvGrid) => void;
73
+ onSettingsChange?: (patch: { headerRow?: boolean }) => void;
74
+ onToggleSource?: () => void;
75
+ };
76
+
77
+ let {
78
+ grid,
79
+ headerRow,
80
+ showRowNumbers,
81
+ scrollViewport = null,
82
+ rowFilter = null,
83
+ searchCell = null,
84
+ onChange,
85
+ onSettingsChange,
86
+ onToggleSource,
87
+ }: Props = $props();
88
+
89
+ const colCount = $derived(grid.getColCount());
90
+ const tableData = $derived.by(() => {
91
+ const body = rowsFromGrid(grid, headerRow);
92
+ const filter = rowFilter;
93
+ if (!filter) return body;
94
+ return body.filter((row) => filter.has(row.rowIndex));
95
+ });
96
+ const headerLabels = $derived(
97
+ headerLabelsFromGrid(grid, headerRow, colCount),
98
+ );
99
+ const bodyRowIndices = $derived(tableData.map((row) => row.rowIndex));
100
+ const columnSpan = $derived(colCount + (showRowNumbers ? 1 : 0) + 1);
101
+ const rowVirtualizer = createCsvRowVirtualizer();
102
+ const visibleRows = $derived(rowVirtualizer.items);
103
+ const totalSize = $derived(rowVirtualizer.totalSize);
104
+
105
+ $effect(() => {
106
+ const count = tableData.length;
107
+ const viewport = scrollViewport;
108
+ untrack(() => {
109
+ rowVirtualizer.setOptions({
110
+ count,
111
+ enabled: viewport != null,
112
+ estimateSize: () => CSV_ROW_HEIGHT,
113
+ getScrollElement: () => viewport,
114
+ overscan: CSV_ROW_OVERSCAN,
115
+ });
116
+ });
117
+ });
118
+
119
+ const rowPadding = $derived.by(() => {
120
+ const items = visibleRows;
121
+ if (items.length === 0) return { top: 0, bottom: 0 };
122
+ const last = items[items.length - 1];
123
+ return {
124
+ top: Math.max(0, items[0].start),
125
+ bottom: Math.max(0, totalSize - last.end),
126
+ };
127
+ });
128
+
129
+ const selectionPos = createSelectionState();
130
+ let selected: CsvCellCoord[] = $state([]);
131
+ let selectionRange = $state<CsvSelectionRange | null>(null);
132
+ let menuCoords: CsvCellCoord = $state([-1, -1]);
133
+ let dragSource: CsvTableDragSource | null = $state(null);
134
+ let dragOverIndex: number | null = $state(null);
135
+ let dragOverlayMetrics: CsvDragOverlayMetrics | null = $state(null);
136
+ let columnSort = $state<CsvColumnSort | null>(null);
137
+
138
+ function replaceSelection(cells: CsvCellCoord[]) {
139
+ selected = cells;
140
+ const first = cells[0];
141
+ const last = cells[cells.length - 1];
142
+ selectionRange =
143
+ first && last ? dragSelectionRange(first, last) : null;
144
+ }
145
+
146
+ function clearSelection() {
147
+ selected = [];
148
+ selectionRange = null;
149
+ }
150
+
151
+ function emitChange(nextGrid: CsvGrid = grid) {
152
+ onChange(nextGrid.clone());
153
+ }
154
+
155
+ function updateCell(row: number, col: number, value: string) {
156
+ const before = grid.getRowCount();
157
+ grid.updateCell(row, col, value);
158
+ if (columnSort && grid.getRowCount() === before) {
159
+ noteCsvColumnSortEdit(columnSort, row, col, value);
160
+ } else {
161
+ columnSort = null;
162
+ }
163
+ emitChange();
164
+ }
165
+
166
+ function updateHeaderLabel(colIndex: number, value: string) {
167
+ if (!headerRow) {
168
+ columnSort = null;
169
+ const labels = headerLabelsFromGrid(grid, false, colCount);
170
+ labels[colIndex] = value;
171
+ grid.ensureHeaderRow(labels);
172
+ onSettingsChange?.({ headerRow: true });
173
+ }
174
+ grid.updateCell(0, colIndex, value);
175
+ if (columnSort) noteCsvColumnSortEdit(columnSort, 0, colIndex, value);
176
+ emitChange();
177
+ }
178
+
179
+ function insertRowAt(index: number) {
180
+ grid.insertRowAt(index);
181
+ columnSort = null;
182
+ emitChange();
183
+ }
184
+
185
+ function deleteRowAt(index: number) {
186
+ grid.deleteRowAt(index);
187
+ columnSort = null;
188
+ clearSelection();
189
+ emitChange();
190
+ }
191
+
192
+ function insertColumnAt(index: number) {
193
+ grid.insertColumnAt(index);
194
+ columnSort = noteCsvColumnInserted(columnSort, index);
195
+ emitChange();
196
+ }
197
+
198
+ function deleteColumnAt(index: number) {
199
+ grid.deleteColumnAt(index);
200
+ columnSort = noteCsvColumnDeleted(columnSort, index);
201
+ clearSelection();
202
+ emitChange();
203
+ }
204
+
205
+ function moveColumnAt(from: number, to: number) {
206
+ grid.moveColumnAt(from, to);
207
+ columnSort = noteCsvColumnMoved(columnSort, from, to);
208
+ emitChange();
209
+ }
210
+
211
+ function moveRowAt(from: number, to: number) {
212
+ grid.moveRowAt(from, to);
213
+ columnSort = null;
214
+ emitChange();
215
+ }
216
+
217
+ function sortColumn(index: number, ascending: boolean) {
218
+ columnSort = applyCsvColumnSort(
219
+ grid,
220
+ columnSort,
221
+ index,
222
+ ascending ? "asc" : "desc",
223
+ headerRow,
224
+ );
225
+ emitChange();
226
+ }
227
+
228
+ function toggleSort(index: number) {
229
+ columnSort = toggleCsvColumnSort(grid, columnSort, index, headerRow);
230
+ emitChange();
231
+ }
232
+
233
+ function clearCells(cells: CsvCellCoord[]) {
234
+ grid.clearCellsAt(cells);
235
+ rememberClearedCells(cells);
236
+ clearSelection();
237
+ emitChange();
238
+ }
239
+
240
+ function rememberClearedCells(cells: CsvCellCoord[]) {
241
+ if (columnSort && grid.getRowCount() === columnSort.baseline.length) {
242
+ for (const [row, col] of cells) noteCsvColumnSortEdit(columnSort, row, col, "");
243
+ return;
244
+ }
245
+ columnSort = null;
246
+ }
247
+
248
+ function deleteCells(cells: CsvCellCoord[]) {
249
+ const { fullRows, fullCols } = fullRowsAndColumns(
250
+ cells,
251
+ grid.getRowCount(),
252
+ colCount,
253
+ );
254
+ if (fullRows.length + fullCols.length > 0) {
255
+ columnSort = null;
256
+ fullRows.sort((a, b) => b - a).forEach((row) => grid.deleteRowAt(row));
257
+ fullCols.sort((a, b) => b - a).forEach((col) => grid.deleteColumnAt(col));
258
+ } else {
259
+ grid.clearCellsAt(cells);
260
+ rememberClearedCells(cells);
261
+ }
262
+ clearSelection();
263
+ emitChange();
264
+ }
265
+
266
+ function navigateFrom(
267
+ row: number,
268
+ col: number,
269
+ direction: "next" | "prev" | "down" | "up",
270
+ ) {
271
+ const maxRow = grid.getRowCount() - 1;
272
+ const maxCol = colCount - 1;
273
+ let nextRow = row;
274
+ let nextCol = col;
275
+
276
+ switch (direction) {
277
+ case "next":
278
+ if (nextCol < maxCol) nextCol += 1;
279
+ else if (nextRow < maxRow) {
280
+ nextRow += 1;
281
+ nextCol = 0;
282
+ }
283
+ break;
284
+ case "prev":
285
+ if (nextCol > 0) nextCol -= 1;
286
+ else if (nextRow > 0) {
287
+ nextRow -= 1;
288
+ nextCol = maxCol;
289
+ }
290
+ break;
291
+ case "down":
292
+ if (nextRow < maxRow) nextRow += 1;
293
+ break;
294
+ case "up":
295
+ if (nextRow > 0) nextRow -= 1;
296
+ break;
297
+ }
298
+
299
+ const target = document.querySelector<HTMLElement>(
300
+ `[data-csv-cell="${nextRow}:${nextCol}"]`,
301
+ );
302
+ target
303
+ ?.querySelector<HTMLElement>(".csv-cell__display, .csv-cell__input")
304
+ ?.focus();
305
+ }
306
+
307
+ function previewSelection(end: CsvCellCoord) {
308
+ if (end[0] < 0 || end[1] < 0) {
309
+ return;
310
+ }
311
+ if (end[0] === selectionPos.end[0] && end[1] === selectionPos.end[1]) {
312
+ return;
313
+ }
314
+ selectionPos.end = end;
315
+ selectionRange = dragSelectionRange(selectionPos.start, end);
316
+ }
317
+
318
+ function onMouseDown(evt: MouseEvent) {
319
+ if (evt.button !== 0) {
320
+ return;
321
+ }
322
+ if ((evt.target as HTMLElement).closest(".csv-cell__input")) {
323
+ return;
324
+ }
325
+ const start = coordFromTarget(evt.target);
326
+ if (start[0] < 0) {
327
+ return;
328
+ }
329
+ clearSelection();
330
+ selectionPos.isMouseDown = true;
331
+ selectionPos.start = start;
332
+ selectionPos.end = start;
333
+ selectionRange = dragSelectionRange(start, start);
334
+ evt.preventDefault();
335
+ }
336
+
337
+ function onPointerMove(evt: PointerEvent) {
338
+ if (!selectionPos.isMouseDown) {
339
+ return;
340
+ }
341
+ previewSelection(coordFromTarget(evt.target));
342
+ }
343
+
344
+ function finishDragSelection() {
345
+ const sameCell =
346
+ selectionPos.start[0] === selectionPos.end[0] &&
347
+ selectionPos.start[1] === selectionPos.end[1];
348
+ selectionPos.isMouseDown = false;
349
+ if (sameCell) {
350
+ clearSelection();
351
+ return;
352
+ }
353
+ replaceSelection(getCellsBetween(selectionPos.start, selectionPos.end));
354
+ }
355
+
356
+ function selectColumn(column: number) {
357
+ setTimeout(() => {
358
+ replaceSelection(
359
+ columnSelectionIncludingHeader(
360
+ grid.getRowCount(),
361
+ column,
362
+ headerRow,
363
+ bodyRowIndices,
364
+ ),
365
+ );
366
+ });
367
+ }
368
+
369
+ function selectRowByIndex(rowIndex: number) {
370
+ setTimeout(() => {
371
+ replaceSelection(rowSelection(colCount, rowIndex));
372
+ });
373
+ }
374
+
375
+ function selectRow(row: CsvTableRow) {
376
+ selectRowByIndex(row.rowIndex);
377
+ }
378
+
379
+ function handleDragOver(event: DragOverEvent) {
380
+ const target = event.operation.target?.data as
381
+ | { type: string; index: number }
382
+ | undefined;
383
+ if (
384
+ target &&
385
+ dragSource &&
386
+ target.type === dragSource.type &&
387
+ target.index !== dragSource.index
388
+ ) {
389
+ dragOverIndex = target.index;
390
+ }
391
+ }
392
+
393
+ function handleDragStart(event: DragStartEvent) {
394
+ const data = event.operation.source?.data as
395
+ | { type: string; index: number }
396
+ | undefined;
397
+ const table = event.operation.source?.element?.closest(".csv-data-table");
398
+ if (data?.type === "csv-table-col") {
399
+ dragSource = { type: "csv-table-col", index: data.index };
400
+ replaceSelection(
401
+ columnSelectionIncludingHeader(
402
+ grid.getRowCount(),
403
+ data.index,
404
+ headerRow,
405
+ bodyRowIndices,
406
+ ),
407
+ );
408
+ dragOverlayMetrics =
409
+ table instanceof HTMLElement
410
+ ? measureColumnOverlayMetrics(table, data.index)
411
+ : null;
412
+ } else if (data?.type === "csv-table-row") {
413
+ dragSource = { type: "csv-table-row", index: data.index };
414
+ replaceSelection(rowSelection(colCount, data.index));
415
+ dragOverlayMetrics =
416
+ table instanceof HTMLElement
417
+ ? measureRowOverlayMetrics(table, data.index)
418
+ : null;
419
+ }
420
+ }
421
+
422
+ function handleDragEnd(event: DragEndEvent) {
423
+ const source = event.operation.source?.data as
424
+ | { type: string; index: number }
425
+ | undefined;
426
+ const target = event.operation.target?.data as
427
+ | { type: string; index: number }
428
+ | undefined;
429
+ const targetIndex =
430
+ target &&
431
+ source &&
432
+ target.type === source.type &&
433
+ target.index !== source.index
434
+ ? target.index
435
+ : dragOverIndex;
436
+
437
+ if (
438
+ !event.canceled &&
439
+ source &&
440
+ targetIndex !== null &&
441
+ source.index !== targetIndex
442
+ ) {
443
+ if (source.type === "csv-table-col") {
444
+ moveColumnAt(source.index, targetIndex);
445
+ selectColumn(targetIndex);
446
+ } else if (source.type === "csv-table-row") {
447
+ moveRowAt(source.index, targetIndex);
448
+ selectRowByIndex(targetIndex);
449
+ }
450
+ }
451
+
452
+ dragSource = null;
453
+ dragOverIndex = null;
454
+ dragOverlayMetrics = null;
455
+ }
456
+
457
+ function menuEvent(
458
+ cb: (props: { row: number; col: number; evt: Event }) => void,
459
+ ) {
460
+ return (evt: MouseEvent | KeyboardEvent) => {
461
+ const [row, col] = menuCoords;
462
+ if (row === -1 || col === -1) {
463
+ return;
464
+ }
465
+ cb({ row, col, evt });
466
+ menuCoords = [-1, -1];
467
+ };
468
+ }
469
+
470
+ function createMenu() {
471
+ const menu = new Menu();
472
+
473
+ if (!selected.length) {
474
+ menu
475
+ .addMenu((submenu) => {
476
+ submenu
477
+ .setTitle("Row")
478
+ .addItem((item) =>
479
+ item
480
+ .setTitle("Insert row above")
481
+ .setIcon("between-vertical-start")
482
+ .onClick(menuEvent(({ row }) => insertRowAt(row))),
483
+ )
484
+ .addItem((item) =>
485
+ item
486
+ .setTitle("Insert row below")
487
+ .setIcon("between-vertical-end")
488
+ .onClick(menuEvent(({ row }) => insertRowAt(row + 1))),
489
+ )
490
+ .addSeparator()
491
+ .addItem((item) =>
492
+ item
493
+ .setTitle("Move row up")
494
+ .setIcon("arrow-up")
495
+ .onClick(
496
+ menuEvent(({ row }) => {
497
+ if (row > 0) moveRowAt(row, row - 1);
498
+ }),
499
+ ),
500
+ )
501
+ .addItem((item) =>
502
+ item
503
+ .setTitle("Move row down")
504
+ .setIcon("arrow-down")
505
+ .onClick(
506
+ menuEvent(({ row }) => {
507
+ if (row < grid.getRowCount() - 1) {
508
+ moveRowAt(row, row + 1);
509
+ }
510
+ }),
511
+ ),
512
+ )
513
+ .addSeparator()
514
+ .addItem((item) =>
515
+ item
516
+ .setTitle("Delete row")
517
+ .setIcon("trash-2")
518
+ .onClick(menuEvent(({ row }) => deleteRowAt(row))),
519
+ );
520
+ })
521
+ .addMenu((submenu) => {
522
+ submenu
523
+ .setTitle("Column")
524
+ .addItem((item) =>
525
+ item
526
+ .setTitle("Sort A–Z")
527
+ .setIcon("arrow-down-a-z")
528
+ .onClick(menuEvent(({ col }) => sortColumn(col, true))),
529
+ )
530
+ .addItem((item) =>
531
+ item
532
+ .setTitle("Sort Z–A")
533
+ .setIcon("arrow-down-z-a")
534
+ .onClick(menuEvent(({ col }) => sortColumn(col, false))),
535
+ )
536
+ .addSeparator()
537
+ .addItem((item) =>
538
+ item
539
+ .setTitle("Move column left")
540
+ .setIcon("arrow-left")
541
+ .onClick(
542
+ menuEvent(({ col }) => {
543
+ if (col > 0) moveColumnAt(col, col - 1);
544
+ }),
545
+ ),
546
+ )
547
+ .addItem((item) =>
548
+ item
549
+ .setTitle("Move column right")
550
+ .setIcon("arrow-right")
551
+ .onClick(
552
+ menuEvent(({ col }) => {
553
+ if (col < colCount - 1) moveColumnAt(col, col + 1);
554
+ }),
555
+ ),
556
+ )
557
+ .addSeparator()
558
+ .addItem((item) =>
559
+ item
560
+ .setTitle("Insert column left")
561
+ .setIcon("between-horizontal-start")
562
+ .onClick(menuEvent(({ col }) => insertColumnAt(col))),
563
+ )
564
+ .addItem((item) =>
565
+ item
566
+ .setTitle("Insert column right")
567
+ .setIcon("between-horizontal-end")
568
+ .onClick(menuEvent(({ col }) => insertColumnAt(col + 1))),
569
+ )
570
+ .addSeparator()
571
+ .addItem((item) =>
572
+ item
573
+ .setTitle("Delete column")
574
+ .setIcon("trash-2")
575
+ .onClick(menuEvent(({ col }) => deleteColumnAt(col))),
576
+ );
577
+ });
578
+ } else {
579
+ menu
580
+ .addItem((item) =>
581
+ item
582
+ .setTitle("Delete cells")
583
+ .setIcon("trash-2")
584
+ .onClick(() => deleteCells(selected)),
585
+ )
586
+ .addItem((item) =>
587
+ item
588
+ .setTitle("Clear cells")
589
+ .setIcon("eraser")
590
+ .onClick(() => clearCells(selected)),
591
+ );
592
+ }
593
+
594
+ return menu;
595
+ }
596
+
597
+ function rainbowClass(colIndex: number, kind: "head" | "cell"): string {
598
+ const slot = (colIndex % 5) + 1;
599
+ return `csv-data-table__${kind} csv-data-table__col-${slot}`;
600
+ }
601
+
602
+ function rowValuesForIndex(rowIndex: number): string[] {
603
+ return Array.from({ length: colCount }, (_, colIndex) =>
604
+ grid.getCell(rowIndex, colIndex),
605
+ );
606
+ }
607
+
608
+ function columnBodyValuesForIndex(colIndex: number): string[] {
609
+ return tableData.map((row) => row.cells[colIndex] ?? "");
610
+ }
611
+
612
+ function displayRowNumberForIndex(rowIndex: number): number {
613
+ const row = tableData.find((entry) => entry.rowIndex === rowIndex);
614
+ return row?.displayRowNumber ?? rowIndex + 1;
615
+ }
616
+
617
+ function headerSelectedClass(colIndex: number): string | undefined {
618
+ if (!selectionRange) {
619
+ return undefined;
620
+ }
621
+ if (headerRow) {
622
+ return selectionClassForCoord(selectionRange, 0, colIndex);
623
+ }
624
+ if (
625
+ bodyRowIndices.length === 0 ||
626
+ bodyRowIndices.some(
627
+ (rowIndex) =>
628
+ !selectionClassForCoord(selectionRange, rowIndex, colIndex),
629
+ )
630
+ ) {
631
+ return undefined;
632
+ }
633
+ return "is-selected top bottom start end";
634
+ }
635
+
636
+ onMount(() => {
637
+ const onMouseUp = (evt: MouseEvent) => {
638
+ const target = evt.target;
639
+ if (
640
+ target instanceof HTMLElement &&
641
+ target.closest(
642
+ ".csv-data-table__col-grip, .csv-data-table__row-grip, .csv-data-table__header-actions, .csv-data-table__header-menu, .csv-data-table__header-sort, .csv-data-table__row-actions, [data-grab-handle], [role='menu'], [role='menuitem'], [data-slot='context-menu-content']",
643
+ )
644
+ ) {
645
+ if (selectionPos.isMouseDown) finishDragSelection();
646
+ return;
647
+ }
648
+ const wasDragging = selectionPos.isMouseDown;
649
+ if (wasDragging) {
650
+ finishDragSelection();
651
+ } else if (selected.length && evt.button === 0) {
652
+ setTimeout(() => {
653
+ clearSelection();
654
+ });
655
+ }
656
+ };
657
+
658
+ document.addEventListener("mouseup", onMouseUp);
659
+ document.addEventListener("pointermove", onPointerMove);
660
+ return () => {
661
+ document.removeEventListener("mouseup", onMouseUp);
662
+ document.removeEventListener("pointermove", onPointerMove);
663
+ };
664
+ });
665
+ </script>
666
+
667
+ <DragDropProvider
668
+ sensors={tableReorderSensors}
669
+ onDragStart={handleDragStart}
670
+ onDragOver={handleDragOver}
671
+ onDragEnd={handleDragEnd}
672
+ >
673
+ <ContextMenu.Root>
674
+ <ContextMenu.Trigger class="csv-data-table__shell group/trigger">
675
+ <Table.Root
676
+ class="csv-data-table"
677
+ data-csv-row-count={tableData.length}
678
+ data-csv-rendered-rows={visibleRows.length}
679
+ >
680
+ <Table.Header>
681
+ <Table.Row class="csv-data-table__header-row">
682
+ {#if showRowNumbers}
683
+ <Table.Head
684
+ class="csv-data-table__head csv-data-table__head--gutter"
685
+ >
686
+ #
687
+ </Table.Head>
688
+ {/if}
689
+ {#each Array.from({ length: colCount }) as _, colIndex (colIndex)}
690
+ <CsvDataTableColumnHead
691
+ {colIndex}
692
+ {dragSource}
693
+ {dragOverIndex}
694
+ selectedClass={headerSelectedClass(colIndex)}
695
+ isDragSource={dragSource?.type === "csv-table-col" &&
696
+ dragSource.index === colIndex}
697
+ class={cn(
698
+ "csv-data-table__head csv-data-table__head--label",
699
+ rainbowClass(colIndex, "head"),
700
+ )}
701
+ >
702
+ {#snippet children({ attachColGripHandle })}
703
+ <CsvDataTableColumnHeader
704
+ title={headerLabels[colIndex] ?? ""}
705
+ {colIndex}
706
+ {colCount}
707
+ isSynthetic={!headerRow}
708
+ {attachColGripHandle}
709
+ onSelectColumn={() => selectColumn(colIndex)}
710
+ onLabelChange={(value) =>
711
+ updateHeaderLabel(colIndex, value)}
712
+ sortDirection={columnSort?.index === colIndex
713
+ ? columnSort.direction
714
+ : null}
715
+ onToggleSort={() => toggleSort(colIndex)}
716
+ onSortAsc={() => sortColumn(colIndex, true)}
717
+ onSortDesc={() => sortColumn(colIndex, false)}
718
+ onInsertLeft={() => insertColumnAt(colIndex)}
719
+ onInsertRight={() => insertColumnAt(colIndex + 1)}
720
+ onMoveLeft={() => moveColumnAt(colIndex, colIndex - 1)}
721
+ onMoveRight={() => moveColumnAt(colIndex, colIndex + 1)}
722
+ onDelete={() => deleteColumnAt(colIndex)}
723
+ />
724
+ {/snippet}
725
+ </CsvDataTableColumnHead>
726
+ {/each}
727
+ <Table.Head
728
+ class="csv-data-table__head csv-data-table__head--actions"
729
+ >
730
+ {#if !onToggleSource}
731
+ <span class="csv-data-table__actions-label">Row actions</span>
732
+ {/if}
733
+ {#if onToggleSource}
734
+ <Button
735
+ variant="ghost"
736
+ size="icon"
737
+ class="csv-data-table__source-toggle"
738
+ aria-label="View CSV source"
739
+ onclick={onToggleSource}
740
+ >
741
+ <CodeIcon />
742
+ </Button>
743
+ {/if}
744
+ </Table.Head>
745
+ </Table.Row>
746
+ </Table.Header>
747
+ <Table.Body>
748
+ {#if rowPadding.top > 0}
749
+ <Table.Row aria-hidden="true">
750
+ <Table.Cell
751
+ colspan={columnSpan}
752
+ style="height: {rowPadding.top}px; padding: 0; border: 0;"
753
+ ></Table.Cell>
754
+ </Table.Row>
755
+ {/if}
756
+ {#each visibleRows as virtualRow (virtualRow.key)}
757
+ {@const row = tableData[virtualRow.index]}
758
+ {#if row}
759
+ <CsvDataTableRow
760
+ rowIndex={row.rowIndex}
761
+ virtualIndex={virtualRow.index}
762
+ class="csv-data-table__row"
763
+ isRowDragSource={dragSource?.type === "csv-table-row" &&
764
+ dragSource.index === row.rowIndex}
765
+ >
766
+ {#snippet children()}
767
+ {#if showRowNumbers}
768
+ <Table.Cell
769
+ class="csv-data-table__cell csv-data-table__cell--gutter"
770
+ >
771
+ <span class="csv-data-table__rownum"
772
+ >{row.displayRowNumber}</span
773
+ >
774
+ </Table.Cell>
775
+ {/if}
776
+ {#each Array.from( { length: colCount }, ) as _, colIndex (colIndex)}
777
+ {@const value = row.cells[colIndex] ?? ""}
778
+ {@const selectedClass = selectionClassForCoord(
779
+ selectionRange,
780
+ row.rowIndex,
781
+ colIndex,
782
+ )}
783
+ {@const cellProps = {
784
+ rowIndex: row.rowIndex,
785
+ colIndex,
786
+ dragSource,
787
+ dragOverIndex,
788
+ selectedClass,
789
+ isColDragSource:
790
+ dragSource?.type === "csv-table-col" &&
791
+ dragSource.index === colIndex,
792
+ class: rainbowClass(colIndex, "cell"),
793
+ oncontextmenu: () => {
794
+ menuCoords = [row.rowIndex, colIndex];
795
+ },
796
+ }}
797
+ {#if colIndex === 0}
798
+ <CsvDataTableRowGripCell
799
+ {...cellProps}
800
+ onSelectRow={() => selectRow(row)}
801
+ >
802
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
803
+ <div
804
+ class="csv-data-table__cell-shell"
805
+ data-csv-cell={`${row.rowIndex}:${colIndex}`}
806
+ data-csv-search={searchCell?.row === row.rowIndex &&
807
+ searchCell.col === colIndex
808
+ ? "active"
809
+ : undefined}
810
+ data-csv-selected={selectedClass ? "true" : undefined}
811
+ onmousedown={onMouseDown}
812
+ onpointerenter={() => {
813
+ if (selectionPos.isMouseDown) {
814
+ previewSelection([row.rowIndex, colIndex]);
815
+ }
816
+ }}
817
+ >
818
+ <CsvCell
819
+ {value}
820
+ multiline={value.includes("\n")}
821
+ class="csv-data-table__cell-editor"
822
+ onContentChange={(nextValue) =>
823
+ updateCell(row.rowIndex, colIndex, nextValue)}
824
+ onNavigate={(direction) =>
825
+ navigateFrom(row.rowIndex, colIndex, direction)}
826
+ />
827
+ </div>
828
+ </CsvDataTableRowGripCell>
829
+ {:else}
830
+ <CsvDataTableDataCell {...cellProps}>
831
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
832
+ <div
833
+ class="csv-data-table__cell-shell"
834
+ data-csv-cell={`${row.rowIndex}:${colIndex}`}
835
+ data-csv-search={searchCell?.row === row.rowIndex &&
836
+ searchCell.col === colIndex
837
+ ? "active"
838
+ : undefined}
839
+ data-csv-selected={selectedClass ? "true" : undefined}
840
+ onmousedown={onMouseDown}
841
+ onpointerenter={() => {
842
+ if (selectionPos.isMouseDown) {
843
+ previewSelection([row.rowIndex, colIndex]);
844
+ }
845
+ }}
846
+ >
847
+ <CsvCell
848
+ {value}
849
+ multiline={value.includes("\n")}
850
+ class="csv-data-table__cell-editor"
851
+ onContentChange={(nextValue) =>
852
+ updateCell(row.rowIndex, colIndex, nextValue)}
853
+ onNavigate={(direction) =>
854
+ navigateFrom(row.rowIndex, colIndex, direction)}
855
+ />
856
+ </div>
857
+ </CsvDataTableDataCell>
858
+ {/if}
859
+ {/each}
860
+ <Table.Cell
861
+ class="csv-data-table__cell csv-data-table__cell--actions"
862
+ >
863
+ <CsvDataTableRowActions
864
+ onInsertAbove={() => insertRowAt(row.rowIndex)}
865
+ onInsertBelow={() => insertRowAt(row.rowIndex + 1)}
866
+ onDelete={() => deleteRowAt(row.rowIndex)}
867
+ />
868
+ </Table.Cell>
869
+ {/snippet}
870
+ </CsvDataTableRow>
871
+ {/if}
872
+ {:else}
873
+ {#if tableData.length === 0}
874
+ <Table.Row>
875
+ <Table.Cell colspan={columnSpan} class="csv-data-table__empty">
876
+ {rowFilter ? "No matching rows" : "No rows"}
877
+ </Table.Cell>
878
+ </Table.Row>
879
+ {/if}
880
+ {/each}
881
+ {#if rowPadding.bottom > 0}
882
+ <Table.Row aria-hidden="true">
883
+ <Table.Cell
884
+ colspan={columnSpan}
885
+ style="height: {rowPadding.bottom}px; padding: 0; border: 0;"
886
+ ></Table.Cell>
887
+ </Table.Row>
888
+ {/if}
889
+ </Table.Body>
890
+ </Table.Root>
891
+ </ContextMenu.Trigger>
892
+ <MenuComponent class="csv-data-table__menu" menu={createMenu()} />
893
+ </ContextMenu.Root>
894
+ <CsvDataTableDragOverlay
895
+ {showRowNumbers}
896
+ {headerLabels}
897
+ metrics={dragOverlayMetrics}
898
+ {rainbowClass}
899
+ {rowValuesForIndex}
900
+ {columnBodyValuesForIndex}
901
+ {displayRowNumberForIndex}
902
+ />
903
+ </DragDropProvider>