@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,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import FileCodeIcon from "@lucide/svelte/icons/file-code";
|
|
3
|
+
import GridIcon from "@lucide/svelte/icons/grid-3x3";
|
|
4
|
+
import SearchIcon from "@lucide/svelte/icons/search";
|
|
5
|
+
import TableIcon from "@lucide/svelte/icons/table";
|
|
6
|
+
import { csvStatusLabel } from "../model/csv-search";
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
columns,
|
|
10
|
+
rows,
|
|
11
|
+
shownRows = undefined,
|
|
12
|
+
searchOpen = false,
|
|
13
|
+
mode = "table",
|
|
14
|
+
onToggleSearch,
|
|
15
|
+
onToggleMode,
|
|
16
|
+
onShowCanvas,
|
|
17
|
+
onShowTable,
|
|
18
|
+
}: {
|
|
19
|
+
columns: number;
|
|
20
|
+
rows: number;
|
|
21
|
+
shownRows?: number;
|
|
22
|
+
searchOpen?: boolean;
|
|
23
|
+
mode?: "table" | "canvas" | "source";
|
|
24
|
+
onToggleSearch: () => void;
|
|
25
|
+
onToggleMode?: () => void;
|
|
26
|
+
onShowCanvas?: () => void;
|
|
27
|
+
onShowTable?: () => void;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
const label = $derived(csvStatusLabel(columns, rows, shownRows));
|
|
31
|
+
const modeLabel = $derived(
|
|
32
|
+
mode === "source" ? "View CSV table" : "View CSV source",
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<footer class="csv-status-bar" data-testid="csv-status-bar">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class={["csv-status-bar__icon", { "csv-status-bar__icon--pressed": searchOpen }]}
|
|
40
|
+
aria-label="Find and replace"
|
|
41
|
+
aria-pressed={searchOpen}
|
|
42
|
+
title="Find and replace"
|
|
43
|
+
onclick={(event) => {
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
onToggleSearch();
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<SearchIcon aria-hidden="true" />
|
|
49
|
+
</button>
|
|
50
|
+
<span class="csv-status-bar__text">{label}</span>
|
|
51
|
+
{#if onShowCanvas || onShowTable || onToggleMode}
|
|
52
|
+
<div class="csv-status-bar__modes">
|
|
53
|
+
{#if onShowCanvas}
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="csv-status-bar__icon"
|
|
57
|
+
aria-label="View CSV canvas"
|
|
58
|
+
title="View CSV canvas"
|
|
59
|
+
onclick={(event) => {
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
onShowCanvas();
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<GridIcon aria-hidden="true" />
|
|
65
|
+
</button>
|
|
66
|
+
{/if}
|
|
67
|
+
{#if onShowTable}
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="csv-status-bar__icon"
|
|
71
|
+
aria-label="View CSV table"
|
|
72
|
+
title="View CSV table"
|
|
73
|
+
onclick={(event) => {
|
|
74
|
+
event.stopPropagation();
|
|
75
|
+
onShowTable();
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<TableIcon aria-hidden="true" />
|
|
79
|
+
</button>
|
|
80
|
+
{/if}
|
|
81
|
+
{#if onToggleMode}
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="csv-status-bar__icon csv-status-bar__mode"
|
|
85
|
+
aria-label={modeLabel}
|
|
86
|
+
title={modeLabel}
|
|
87
|
+
onclick={(event) => {
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
onToggleMode();
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{#if mode === "source"}
|
|
93
|
+
<TableIcon aria-hidden="true" />
|
|
94
|
+
{:else}
|
|
95
|
+
<FileCodeIcon aria-hidden="true" />
|
|
96
|
+
{/if}
|
|
97
|
+
</button>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
{/if}
|
|
101
|
+
</footer>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
columns: number;
|
|
3
|
+
rows: number;
|
|
4
|
+
shownRows?: number;
|
|
5
|
+
searchOpen?: boolean;
|
|
6
|
+
mode?: "table" | "canvas" | "source";
|
|
7
|
+
onToggleSearch: () => void;
|
|
8
|
+
onToggleMode?: () => void;
|
|
9
|
+
onShowCanvas?: () => void;
|
|
10
|
+
onShowTable?: () => void;
|
|
11
|
+
};
|
|
12
|
+
declare const CsvStatusBar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type CsvStatusBar = ReturnType<typeof CsvStatusBar>;
|
|
14
|
+
export default CsvStatusBar;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type TableDragData, type TableDragSource } from "@lapis-notes/ui/table-dnd/utils";
|
|
2
|
+
export declare const CSV_TABLE_ROW_TYPE: string;
|
|
3
|
+
export declare const CSV_TABLE_COL_TYPE: string;
|
|
4
|
+
export type CsvTableDragType = typeof CSV_TABLE_ROW_TYPE | typeof CSV_TABLE_COL_TYPE;
|
|
5
|
+
export type CsvTableDragSource = TableDragSource;
|
|
6
|
+
export type CsvTableDragData = TableDragData;
|
|
7
|
+
export declare function dropIndicatorClasses(dragSource: CsvTableDragSource | null, dragOverIndex: number | null, rowIndex: number, colIndex: number): string;
|
|
8
|
+
export declare function isRowDragSource(dragSource: CsvTableDragSource | null, rowIndex: number): boolean;
|
|
9
|
+
export declare function isColumnDragSource(dragSource: CsvTableDragSource | null, colIndex: number): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { dropIndicatorClasses as dropIndicatorClassesBase, isColumnDragSource as isColumnDragSourceBase, isRowDragSource as isRowDragSourceBase, tableColDragType, tableRowDragType, } from "@lapis-notes/ui/table-dnd/utils";
|
|
2
|
+
export const CSV_TABLE_ROW_TYPE = tableRowDragType("csv");
|
|
3
|
+
export const CSV_TABLE_COL_TYPE = tableColDragType("csv");
|
|
4
|
+
export function dropIndicatorClasses(dragSource, dragOverIndex, rowIndex, colIndex) {
|
|
5
|
+
return dropIndicatorClassesBase(dragSource, dragOverIndex, rowIndex, colIndex, CSV_TABLE_ROW_TYPE, CSV_TABLE_COL_TYPE);
|
|
6
|
+
}
|
|
7
|
+
export function isRowDragSource(dragSource, rowIndex) {
|
|
8
|
+
return isRowDragSourceBase(dragSource, rowIndex, CSV_TABLE_ROW_TYPE);
|
|
9
|
+
}
|
|
10
|
+
export function isColumnDragSource(dragSource, colIndex) {
|
|
11
|
+
return isColumnDragSourceBase(dragSource, colIndex, CSV_TABLE_COL_TYPE);
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type CsvDragOverlayMetrics = {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
segmentSizes: number[];
|
|
5
|
+
};
|
|
6
|
+
export declare function measureRowOverlayMetrics(table: HTMLElement, rowIndex: number): CsvDragOverlayMetrics | null;
|
|
7
|
+
export declare function measureColumnOverlayMetrics(table: HTMLElement, colIndex: number): CsvDragOverlayMetrics | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function readSize(element) {
|
|
2
|
+
return element.getBoundingClientRect().width;
|
|
3
|
+
}
|
|
4
|
+
function readHeight(element) {
|
|
5
|
+
return element.getBoundingClientRect().height;
|
|
6
|
+
}
|
|
7
|
+
export function measureRowOverlayMetrics(table, rowIndex) {
|
|
8
|
+
const row = table.querySelector(`tr[data-csv-row-index="${rowIndex}"]`);
|
|
9
|
+
if (!row) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const cells = Array.from(row.querySelectorAll("td.csv-data-table__cell:not(.csv-data-table__cell--actions)"));
|
|
13
|
+
if (!cells.length) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const segmentSizes = cells.map(readSize);
|
|
17
|
+
const height = Math.max(...cells.map(readHeight));
|
|
18
|
+
return {
|
|
19
|
+
width: segmentSizes.reduce((total, size) => total + size, 0),
|
|
20
|
+
height,
|
|
21
|
+
segmentSizes,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function measureColumnOverlayMetrics(table, colIndex) {
|
|
25
|
+
const head = table.querySelector(`th[data-csv-col-index="${colIndex}"]`);
|
|
26
|
+
const bodyCells = Array.from(table.querySelectorAll(`td[data-csv-col-index="${colIndex}"]`));
|
|
27
|
+
const segments = head ? [head, ...bodyCells] : bodyCells;
|
|
28
|
+
if (!segments.length) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const segmentSizes = segments.map(readHeight);
|
|
32
|
+
const width = Math.max(...segments.map(readSize));
|
|
33
|
+
return {
|
|
34
|
+
width,
|
|
35
|
+
height: segmentSizes.reduce((total, size) => total + size, 0),
|
|
36
|
+
segmentSizes,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type CsvCellCoord = [row: number, col: number];
|
|
2
|
+
export type CsvSelectionState = {
|
|
3
|
+
start: CsvCellCoord;
|
|
4
|
+
end: CsvCellCoord;
|
|
5
|
+
isMouseDown: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function createSelectionState(): CsvSelectionState;
|
|
8
|
+
export type CsvSelectionRange = {
|
|
9
|
+
minRow: number;
|
|
10
|
+
maxRow: number;
|
|
11
|
+
minCol: number;
|
|
12
|
+
maxCol: number;
|
|
13
|
+
};
|
|
14
|
+
/** Inclusive bounds for a drag, including the anchor cell. */
|
|
15
|
+
export declare function dragSelectionRange(start: CsvCellCoord, end: CsvCellCoord): CsvSelectionRange | null;
|
|
16
|
+
export declare function selectionClassForCoord(range: CsvSelectionRange | null, row: number, col: number): string | undefined;
|
|
17
|
+
export declare function coordFromTarget(target: EventTarget | null): CsvCellCoord;
|
|
18
|
+
export declare function getCellsBetween(start: CsvCellCoord, end: CsvCellCoord): CsvCellCoord[];
|
|
19
|
+
export declare function selectionClassesForCells(selected: CsvCellCoord[]): Record<string, string>;
|
|
20
|
+
export declare function headerSelectionClassForColumn(colIndex: number, selected: CsvCellCoord[], headerRow: boolean, bodyRowIndices: number[]): string | undefined;
|
|
21
|
+
export declare function columnSelection(rowCount: number, column: number): CsvCellCoord[];
|
|
22
|
+
export declare function rowSelection(colCount: number, row: number): CsvCellCoord[];
|
|
23
|
+
export declare function columnSelectionIncludingHeader(gridRowCount: number, column: number, headerRow: boolean, bodyRowIndices: number[]): CsvCellCoord[];
|
|
24
|
+
export declare function fullRowsAndColumns(selectedCells: CsvCellCoord[], rowCount: number, colCount: number): {
|
|
25
|
+
fullRows: number[];
|
|
26
|
+
fullCols: number[];
|
|
27
|
+
};
|
|
28
|
+
export declare function coordsFromCellElement(cell: HTMLElement | null, currentTarget: EventTarget | null): CsvCellCoord;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export function createSelectionState() {
|
|
2
|
+
return { start: [-1, -1], end: [-1, -1], isMouseDown: false };
|
|
3
|
+
}
|
|
4
|
+
/** Inclusive bounds for a drag, including the anchor cell. */
|
|
5
|
+
export function dragSelectionRange(start, end) {
|
|
6
|
+
const [row1, col1] = start;
|
|
7
|
+
const [row2, col2] = end;
|
|
8
|
+
if (row1 < 0 || col1 < 0 || row2 < 0 || col2 < 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
minRow: Math.min(row1, row2),
|
|
13
|
+
maxRow: Math.max(row1, row2),
|
|
14
|
+
minCol: Math.min(col1, col2),
|
|
15
|
+
maxCol: Math.max(col1, col2),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function selectionClassForCoord(range, row, col) {
|
|
19
|
+
if (!range ||
|
|
20
|
+
row < range.minRow ||
|
|
21
|
+
row > range.maxRow ||
|
|
22
|
+
col < range.minCol ||
|
|
23
|
+
col > range.maxCol) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const decorations = ["is-selected"];
|
|
27
|
+
if (row === range.minRow)
|
|
28
|
+
decorations.push("top");
|
|
29
|
+
if (col === range.minCol)
|
|
30
|
+
decorations.push("start");
|
|
31
|
+
if (row === range.maxRow)
|
|
32
|
+
decorations.push("bottom");
|
|
33
|
+
if (col === range.maxCol)
|
|
34
|
+
decorations.push("end");
|
|
35
|
+
return decorations.join(" ");
|
|
36
|
+
}
|
|
37
|
+
export function coordFromTarget(target) {
|
|
38
|
+
if (!(target instanceof Element)) {
|
|
39
|
+
return [-1, -1];
|
|
40
|
+
}
|
|
41
|
+
const cell = target.closest("[data-csv-cell]");
|
|
42
|
+
if (!(cell instanceof HTMLElement) || !cell.dataset.csvCell) {
|
|
43
|
+
return [-1, -1];
|
|
44
|
+
}
|
|
45
|
+
const [row, col] = cell.dataset.csvCell.split(":").map(Number);
|
|
46
|
+
if (!Number.isInteger(row) || !Number.isInteger(col)) {
|
|
47
|
+
return [-1, -1];
|
|
48
|
+
}
|
|
49
|
+
return [row, col];
|
|
50
|
+
}
|
|
51
|
+
export function getCellsBetween(start, end) {
|
|
52
|
+
const [row1, col1] = start;
|
|
53
|
+
const [row2, col2] = end;
|
|
54
|
+
if (row1 < 0 || col1 < 0 || row2 < 0 || col2 < 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const minRow = Math.min(row1, row2);
|
|
58
|
+
const maxRow = Math.max(row1, row2);
|
|
59
|
+
const minCol = Math.min(col1, col2);
|
|
60
|
+
const maxCol = Math.max(col1, col2);
|
|
61
|
+
const cells = [];
|
|
62
|
+
for (let row = minRow; row <= maxRow; row += 1) {
|
|
63
|
+
for (let col = minCol; col <= maxCol; col += 1) {
|
|
64
|
+
cells.push([row, col]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (cells.length === 1) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return cells;
|
|
71
|
+
}
|
|
72
|
+
export function selectionClassesForCells(selected) {
|
|
73
|
+
const data = {};
|
|
74
|
+
if (!selected.length) {
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
let minRow = selected[0][0];
|
|
78
|
+
let maxRow = selected[0][0];
|
|
79
|
+
let minCol = selected[0][1];
|
|
80
|
+
let maxCol = selected[0][1];
|
|
81
|
+
for (const [row, col] of selected) {
|
|
82
|
+
if (row < minRow)
|
|
83
|
+
minRow = row;
|
|
84
|
+
if (row > maxRow)
|
|
85
|
+
maxRow = row;
|
|
86
|
+
if (col < minCol)
|
|
87
|
+
minCol = col;
|
|
88
|
+
if (col > maxCol)
|
|
89
|
+
maxCol = col;
|
|
90
|
+
}
|
|
91
|
+
for (const pos of selected) {
|
|
92
|
+
const id = pos.join(",");
|
|
93
|
+
const [row, col] = pos;
|
|
94
|
+
const decorations = ["is-selected"];
|
|
95
|
+
if (row === minRow) {
|
|
96
|
+
decorations.push("top");
|
|
97
|
+
}
|
|
98
|
+
if (col === minCol) {
|
|
99
|
+
decorations.push("start");
|
|
100
|
+
}
|
|
101
|
+
if (row === maxRow) {
|
|
102
|
+
decorations.push("bottom");
|
|
103
|
+
}
|
|
104
|
+
if (col === maxCol) {
|
|
105
|
+
decorations.push("end");
|
|
106
|
+
}
|
|
107
|
+
data[id] = decorations.join(" ");
|
|
108
|
+
}
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
export function headerSelectionClassForColumn(colIndex, selected, headerRow, bodyRowIndices) {
|
|
112
|
+
if (headerRow) {
|
|
113
|
+
return selectionClassesForCells(selected)[`0,${colIndex}`];
|
|
114
|
+
}
|
|
115
|
+
if (bodyRowIndices.length === 0) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const allBodyRowsSelected = bodyRowIndices.every((rowIndex) => selected.some(([row, col]) => row === rowIndex && col === colIndex));
|
|
119
|
+
if (!allBodyRowsSelected) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return "is-selected top bottom start end";
|
|
123
|
+
}
|
|
124
|
+
export function columnSelection(rowCount, column) {
|
|
125
|
+
return Array.from({ length: rowCount }, (_, row) => [row, column]);
|
|
126
|
+
}
|
|
127
|
+
export function rowSelection(colCount, row) {
|
|
128
|
+
return Array.from({ length: colCount }, (_, col) => [row, col]);
|
|
129
|
+
}
|
|
130
|
+
export function columnSelectionIncludingHeader(gridRowCount, column, headerRow, bodyRowIndices) {
|
|
131
|
+
const coords = [];
|
|
132
|
+
if (headerRow && gridRowCount > 0) {
|
|
133
|
+
coords.push([0, column]);
|
|
134
|
+
}
|
|
135
|
+
for (const rowIndex of bodyRowIndices) {
|
|
136
|
+
coords.push([rowIndex, column]);
|
|
137
|
+
}
|
|
138
|
+
return coords;
|
|
139
|
+
}
|
|
140
|
+
export function fullRowsAndColumns(selectedCells, rowCount, colCount) {
|
|
141
|
+
const rowMap = new Map();
|
|
142
|
+
const colMap = new Map();
|
|
143
|
+
for (const [row, col] of selectedCells) {
|
|
144
|
+
if (!rowMap.has(row))
|
|
145
|
+
rowMap.set(row, new Set());
|
|
146
|
+
if (!colMap.has(col))
|
|
147
|
+
colMap.set(col, new Set());
|
|
148
|
+
rowMap.get(row).add(col);
|
|
149
|
+
colMap.get(col).add(row);
|
|
150
|
+
}
|
|
151
|
+
const fullRows = [];
|
|
152
|
+
const fullCols = [];
|
|
153
|
+
for (const [row, cols] of rowMap.entries()) {
|
|
154
|
+
if (cols.size === colCount) {
|
|
155
|
+
fullRows.push(row);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [col, rows] of colMap.entries()) {
|
|
159
|
+
if (rows.size === rowCount) {
|
|
160
|
+
fullCols.push(col);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { fullRows, fullCols };
|
|
164
|
+
}
|
|
165
|
+
export function coordsFromCellElement(cell, currentTarget) {
|
|
166
|
+
let tableCell = cell;
|
|
167
|
+
while (tableCell && !tableCell.dataset.csvCell) {
|
|
168
|
+
if (tableCell === currentTarget) {
|
|
169
|
+
return [-1, -1];
|
|
170
|
+
}
|
|
171
|
+
tableCell = tableCell.parentElement;
|
|
172
|
+
}
|
|
173
|
+
if (!tableCell?.dataset.csvCell) {
|
|
174
|
+
return [-1, -1];
|
|
175
|
+
}
|
|
176
|
+
const [row, col] = tableCell.dataset.csvCell.split(":").map(Number);
|
|
177
|
+
return [row, col];
|
|
178
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
|
|
3
|
+
import { cn } from "@lapis-notes/ui";
|
|
4
|
+
import type { App, TFile } from "@lapis-notes/api";
|
|
5
|
+
import { onMount } from "svelte";
|
|
6
|
+
import type { Attachment } from "svelte/attachments";
|
|
7
|
+
import { CsvGrid } from "../model/csv-grid";
|
|
8
|
+
import { parseCsvText } from "../model/parse-csv";
|
|
9
|
+
import {
|
|
10
|
+
csvBodyCounts,
|
|
11
|
+
csvCellMatches,
|
|
12
|
+
csvSearchQuery,
|
|
13
|
+
csvShownRowCount,
|
|
14
|
+
isCsvFindShortcut,
|
|
15
|
+
} from "../model/csv-search";
|
|
16
|
+
import {
|
|
17
|
+
resolveDelimiter,
|
|
18
|
+
type CsvPluginSettings,
|
|
19
|
+
type CsvPluginSettingsPatch,
|
|
20
|
+
} from "../../csv-settings";
|
|
21
|
+
import CsvDataTable from "./csv-data-table.svelte";
|
|
22
|
+
import CsvFind from "./csv-find.svelte";
|
|
23
|
+
import { CSV_ROW_HEIGHT } from "./csv-row-virtualizer.svelte";
|
|
24
|
+
import CsvStatusBar from "./csv-status-bar.svelte";
|
|
25
|
+
|
|
26
|
+
type Props = {
|
|
27
|
+
app: App;
|
|
28
|
+
file: TFile;
|
|
29
|
+
content: string;
|
|
30
|
+
settings: CsvPluginSettings;
|
|
31
|
+
class?: string;
|
|
32
|
+
onContentChange?: (text: string) => void;
|
|
33
|
+
onSettingsChange?: (patch: CsvPluginSettingsPatch) => void;
|
|
34
|
+
onToggleSource?: () => void;
|
|
35
|
+
onShowCanvas?: () => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
app,
|
|
40
|
+
file,
|
|
41
|
+
content,
|
|
42
|
+
settings,
|
|
43
|
+
class: className,
|
|
44
|
+
onContentChange,
|
|
45
|
+
onSettingsChange,
|
|
46
|
+
onToggleSource,
|
|
47
|
+
onShowCanvas,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
let scrollViewport = $state<HTMLElement | null>(null);
|
|
51
|
+
let incoming = $state<string | null>(null);
|
|
52
|
+
let searchOpen = $state(false);
|
|
53
|
+
let searchRequest = $state(0);
|
|
54
|
+
let searchValue = $state("");
|
|
55
|
+
let replaceValue = $state("");
|
|
56
|
+
let caseSensitive = $state(false);
|
|
57
|
+
let wholeWord = $state(false);
|
|
58
|
+
let regexp = $state(false);
|
|
59
|
+
let active = $state(0);
|
|
60
|
+
const text = $derived(incoming ?? content);
|
|
61
|
+
const delimiter = $derived(resolveDelimiter(settings, file.extension));
|
|
62
|
+
const parsed = $derived.by(() => {
|
|
63
|
+
const source = text;
|
|
64
|
+
const result = parseCsvText(source, { delimiter });
|
|
65
|
+
return {
|
|
66
|
+
errors: result.errors.map(
|
|
67
|
+
(error) => `Row ${error.row ?? "?"}: ${error.message}`,
|
|
68
|
+
),
|
|
69
|
+
grid:
|
|
70
|
+
result.rows.length > 0
|
|
71
|
+
? CsvGrid.fromText(source, {
|
|
72
|
+
delimiter,
|
|
73
|
+
headerRow: settings.headerRow,
|
|
74
|
+
})
|
|
75
|
+
: CsvGrid.empty(),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const rows = $derived(parsed.grid.getRows());
|
|
80
|
+
const counts = $derived(
|
|
81
|
+
csvBodyCounts(
|
|
82
|
+
parsed.grid.getRowCount(),
|
|
83
|
+
parsed.grid.getColCount(),
|
|
84
|
+
settings.headerRow,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
const searchQuery = $derived(
|
|
88
|
+
csvSearchQuery({
|
|
89
|
+
search: searchValue,
|
|
90
|
+
replace: replaceValue,
|
|
91
|
+
caseSensitive,
|
|
92
|
+
wholeWord,
|
|
93
|
+
regexp,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
const filtering = $derived(
|
|
97
|
+
searchOpen && searchQuery.search.length > 0 && searchQuery.valid,
|
|
98
|
+
);
|
|
99
|
+
const searchMatches = $derived.by(() => {
|
|
100
|
+
if (!filtering) return [];
|
|
101
|
+
return csvCellMatches(rows, searchQuery);
|
|
102
|
+
});
|
|
103
|
+
const matchedRows = $derived(
|
|
104
|
+
new Set(searchMatches.map((match) => match.row)),
|
|
105
|
+
);
|
|
106
|
+
const filteredBodyIndexes = $derived.by(() => {
|
|
107
|
+
if (!filtering) return null;
|
|
108
|
+
const start = settings.headerRow ? 1 : 0;
|
|
109
|
+
const indexes: number[] = [];
|
|
110
|
+
for (let index = start; index < rows.length; index += 1) {
|
|
111
|
+
if (matchedRows.has(index)) indexes.push(index);
|
|
112
|
+
}
|
|
113
|
+
return indexes;
|
|
114
|
+
});
|
|
115
|
+
const shownRows = $derived(
|
|
116
|
+
filtering
|
|
117
|
+
? csvShownRowCount(rows.length, settings.headerRow, searchMatches)
|
|
118
|
+
: undefined,
|
|
119
|
+
);
|
|
120
|
+
const searchCell = $derived.by(() => {
|
|
121
|
+
if (searchMatches.length === 0) return null;
|
|
122
|
+
const match = searchMatches[Math.min(active, searchMatches.length - 1)];
|
|
123
|
+
return match ? { row: match.row, col: match.col } : null;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function handleGridChange(nextGrid: CsvGrid) {
|
|
127
|
+
const next = nextGrid.toText({ delimiter });
|
|
128
|
+
incoming = next;
|
|
129
|
+
onContentChange?.(next);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function replaceCells(
|
|
133
|
+
edits: { row: number; col: number; value: string }[],
|
|
134
|
+
) {
|
|
135
|
+
const next = parsed.grid.clone();
|
|
136
|
+
for (const edit of edits) next.updateCell(edit.row, edit.col, edit.value);
|
|
137
|
+
handleGridChange(next);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function openSearch() {
|
|
141
|
+
searchOpen = true;
|
|
142
|
+
searchRequest += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toggleSearch() {
|
|
146
|
+
if (searchOpen) {
|
|
147
|
+
searchOpen = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
openSearch();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function onFindShortcut(event: KeyboardEvent) {
|
|
154
|
+
if (!isCsvFindShortcut(event)) return;
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
event.stopPropagation();
|
|
157
|
+
openSearch();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const findShortcut: Attachment<HTMLElement> = (node) => {
|
|
161
|
+
node.addEventListener("keydown", onFindShortcut, true);
|
|
162
|
+
return () => node.removeEventListener("keydown", onFindShortcut, true);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const scrollToSearch: Attachment<HTMLElement> = () => {
|
|
166
|
+
const viewport = scrollViewport;
|
|
167
|
+
if (!viewport) return;
|
|
168
|
+
const target = searchCell;
|
|
169
|
+
if (!target) {
|
|
170
|
+
if (filtering) viewport.scrollTop = 0;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (settings.headerRow && target.row === 0) return;
|
|
174
|
+
const bodyIndex = filteredBodyIndexes
|
|
175
|
+
? filteredBodyIndexes.indexOf(target.row)
|
|
176
|
+
: settings.headerRow
|
|
177
|
+
? target.row - 1
|
|
178
|
+
: target.row;
|
|
179
|
+
if (bodyIndex < 0) return;
|
|
180
|
+
const top = Math.max(0, bodyIndex) * CSV_ROW_HEIGHT;
|
|
181
|
+
const bottom = top + CSV_ROW_HEIGHT;
|
|
182
|
+
const viewTop = viewport.scrollTop;
|
|
183
|
+
const viewBottom = viewTop + viewport.clientHeight;
|
|
184
|
+
if (top >= viewTop && bottom <= viewBottom) return;
|
|
185
|
+
viewport.scrollTop = Math.max(
|
|
186
|
+
0,
|
|
187
|
+
top - Math.floor(viewport.clientHeight / 3),
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const searchScrollKey = $derived.by(() => {
|
|
192
|
+
if (!filtering) return "";
|
|
193
|
+
if (!searchCell) return "none";
|
|
194
|
+
const bodyIndex = filteredBodyIndexes?.indexOf(searchCell.row) ?? -1;
|
|
195
|
+
return `${bodyIndex}:${searchCell.col}`;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
onMount(() => {
|
|
199
|
+
const currentApp = app;
|
|
200
|
+
const currentFile = file;
|
|
201
|
+
const modified = currentApp.vault.on("modify", (candidate) => {
|
|
202
|
+
if (candidate.path !== currentFile.path) return;
|
|
203
|
+
void currentApp.vault.cachedRead(currentFile).then((next) => {
|
|
204
|
+
incoming = next;
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
return () => {
|
|
208
|
+
currentApp.vault.offref(modified);
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<div class={cn("csv-table-view", className)} {@attach findShortcut}>
|
|
214
|
+
{#if parsed.errors.length > 0}
|
|
215
|
+
<div class="csv-table-view__errors" role="status">
|
|
216
|
+
{#each parsed.errors as error (error)}
|
|
217
|
+
<p>{error}</p>
|
|
218
|
+
{/each}
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
<CsvFind
|
|
223
|
+
bind:open={searchOpen}
|
|
224
|
+
bind:searchValue
|
|
225
|
+
bind:replaceValue
|
|
226
|
+
bind:caseSensitive
|
|
227
|
+
bind:wholeWord
|
|
228
|
+
bind:regexp
|
|
229
|
+
bind:active
|
|
230
|
+
{searchRequest}
|
|
231
|
+
{rows}
|
|
232
|
+
onReplaceCells={replaceCells}
|
|
233
|
+
/>
|
|
234
|
+
{#key searchScrollKey}
|
|
235
|
+
<span hidden {@attach scrollToSearch}></span>
|
|
236
|
+
{/key}
|
|
237
|
+
|
|
238
|
+
<ScrollArea
|
|
239
|
+
class="csv-table-view__scroll"
|
|
240
|
+
orientation="both"
|
|
241
|
+
bind:viewportRef={scrollViewport}
|
|
242
|
+
>
|
|
243
|
+
<CsvDataTable
|
|
244
|
+
grid={parsed.grid}
|
|
245
|
+
headerRow={settings.headerRow}
|
|
246
|
+
showRowNumbers={settings.showRowNumbers}
|
|
247
|
+
{scrollViewport}
|
|
248
|
+
rowFilter={filtering ? matchedRows : null}
|
|
249
|
+
{searchCell}
|
|
250
|
+
onChange={handleGridChange}
|
|
251
|
+
{onSettingsChange}
|
|
252
|
+
{onToggleSource}
|
|
253
|
+
/>
|
|
254
|
+
</ScrollArea>
|
|
255
|
+
<CsvStatusBar
|
|
256
|
+
columns={counts.columns}
|
|
257
|
+
rows={counts.rows}
|
|
258
|
+
{shownRows}
|
|
259
|
+
{searchOpen}
|
|
260
|
+
mode="table"
|
|
261
|
+
onToggleSearch={toggleSearch}
|
|
262
|
+
onToggleMode={onToggleSource}
|
|
263
|
+
{onShowCanvas}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { App, TFile } from "@lapis-notes/api";
|
|
2
|
+
import { type CsvPluginSettings, type CsvPluginSettingsPatch } from "../../csv-settings";
|
|
3
|
+
type Props = {
|
|
4
|
+
app: App;
|
|
5
|
+
file: TFile;
|
|
6
|
+
content: string;
|
|
7
|
+
settings: CsvPluginSettings;
|
|
8
|
+
class?: string;
|
|
9
|
+
onContentChange?: (text: string) => void;
|
|
10
|
+
onSettingsChange?: (patch: CsvPluginSettingsPatch) => void;
|
|
11
|
+
onToggleSource?: () => void;
|
|
12
|
+
onShowCanvas?: () => void;
|
|
13
|
+
};
|
|
14
|
+
declare const CsvTableView: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type CsvTableView = ReturnType<typeof CsvTableView>;
|
|
16
|
+
export default CsvTableView;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Plugin, type App, type PluginManifest } from "@lapis-notes/api";
|
|
2
|
+
import { CSV_FILENAME_PATTERNS, CSV_PLUGIN_ID, CSV_VIEW_TYPE } from "./ids";
|
|
3
|
+
export { CSV_PLUGIN_ID, CSV_VIEW_TYPE, CSV_FILENAME_PATTERNS };
|
|
4
|
+
export declare class CSVPlugin extends Plugin {
|
|
5
|
+
constructor(app: App, manifest?: PluginManifest);
|
|
6
|
+
onload(): void;
|
|
7
|
+
private openTable;
|
|
8
|
+
}
|
|
9
|
+
export default CSVPlugin;
|