@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE.md +4 -0
- package/README.md +19 -2
- package/dist/app.css +210 -0
- package/dist/csv-settings.d.ts +13 -0
- package/dist/csv-settings.js +28 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/lib/CsvSheet.svelte +58 -0
- package/dist/lib/CsvSheet.svelte.d.ts +11 -0
- package/dist/lib/CsvTable.svelte +58 -0
- package/dist/lib/CsvTable.svelte.d.ts +11 -0
- package/dist/lib/codemirror/csv-language-extensions.d.ts +3 -0
- package/dist/lib/codemirror/csv-language-extensions.js +11 -0
- package/dist/lib/codemirror/csv-language.d.ts +3 -0
- package/dist/lib/codemirror/csv-language.js +24 -0
- package/dist/lib/codemirror/csv-rainbow-theme.d.ts +3 -0
- package/dist/lib/codemirror/csv-rainbow-theme.js +15 -0
- package/dist/lib/codemirror/csv-source-extensions.d.ts +7 -0
- package/dist/lib/codemirror/csv-source-extensions.js +9 -0
- package/dist/lib/codemirror/csv.parser.d.ts +2 -0
- package/dist/lib/codemirror/csv.parser.js +16 -0
- package/dist/lib/codemirror/csv.parser.terms.d.ts +1 -0
- package/dist/lib/codemirror/csv.parser.terms.js +2 -0
- package/dist/lib/codemirror/syntax.grammar +36 -0
- package/dist/lib/components/csv-cell.svelte +137 -0
- package/dist/lib/components/csv-cell.svelte.d.ts +11 -0
- package/dist/lib/components/csv-data-table-column-head.svelte +76 -0
- package/dist/lib/components/csv-data-table-column-head.svelte.d.ts +17 -0
- package/dist/lib/components/csv-data-table-column-header.svelte +159 -0
- package/dist/lib/components/csv-data-table-column-header.svelte.d.ts +23 -0
- package/dist/lib/components/csv-data-table-data-cell.svelte +72 -0
- package/dist/lib/components/csv-data-table-data-cell.svelte.d.ts +18 -0
- package/dist/lib/components/csv-data-table-drag-overlay.svelte +119 -0
- package/dist/lib/components/csv-data-table-drag-overlay.svelte.d.ts +13 -0
- package/dist/lib/components/csv-data-table-row-actions.svelte +47 -0
- package/dist/lib/components/csv-data-table-row-actions.svelte.d.ts +8 -0
- package/dist/lib/components/csv-data-table-row-grip-cell.svelte +102 -0
- package/dist/lib/components/csv-data-table-row-grip-cell.svelte.d.ts +19 -0
- package/dist/lib/components/csv-data-table-row.svelte +38 -0
- package/dist/lib/components/csv-data-table-row.svelte.d.ts +11 -0
- package/dist/lib/components/csv-data-table.svelte +903 -0
- package/dist/lib/components/csv-data-table.svelte.d.ts +21 -0
- package/dist/lib/components/csv-find.svelte +382 -0
- package/dist/lib/components/csv-find.svelte.d.ts +24 -0
- package/dist/lib/components/csv-row-virtualizer.svelte.d.ts +9 -0
- package/dist/lib/components/csv-row-virtualizer.svelte.js +59 -0
- package/dist/lib/components/csv-source-view.svelte +127 -0
- package/dist/lib/components/csv-source-view.svelte.d.ts +10 -0
- package/dist/lib/components/csv-status-bar.svelte +101 -0
- package/dist/lib/components/csv-status-bar.svelte.d.ts +14 -0
- package/dist/lib/components/csv-table-dnd.d.ts +9 -0
- package/dist/lib/components/csv-table-dnd.js +12 -0
- package/dist/lib/components/csv-table-drag-measure.d.ts +7 -0
- package/dist/lib/components/csv-table-drag-measure.js +38 -0
- package/dist/lib/components/csv-table-selection.d.ts +28 -0
- package/dist/lib/components/csv-table-selection.js +178 -0
- package/dist/lib/components/csv-table-view.svelte +265 -0
- package/dist/lib/components/csv-table-view.svelte.d.ts +16 -0
- package/dist/lib/csv-plugin.d.ts +9 -0
- package/dist/lib/csv-plugin.js +107 -0
- package/dist/lib/csv-view-settings.d.ts +43 -0
- package/dist/lib/csv-view-settings.js +86 -0
- package/dist/lib/ids.d.ts +8 -0
- package/dist/lib/ids.js +19 -0
- package/dist/lib/model/csv-column-sort.d.ts +16 -0
- package/dist/lib/model/csv-column-sort.js +81 -0
- package/dist/lib/model/csv-grid.d.ts +31 -0
- package/dist/lib/model/csv-grid.js +161 -0
- package/dist/lib/model/csv-search.d.ts +38 -0
- package/dist/lib/model/csv-search.js +101 -0
- package/dist/lib/model/csv-table-model.d.ts +10 -0
- package/dist/lib/model/csv-table-model.js +18 -0
- package/dist/lib/model/parse-csv.d.ts +10 -0
- package/dist/lib/model/parse-csv.js +13 -0
- package/dist/lib/model/serialize-csv.d.ts +5 -0
- package/dist/lib/model/serialize-csv.js +10 -0
- package/dist/lib/sheet/csv-sheet-toolbar.svelte +244 -0
- package/dist/lib/sheet/csv-sheet-toolbar.svelte.d.ts +20 -0
- package/dist/lib/sheet/csv-sheet-view.svelte +444 -0
- package/dist/lib/sheet/csv-sheet-view.svelte.d.ts +18 -0
- package/dist/lib/sheet/csv-sheet.svelte +1121 -0
- package/dist/lib/sheet/csv-sheet.svelte.d.ts +37 -0
- package/dist/lib/sheet/sheet-glyph.svelte +57 -0
- package/dist/lib/sheet/sheet-glyph.svelte.d.ts +7 -0
- package/dist/lib/sheet/sheet-history.d.ts +14 -0
- package/dist/lib/sheet/sheet-history.js +33 -0
- package/dist/lib/sheet/sheet-layout.d.ts +121 -0
- package/dist/lib/sheet/sheet-layout.js +336 -0
- package/dist/lib/sheet/sheet-paint.d.ts +45 -0
- package/dist/lib/sheet/sheet-paint.js +274 -0
- package/dist/lib/sheet/sheet-theme.d.ts +2 -0
- package/dist/lib/sheet/sheet-theme.js +26 -0
- package/dist/lib/views/csv-view-mode.d.ts +3 -0
- package/dist/lib/views/csv-view-mode.js +6 -0
- package/dist/lib/views/csv-view.d.ts +24 -0
- package/dist/lib/views/csv-view.js +125 -0
- package/dist/styles/csv-find.css +313 -0
- package/dist/styles/csv-table.css +882 -0
- package/dist/styles.css +37 -0
- package/manifest.json +45 -0
- package/package.json +86 -3
- package/registry-content/overview.md +3 -0
- package/registry.json +322 -0
- 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;
|