@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,21 @@
1
+ import { CsvGrid } from "../model/csv-grid";
2
+ type Props = {
3
+ grid: CsvGrid;
4
+ headerRow: boolean;
5
+ showRowNumbers: boolean;
6
+ scrollViewport?: HTMLElement | null;
7
+ /** Grid row indexes to keep. Null shows every body row. */
8
+ rowFilter?: ReadonlySet<number> | null;
9
+ searchCell?: {
10
+ row: number;
11
+ col: number;
12
+ } | null;
13
+ onChange: (grid: CsvGrid) => void;
14
+ onSettingsChange?: (patch: {
15
+ headerRow?: boolean;
16
+ }) => void;
17
+ onToggleSource?: () => void;
18
+ };
19
+ declare const CsvDataTable: import("svelte").Component<Props, {}, "">;
20
+ type CsvDataTable = ReturnType<typeof CsvDataTable>;
21
+ export default CsvDataTable;
@@ -0,0 +1,382 @@
1
+ <script lang="ts">
2
+ import type { Attachment } from "svelte/attachments";
3
+ import { setSearchQuery } from "@codemirror/search";
4
+ import type { EditorView } from "@codemirror/view";
5
+ import { cn } from "@lapis-notes/ui";
6
+ import ArrowDown from "@lucide/svelte/icons/arrow-down";
7
+ import ArrowUp from "@lucide/svelte/icons/arrow-up";
8
+ import CaseSensitive from "@lucide/svelte/icons/case-sensitive";
9
+ import ChevronDown from "@lucide/svelte/icons/chevron-down";
10
+ import ChevronRight from "@lucide/svelte/icons/chevron-right";
11
+ import Regex from "@lucide/svelte/icons/regex";
12
+ import Replace from "@lucide/svelte/icons/replace";
13
+ import ReplaceAll from "@lucide/svelte/icons/replace-all";
14
+ import SearchIcon from "@lucide/svelte/icons/search";
15
+ import WholeWord from "@lucide/svelte/icons/whole-word";
16
+ import X from "@lucide/svelte/icons/x";
17
+ import {
18
+ csvCellMatches,
19
+ csvDocumentMatches,
20
+ csvSearchQuery,
21
+ replaceCellText,
22
+ searchReplacement,
23
+ type CsvCellMatch,
24
+ } from "../model/csv-search";
25
+ import "../../styles/csv-find.css";
26
+
27
+ type CellEdit = { row: number; col: number; value: string };
28
+
29
+ let {
30
+ open = $bindable(false),
31
+ searchRequest = 0,
32
+ rows = [],
33
+ editor = null,
34
+ revision = 0,
35
+ searchValue = $bindable(""),
36
+ replaceValue = $bindable(""),
37
+ caseSensitive = $bindable(false),
38
+ wholeWord = $bindable(false),
39
+ regexp = $bindable(false),
40
+ active = $bindable(0),
41
+ onReplaceCells,
42
+ }: {
43
+ open?: boolean;
44
+ searchRequest?: number;
45
+ rows?: readonly (readonly string[])[];
46
+ editor?: EditorView | null;
47
+ revision?: number;
48
+ searchValue?: string;
49
+ replaceValue?: string;
50
+ caseSensitive?: boolean;
51
+ wholeWord?: boolean;
52
+ regexp?: boolean;
53
+ active?: number;
54
+ onReplaceCells?: (edits: CellEdit[]) => void;
55
+ } = $props();
56
+
57
+ let replaceOpen = $state(false);
58
+
59
+ const canReplace = $derived(editor != null || onReplaceCells != null);
60
+
61
+ function readQuery() {
62
+ return csvSearchQuery({
63
+ search: searchValue,
64
+ replace: replaceValue,
65
+ caseSensitive,
66
+ wholeWord,
67
+ regexp,
68
+ });
69
+ }
70
+
71
+ const tableMatches = $derived.by(() => {
72
+ if (editor) return [];
73
+ return csvCellMatches(rows, readQuery());
74
+ });
75
+ const sourceMatches = $derived.by(() => {
76
+ if (!editor) return [];
77
+ void revision;
78
+ return csvDocumentMatches(editor.state.doc, readQuery());
79
+ });
80
+ const matchCount = $derived(
81
+ editor ? sourceMatches.length : tableMatches.length,
82
+ );
83
+ const currentIndex = $derived(
84
+ matchCount === 0 ? 0 : Math.min(active, matchCount - 1),
85
+ );
86
+ const noMatches = $derived(
87
+ Boolean(searchValue) && (!readQuery().valid || matchCount === 0),
88
+ );
89
+ const status = $derived(
90
+ !searchValue
91
+ ? ""
92
+ : noMatches
93
+ ? "No matches"
94
+ : `${currentIndex + 1} of ${matchCount}`,
95
+ );
96
+
97
+ function close() {
98
+ open = false;
99
+ }
100
+
101
+ function move(delta: number) {
102
+ if (matchCount === 0) return;
103
+ active = (currentIndex + delta + matchCount) % matchCount;
104
+ }
105
+
106
+ function replaceTable(list: readonly CsvCellMatch[]) {
107
+ if (!onReplaceCells || list.length === 0) return;
108
+ const query = readQuery();
109
+ const groups: Record<string, CsvCellMatch[]> = {};
110
+ for (const match of list) {
111
+ const key = `${match.row}:${match.col}`;
112
+ const group = groups[key] ?? [];
113
+ group.push(match);
114
+ groups[key] = group;
115
+ }
116
+ const edits: CellEdit[] = [];
117
+ for (const group of Object.values(groups)) {
118
+ const first = group[0];
119
+ if (!first) continue;
120
+ const cell = rows[first.row]?.[first.col] ?? "";
121
+ edits.push({
122
+ row: first.row,
123
+ col: first.col,
124
+ value: replaceCellText(cell, group, query),
125
+ });
126
+ }
127
+ onReplaceCells(edits);
128
+ }
129
+
130
+ function replaceSource(list: readonly { from: number; to: number }[]) {
131
+ const view = editor;
132
+ if (!view || list.length === 0) return;
133
+ const query = readQuery();
134
+ view.dispatch({
135
+ changes: [...list]
136
+ .sort((left, right) => right.from - left.from)
137
+ .map((match) => ({
138
+ from: match.from,
139
+ to: match.to,
140
+ insert: searchReplacement(
141
+ query,
142
+ view.state.sliceDoc(match.from, match.to),
143
+ ),
144
+ })),
145
+ });
146
+ }
147
+
148
+ function replaceCurrent() {
149
+ if (editor) {
150
+ const match = sourceMatches[currentIndex];
151
+ if (match) replaceSource([match]);
152
+ return;
153
+ }
154
+ const match = tableMatches[currentIndex];
155
+ if (match) replaceTable([match]);
156
+ }
157
+
158
+ function replaceAll() {
159
+ if (editor) {
160
+ replaceSource(sourceMatches);
161
+ return;
162
+ }
163
+ replaceTable(tableMatches);
164
+ }
165
+
166
+ function onKeydown(event: KeyboardEvent) {
167
+ if (event.key === "Escape") {
168
+ event.preventDefault();
169
+ close();
170
+ return;
171
+ }
172
+ if (event.key !== "Enter") return;
173
+ event.preventDefault();
174
+ move(event.shiftKey ? -1 : 1);
175
+ }
176
+
177
+ const sourcePaintKey = $derived(
178
+ editor
179
+ ? [
180
+ revision,
181
+ open ? 1 : 0,
182
+ searchValue,
183
+ replaceValue,
184
+ caseSensitive ? 1 : 0,
185
+ wholeWord ? 1 : 0,
186
+ regexp ? 1 : 0,
187
+ active,
188
+ ].join("\u0000")
189
+ : "table",
190
+ );
191
+
192
+ const paintSource: Attachment<HTMLElement> = () => {
193
+ const view = editor;
194
+ if (!view) return;
195
+ const query = readQuery();
196
+ const shown = open;
197
+ const index = currentIndex;
198
+ view.dispatch({
199
+ effects: setSearchQuery.of(shown ? query : csvSearchQuery({ search: "" })),
200
+ });
201
+ if (!shown) return;
202
+ const list = csvDocumentMatches(view.state.doc, query);
203
+ const match = list[index];
204
+ if (!match) return;
205
+ const selection = view.state.selection.main;
206
+ if (selection.from === match.from && selection.to === match.to) return;
207
+ view.dispatch({
208
+ selection: { anchor: match.from, head: match.to },
209
+ scrollIntoView: true,
210
+ });
211
+ };
212
+
213
+ const focusSearchField: Attachment<HTMLInputElement> = (node) => {
214
+ node.focus();
215
+ node.select();
216
+ };
217
+ </script>
218
+
219
+ {#key sourcePaintKey}
220
+ <span hidden {@attach paintSource}></span>
221
+ {/key}
222
+
223
+ {#if open}
224
+ <search class="ui-transcript-search" data-csv-find>
225
+ <div class="ui-transcript-search__body">
226
+ {#if canReplace}
227
+ <button
228
+ type="button"
229
+ class="ui-transcript-search__button"
230
+ aria-label="Toggle replace"
231
+ aria-expanded={replaceOpen}
232
+ title="Toggle replace"
233
+ onclick={() => (replaceOpen = !replaceOpen)}
234
+ >
235
+ {#if replaceOpen}
236
+ <ChevronDown size={16} aria-hidden="true" />
237
+ {:else}
238
+ <ChevronRight size={16} aria-hidden="true" />
239
+ {/if}
240
+ </button>
241
+ {/if}
242
+ <div class="ui-transcript-search__fields">
243
+ <div class="ui-transcript-search__row">
244
+ <div
245
+ class={cn("ui-transcript-search__field", {
246
+ "ui-transcript-search__field--invalid": noMatches,
247
+ })}
248
+ >
249
+ <span class="ui-transcript-search__find-icon">
250
+ <SearchIcon size={16} aria-hidden="true" />
251
+ </span>
252
+ {#key searchRequest}
253
+ <input
254
+ {@attach focusSearchField}
255
+ class="ui-transcript-search__input"
256
+ type="text"
257
+ placeholder="Find"
258
+ aria-label="Find"
259
+ aria-invalid={noMatches}
260
+ value={searchValue}
261
+ onkeydown={onKeydown}
262
+ oninput={(event) => {
263
+ searchValue = event.currentTarget.value;
264
+ active = 0;
265
+ }}
266
+ />
267
+ {/key}
268
+ {#if status}
269
+ <span class="ui-transcript-search__count">{status}</span>
270
+ {/if}
271
+ <div
272
+ class="ui-transcript-search__options"
273
+ role="group"
274
+ aria-label="Search options"
275
+ >
276
+ <button
277
+ type="button"
278
+ class={cn(
279
+ "ui-transcript-search__button ui-transcript-search__toggle",
280
+ { "ui-transcript-search__button--active": caseSensitive },
281
+ )}
282
+ aria-label="Case sensitive"
283
+ aria-pressed={caseSensitive}
284
+ title="Case sensitive"
285
+ onclick={() => (caseSensitive = !caseSensitive)}
286
+ >
287
+ <CaseSensitive size={16} aria-hidden="true" />
288
+ </button>
289
+ <button
290
+ type="button"
291
+ class={cn(
292
+ "ui-transcript-search__button ui-transcript-search__toggle",
293
+ { "ui-transcript-search__button--active": wholeWord },
294
+ )}
295
+ aria-label="Match whole word"
296
+ aria-pressed={wholeWord}
297
+ title="Match whole word"
298
+ onclick={() => (wholeWord = !wholeWord)}
299
+ >
300
+ <WholeWord size={16} aria-hidden="true" />
301
+ </button>
302
+ <button
303
+ type="button"
304
+ class={cn(
305
+ "ui-transcript-search__button ui-transcript-search__toggle",
306
+ { "ui-transcript-search__button--active": regexp },
307
+ )}
308
+ aria-label="Use regular expression"
309
+ aria-pressed={regexp}
310
+ title="Use regular expression"
311
+ onclick={() => (regexp = !regexp)}
312
+ >
313
+ <Regex size={16} aria-hidden="true" />
314
+ </button>
315
+ </div>
316
+ </div>
317
+ <button
318
+ type="button"
319
+ class="ui-transcript-search__button"
320
+ aria-label="Previous match"
321
+ title="Previous match"
322
+ onclick={() => move(-1)}
323
+ >
324
+ <ArrowUp size={16} aria-hidden="true" />
325
+ </button>
326
+ <button
327
+ type="button"
328
+ class="ui-transcript-search__button"
329
+ aria-label="Next match"
330
+ title="Next match"
331
+ onclick={() => move(1)}
332
+ >
333
+ <ArrowDown size={16} aria-hidden="true" />
334
+ </button>
335
+ <button
336
+ type="button"
337
+ class="ui-transcript-search__button"
338
+ aria-label="Close search"
339
+ title="Close search"
340
+ onclick={close}
341
+ >
342
+ <X size={16} aria-hidden="true" />
343
+ </button>
344
+ </div>
345
+ {#if replaceOpen && canReplace}
346
+ <div
347
+ class="ui-transcript-search__row ui-transcript-search__replace-row"
348
+ >
349
+ <input
350
+ class="ui-transcript-search__input ui-transcript-search__replace-input"
351
+ type="text"
352
+ placeholder="Replace"
353
+ aria-label="Replace"
354
+ value={replaceValue}
355
+ onkeydown={onKeydown}
356
+ oninput={(event) => (replaceValue = event.currentTarget.value)}
357
+ />
358
+ <button
359
+ type="button"
360
+ class="ui-transcript-search__button"
361
+ aria-label="Replace match"
362
+ title="Replace match"
363
+ onclick={replaceCurrent}
364
+ >
365
+ <Replace size={16} aria-hidden="true" />
366
+ </button>
367
+ <button
368
+ type="button"
369
+ class="ui-transcript-search__button"
370
+ aria-label="Replace all matches"
371
+ title="Replace all"
372
+ onclick={replaceAll}
373
+ >
374
+ <ReplaceAll size={16} aria-hidden="true" />
375
+ </button>
376
+ </div>
377
+ {/if}
378
+ </div>
379
+ </div>
380
+ <span class="ui-transcript-search__status" role="status">{status}</span>
381
+ </search>
382
+ {/if}
@@ -0,0 +1,24 @@
1
+ import type { EditorView } from "@codemirror/view";
2
+ import "../../styles/csv-find.css";
3
+ type CellEdit = {
4
+ row: number;
5
+ col: number;
6
+ value: string;
7
+ };
8
+ type $$ComponentProps = {
9
+ open?: boolean;
10
+ searchRequest?: number;
11
+ rows?: readonly (readonly string[])[];
12
+ editor?: EditorView | null;
13
+ revision?: number;
14
+ searchValue?: string;
15
+ replaceValue?: string;
16
+ caseSensitive?: boolean;
17
+ wholeWord?: boolean;
18
+ regexp?: boolean;
19
+ active?: number;
20
+ onReplaceCells?: (edits: CellEdit[]) => void;
21
+ };
22
+ declare const CsvFind: import("svelte").Component<$$ComponentProps, {}, "open" | "active" | "searchValue" | "replaceValue" | "caseSensitive" | "wholeWord" | "regexp">;
23
+ type CsvFind = ReturnType<typeof CsvFind>;
24
+ export default CsvFind;
@@ -0,0 +1,9 @@
1
+ import { type VirtualItem, type VirtualizerOptions } from "@tanstack/virtual-core";
2
+ export declare const CSV_ROW_HEIGHT = 32;
3
+ /** Rows kept above and below the viewport so a fast scroll does not run into the spacer. */
4
+ export declare const CSV_ROW_OVERSCAN = 16;
5
+ export declare function createCsvRowVirtualizer(): {
6
+ readonly items: VirtualItem[];
7
+ readonly totalSize: number;
8
+ setOptions(next: Partial<VirtualizerOptions<HTMLElement, HTMLTableRowElement>>): void;
9
+ };
@@ -0,0 +1,59 @@
1
+ import { elementScroll, observeElementOffset, observeElementRect, Virtualizer, } from "@tanstack/virtual-core";
2
+ import { flushSync } from "svelte";
3
+ export const CSV_ROW_HEIGHT = 32;
4
+ /** Rows kept above and below the viewport so a fast scroll does not run into the spacer. */
5
+ export const CSV_ROW_OVERSCAN = 16;
6
+ export function createCsvRowVirtualizer() {
7
+ let items = $state.raw([]);
8
+ let totalSize = $state(0);
9
+ let flushing = false;
10
+ const options = {
11
+ count: 0,
12
+ enabled: false,
13
+ estimateSize: () => CSV_ROW_HEIGHT,
14
+ getScrollElement: () => null,
15
+ observeElementOffset,
16
+ observeElementRect,
17
+ onChange: (instance, scrolling) => {
18
+ items = instance.getVirtualItems();
19
+ totalSize = instance.getTotalSize();
20
+ if (!scrolling || flushing)
21
+ return;
22
+ flushing = true;
23
+ try {
24
+ flushSync();
25
+ }
26
+ finally {
27
+ flushing = false;
28
+ }
29
+ },
30
+ overscan: CSV_ROW_OVERSCAN,
31
+ scrollToFn: elementScroll,
32
+ };
33
+ const virtualizer = new Virtualizer(options);
34
+ $effect(() => {
35
+ const cleanup = virtualizer._didMount();
36
+ virtualizer._willUpdate();
37
+ return cleanup;
38
+ });
39
+ return {
40
+ get items() {
41
+ return items;
42
+ },
43
+ get totalSize() {
44
+ return totalSize;
45
+ },
46
+ setOptions(next) {
47
+ virtualizer.setOptions({
48
+ ...virtualizer.options,
49
+ ...next,
50
+ onChange: options.onChange,
51
+ });
52
+ // virtual-core only observes the scroll element from `_willUpdate`.
53
+ // The count and scrollport arrive after mount, so rebinding has to
54
+ // happen here or `getVirtualItems()` stays empty.
55
+ virtualizer._willUpdate();
56
+ virtualizer.measure();
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import { EditorState } from "@codemirror/state";
3
+ import { EditorView } from "@codemirror/view";
4
+ import type { App, TFile } from "@lapis-notes/api";
5
+ import type { Attachment } from "svelte/attachments";
6
+ import {
7
+ DEFAULT_CSV_SETTINGS,
8
+ resolveDelimiter,
9
+ } from "../../csv-settings";
10
+ import { csvSourceExtensions } from "../codemirror/csv-source-extensions";
11
+ import { CsvGrid } from "../model/csv-grid";
12
+ import { csvBodyCounts, isCsvFindShortcut } from "../model/csv-search";
13
+ import CsvFind from "./csv-find.svelte";
14
+ import CsvStatusBar from "./csv-status-bar.svelte";
15
+
16
+ let {
17
+ app,
18
+ file,
19
+ onShowTable,
20
+ onShowCanvas,
21
+ }: {
22
+ app: App;
23
+ file: TFile;
24
+ onShowTable: () => void;
25
+ onShowCanvas?: () => void;
26
+ } = $props();
27
+
28
+ let editor = $state<EditorView | null>(null);
29
+ let text = $state("");
30
+ let revision = $state(0);
31
+ let searchOpen = $state(false);
32
+ let searchRequest = $state(0);
33
+
34
+ const counts = $derived.by(() => {
35
+ if (!text) return { columns: 0, rows: 0 };
36
+ const delimiter = resolveDelimiter(DEFAULT_CSV_SETTINGS, file.extension);
37
+ const grid = CsvGrid.fromText(text, { delimiter, headerRow: true });
38
+ return csvBodyCounts(grid.getRowCount(), grid.getColCount(), true);
39
+ });
40
+
41
+ function openSearch() {
42
+ searchOpen = true;
43
+ searchRequest += 1;
44
+ }
45
+
46
+ function toggleSearch() {
47
+ if (searchOpen) {
48
+ searchOpen = false;
49
+ return;
50
+ }
51
+ openSearch();
52
+ }
53
+
54
+ function onFindShortcut(event: KeyboardEvent) {
55
+ if (!isCsvFindShortcut(event)) return;
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ openSearch();
59
+ }
60
+
61
+ const findShortcut: Attachment<HTMLElement> = (node) => {
62
+ node.addEventListener("keydown", onFindShortcut, true);
63
+ return () => node.removeEventListener("keydown", onFindShortcut, true);
64
+ };
65
+
66
+ const mountEditor: Attachment<HTMLDivElement> = (parent) => {
67
+ let view: EditorView | null = null;
68
+ let disposed = false;
69
+ let writing = false;
70
+ const currentApp = app;
71
+ const currentFile = file;
72
+ void currentApp.vault.cachedRead(currentFile).then((next) => {
73
+ if (disposed) return;
74
+ text = next;
75
+ view = new EditorView({
76
+ parent,
77
+ state: EditorState.create({
78
+ doc: next,
79
+ extensions: [
80
+ csvSourceExtensions(currentApp),
81
+ EditorView.updateListener.of((update) => {
82
+ if (!update.docChanged || writing) return;
83
+ const value = update.state.doc.toString();
84
+ text = value;
85
+ revision += 1;
86
+ writing = true;
87
+ void currentApp.vault.modify(currentFile, value).finally(() => {
88
+ writing = false;
89
+ });
90
+ }),
91
+ ],
92
+ }),
93
+ });
94
+ editor = view;
95
+ revision += 1;
96
+ });
97
+ return () => {
98
+ disposed = true;
99
+ editor = null;
100
+ view?.destroy();
101
+ };
102
+ };
103
+ </script>
104
+
105
+ <div
106
+ class="csv-source-pane"
107
+ data-testid="csv-source"
108
+ data-csv-role="source"
109
+ {@attach findShortcut}
110
+ >
111
+ <CsvFind
112
+ bind:open={searchOpen}
113
+ {searchRequest}
114
+ {editor}
115
+ {revision}
116
+ />
117
+ <div class="csv-source-pane__editor" {@attach mountEditor}></div>
118
+ <CsvStatusBar
119
+ columns={counts.columns}
120
+ rows={counts.rows}
121
+ {searchOpen}
122
+ mode="source"
123
+ onToggleSearch={toggleSearch}
124
+ onToggleMode={onShowTable}
125
+ {onShowCanvas}
126
+ />
127
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { App, TFile } from "@lapis-notes/api";
2
+ type $$ComponentProps = {
3
+ app: App;
4
+ file: TFile;
5
+ onShowTable: () => void;
6
+ onShowCanvas?: () => void;
7
+ };
8
+ declare const CsvSourceView: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type CsvSourceView = ReturnType<typeof CsvSourceView>;
10
+ export default CsvSourceView;