@lapis-notes/csv 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -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,37 @@
|
|
|
1
|
+
import type { CsvColumnSortDirection } from "../model/csv-column-sort";
|
|
2
|
+
import type { CsvGrid } from "../model/csv-grid";
|
|
3
|
+
type Cell = {
|
|
4
|
+
row: number;
|
|
5
|
+
col: number;
|
|
6
|
+
};
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
grid: CsvGrid;
|
|
9
|
+
headerRow: boolean;
|
|
10
|
+
bodyRows: readonly number[];
|
|
11
|
+
searchCell?: Cell | null;
|
|
12
|
+
sort?: {
|
|
13
|
+
index: number;
|
|
14
|
+
direction: CsvColumnSortDirection;
|
|
15
|
+
} | null;
|
|
16
|
+
widths: number[];
|
|
17
|
+
onWidths: (widths: number[]) => void;
|
|
18
|
+
onCellChange: (row: number, col: number, value: string) => void;
|
|
19
|
+
onSort: (col: number) => void;
|
|
20
|
+
onDeleteRow: (index: number) => void;
|
|
21
|
+
onDeleteColumn: (index: number) => void;
|
|
22
|
+
onMoveColumn: (from: number, to: number) => void;
|
|
23
|
+
onMoveRow: (from: number, to: number) => void;
|
|
24
|
+
onClearColumn: (col: number) => void;
|
|
25
|
+
onClearRow: (row: number) => void;
|
|
26
|
+
onClearCell: (row: number, col: number) => void;
|
|
27
|
+
onClearSheet: () => void;
|
|
28
|
+
onDeleteSheet: () => void;
|
|
29
|
+
canUndo: boolean;
|
|
30
|
+
canRedo: boolean;
|
|
31
|
+
onUndo: () => void;
|
|
32
|
+
onRedo: () => void;
|
|
33
|
+
onHeaderRow: (headerRow: boolean) => void;
|
|
34
|
+
};
|
|
35
|
+
declare const CsvSheet: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
36
|
+
type CsvSheet = ReturnType<typeof CsvSheet>;
|
|
37
|
+
export default CsvSheet;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ArrowDown from "@lucide/svelte/icons/arrow-down";
|
|
3
|
+
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
|
|
4
|
+
import ArrowRight from "@lucide/svelte/icons/arrow-right";
|
|
5
|
+
import ArrowUp from "@lucide/svelte/icons/arrow-up";
|
|
6
|
+
import BetweenVerticalStart from "@lucide/svelte/icons/between-vertical-start";
|
|
7
|
+
import Eraser from "@lucide/svelte/icons/eraser";
|
|
8
|
+
import Palette from "@lucide/svelte/icons/palette";
|
|
9
|
+
import Redo2 from "@lucide/svelte/icons/redo-2";
|
|
10
|
+
import Trash2 from "@lucide/svelte/icons/trash-2";
|
|
11
|
+
import Undo2 from "@lucide/svelte/icons/undo-2";
|
|
12
|
+
import WrapText from "@lucide/svelte/icons/wrap-text";
|
|
13
|
+
import type { SheetCommandId } from "./sheet-layout";
|
|
14
|
+
|
|
15
|
+
let { command }: { command: SheetCommandId | "wrap" | "colours" | "header" | "undo" | "redo" } =
|
|
16
|
+
$props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span class="sheet-glyph" aria-hidden="true">
|
|
20
|
+
{#if command === "move-left"}
|
|
21
|
+
<ArrowLeft />
|
|
22
|
+
{:else if command === "move-right"}
|
|
23
|
+
<ArrowRight />
|
|
24
|
+
{:else if command === "move-up"}
|
|
25
|
+
<ArrowUp />
|
|
26
|
+
{:else if command === "move-down"}
|
|
27
|
+
<ArrowDown />
|
|
28
|
+
{:else if command === "clear"}
|
|
29
|
+
<Eraser />
|
|
30
|
+
{:else if command === "delete"}
|
|
31
|
+
<Trash2 />
|
|
32
|
+
{:else if command === "colours"}
|
|
33
|
+
<Palette />
|
|
34
|
+
{:else if command === "header"}
|
|
35
|
+
<BetweenVerticalStart />
|
|
36
|
+
{:else if command === "undo"}
|
|
37
|
+
<Undo2 />
|
|
38
|
+
{:else if command === "redo"}
|
|
39
|
+
<Redo2 />
|
|
40
|
+
{:else}
|
|
41
|
+
<WrapText />
|
|
42
|
+
{/if}
|
|
43
|
+
</span>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.sheet-glyph {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
flex: none;
|
|
49
|
+
width: 1rem;
|
|
50
|
+
height: 1rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sheet-glyph :global(svg) {
|
|
54
|
+
width: 1rem;
|
|
55
|
+
height: 1rem;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SheetCommandId } from "./sheet-layout";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
command: SheetCommandId | "wrap" | "colours" | "header" | "undo" | "redo";
|
|
4
|
+
};
|
|
5
|
+
declare const SheetGlyph: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type SheetGlyph = ReturnType<typeof SheetGlyph>;
|
|
7
|
+
export default SheetGlyph;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SheetHistory<T> = {
|
|
2
|
+
past: T[];
|
|
3
|
+
future: T[];
|
|
4
|
+
};
|
|
5
|
+
export declare function emptySheetHistory<T>(): SheetHistory<T>;
|
|
6
|
+
export declare function recordSheetEdit<T>(history: SheetHistory<T>, before: T): SheetHistory<T>;
|
|
7
|
+
export declare function undoSheetEdit<T>(history: SheetHistory<T>, current: T): {
|
|
8
|
+
history: SheetHistory<T>;
|
|
9
|
+
value: T;
|
|
10
|
+
} | null;
|
|
11
|
+
export declare function redoSheetEdit<T>(history: SheetHistory<T>, current: T): {
|
|
12
|
+
history: SheetHistory<T>;
|
|
13
|
+
value: T;
|
|
14
|
+
} | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function emptySheetHistory() {
|
|
2
|
+
return { past: [], future: [] };
|
|
3
|
+
}
|
|
4
|
+
export function recordSheetEdit(history, before) {
|
|
5
|
+
const past = [...history.past, before];
|
|
6
|
+
if (past.length > 100)
|
|
7
|
+
past.shift();
|
|
8
|
+
return { past, future: [] };
|
|
9
|
+
}
|
|
10
|
+
export function undoSheetEdit(history, current) {
|
|
11
|
+
const value = history.past.at(-1);
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
return {
|
|
15
|
+
history: {
|
|
16
|
+
past: history.past.slice(0, -1),
|
|
17
|
+
future: [...history.future, current],
|
|
18
|
+
},
|
|
19
|
+
value,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function redoSheetEdit(history, current) {
|
|
23
|
+
const value = history.future.at(-1);
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
history: {
|
|
28
|
+
past: [...history.past, current],
|
|
29
|
+
future: history.future.slice(0, -1),
|
|
30
|
+
},
|
|
31
|
+
value,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export declare const SHEET_ROW_HEIGHT = 28;
|
|
2
|
+
export declare const SHEET_LETTER_HEIGHT = 22;
|
|
3
|
+
export declare const SHEET_HEADER_HEIGHT = 28;
|
|
4
|
+
export declare const SHEET_GUTTER_WIDTH = 48;
|
|
5
|
+
export declare const SHEET_MIN_COLUMN_WIDTH = 48;
|
|
6
|
+
export declare const SHEET_DEFAULT_COLUMN_WIDTH = 120;
|
|
7
|
+
export declare const SHEET_RESIZE_SLOP = 5;
|
|
8
|
+
export declare const SHEET_OVERSCAN = 8;
|
|
9
|
+
export declare const SHEET_SORT_HIT = 40;
|
|
10
|
+
export declare const SHEET_SORT_PAD = 3;
|
|
11
|
+
export declare const SHEET_CELL_PADDING = 16;
|
|
12
|
+
export declare const SHEET_MAX_COLUMN_WIDTH = 960;
|
|
13
|
+
export declare const SHEET_LINE_HEIGHT = 16;
|
|
14
|
+
export declare const SHEET_WRAP_MAX_LINES = 4;
|
|
15
|
+
export type SheetCommandId = "move-left" | "move-right" | "move-up" | "move-down" | "clear" | "delete";
|
|
16
|
+
export type SheetCommand = {
|
|
17
|
+
id: SheetCommandId;
|
|
18
|
+
label: string;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type SheetHit = {
|
|
22
|
+
kind: "corner";
|
|
23
|
+
} | {
|
|
24
|
+
kind: "letter";
|
|
25
|
+
col: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "sort";
|
|
28
|
+
col: number;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "header";
|
|
31
|
+
col: number;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "body";
|
|
34
|
+
index: number;
|
|
35
|
+
col: number;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "row";
|
|
38
|
+
index: number;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "resize";
|
|
41
|
+
col: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function columnLetter(index: number): string;
|
|
44
|
+
export type SheetAddress = {
|
|
45
|
+
kind: "cell";
|
|
46
|
+
row: number;
|
|
47
|
+
col: number;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "column";
|
|
50
|
+
col: number;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "row";
|
|
53
|
+
row: number;
|
|
54
|
+
};
|
|
55
|
+
export declare function parseSheetAddress(input: string): SheetAddress | null;
|
|
56
|
+
/** Rail and address number for a grid row. The first row is 1, including a fixed header. */
|
|
57
|
+
export declare function sheetRowNumber(gridRow: number): number | null;
|
|
58
|
+
/** Grid row for a parsed address row (`A1` is the first row). */
|
|
59
|
+
export declare function gridRowFromAddress(addressRow: number, rowCount: number): number | null;
|
|
60
|
+
export declare function clampColumnWidth(width: number): number;
|
|
61
|
+
export declare function fittedColumnWidth(measure: (text: string) => number, texts: readonly string[]): number;
|
|
62
|
+
export declare function columnWidths(count: number, stored: readonly number[] | undefined): number[];
|
|
63
|
+
export declare function readSessionWidths(path: string, count: number): number[];
|
|
64
|
+
export declare function writeSessionWidths(path: string, widths: readonly number[]): void;
|
|
65
|
+
export declare function columnEdges(widths: readonly number[]): number[];
|
|
66
|
+
export declare function chromeHeight(headerRow: boolean): number;
|
|
67
|
+
export declare function sheetContentSize(widths: readonly number[], bodyCount: number, headerRow: boolean, bodyHeight?: number): {
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
};
|
|
71
|
+
export declare function wrapLines(text: string, innerWidth: number, measure: (value: string) => number): string[];
|
|
72
|
+
export declare function wrappedRowHeight(lineCount: number): number;
|
|
73
|
+
export declare function bodyOffsets(heights: readonly number[]): number[];
|
|
74
|
+
export declare function rowIndexAt(offsets: readonly number[], position: number): number;
|
|
75
|
+
export declare function sheetCommands(target: "column" | "row" | "cell" | "sheet", position: {
|
|
76
|
+
row: number;
|
|
77
|
+
col: number;
|
|
78
|
+
}, bounds: {
|
|
79
|
+
rows: number;
|
|
80
|
+
cols: number;
|
|
81
|
+
}): SheetCommand[];
|
|
82
|
+
export declare function visibleBodyRange(options: {
|
|
83
|
+
scrollTop: number;
|
|
84
|
+
viewportHeight: number;
|
|
85
|
+
bodyCount: number;
|
|
86
|
+
overscan?: number;
|
|
87
|
+
offsets?: readonly number[] | null;
|
|
88
|
+
}): {
|
|
89
|
+
start: number;
|
|
90
|
+
end: number;
|
|
91
|
+
};
|
|
92
|
+
export declare function visibleColumnRange(options: {
|
|
93
|
+
scrollLeft: number;
|
|
94
|
+
viewportWidth: number;
|
|
95
|
+
widths: readonly number[];
|
|
96
|
+
}): {
|
|
97
|
+
start: number;
|
|
98
|
+
end: number;
|
|
99
|
+
};
|
|
100
|
+
export declare function sheetMenuTarget(hit: SheetHit): "column" | "row" | "cell" | "sheet" | null;
|
|
101
|
+
export declare function hitTestSheet(options: {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
scrollLeft: number;
|
|
105
|
+
scrollTop: number;
|
|
106
|
+
widths: readonly number[];
|
|
107
|
+
headerRow: boolean;
|
|
108
|
+
bodyCount: number;
|
|
109
|
+
offsets?: readonly number[] | null;
|
|
110
|
+
}): SheetHit | null;
|
|
111
|
+
export declare function cellDocumentRect(options: {
|
|
112
|
+
col: number;
|
|
113
|
+
top: number;
|
|
114
|
+
widths: readonly number[];
|
|
115
|
+
height: number;
|
|
116
|
+
}): {
|
|
117
|
+
x: number;
|
|
118
|
+
y: number;
|
|
119
|
+
width: number;
|
|
120
|
+
height: number;
|
|
121
|
+
} | null;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
export const SHEET_ROW_HEIGHT = 28;
|
|
2
|
+
export const SHEET_LETTER_HEIGHT = 22;
|
|
3
|
+
export const SHEET_HEADER_HEIGHT = 28;
|
|
4
|
+
export const SHEET_GUTTER_WIDTH = 48;
|
|
5
|
+
export const SHEET_MIN_COLUMN_WIDTH = 48;
|
|
6
|
+
export const SHEET_DEFAULT_COLUMN_WIDTH = 120;
|
|
7
|
+
export const SHEET_RESIZE_SLOP = 5;
|
|
8
|
+
export const SHEET_OVERSCAN = 8;
|
|
9
|
+
export const SHEET_SORT_HIT = 40;
|
|
10
|
+
export const SHEET_SORT_PAD = 3;
|
|
11
|
+
export const SHEET_CELL_PADDING = 16;
|
|
12
|
+
export const SHEET_MAX_COLUMN_WIDTH = 960;
|
|
13
|
+
export const SHEET_LINE_HEIGHT = 16;
|
|
14
|
+
export const SHEET_WRAP_MAX_LINES = 4;
|
|
15
|
+
const sessionWidths = new Map();
|
|
16
|
+
export function columnLetter(index) {
|
|
17
|
+
let value = index + 1;
|
|
18
|
+
let label = "";
|
|
19
|
+
while (value > 0) {
|
|
20
|
+
value -= 1;
|
|
21
|
+
label = String.fromCharCode(65 + (value % 26)) + label;
|
|
22
|
+
value = Math.floor(value / 26);
|
|
23
|
+
}
|
|
24
|
+
return label;
|
|
25
|
+
}
|
|
26
|
+
export function parseSheetAddress(input) {
|
|
27
|
+
const value = input.trim().replaceAll("$", "").toUpperCase();
|
|
28
|
+
if (value.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const cell = /^([A-Z]+)(\d+)$/.exec(value);
|
|
31
|
+
if (cell) {
|
|
32
|
+
const col = columnIndex(cell[1] ?? "");
|
|
33
|
+
const row = Number(cell[2]) - 1;
|
|
34
|
+
if (col == null || row < 0)
|
|
35
|
+
return null;
|
|
36
|
+
return { kind: "cell", row, col };
|
|
37
|
+
}
|
|
38
|
+
if (/^[A-Z]+$/.test(value)) {
|
|
39
|
+
const col = columnIndex(value);
|
|
40
|
+
return col == null ? null : { kind: "column", col };
|
|
41
|
+
}
|
|
42
|
+
if (/^\d+$/.test(value)) {
|
|
43
|
+
const row = Number(value) - 1;
|
|
44
|
+
return row < 0 ? null : { kind: "row", row };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** Rail and address number for a grid row. The first row is 1, including a fixed header. */
|
|
49
|
+
export function sheetRowNumber(gridRow) {
|
|
50
|
+
if (gridRow < 0)
|
|
51
|
+
return null;
|
|
52
|
+
return gridRow + 1;
|
|
53
|
+
}
|
|
54
|
+
/** Grid row for a parsed address row (`A1` is the first row). */
|
|
55
|
+
export function gridRowFromAddress(addressRow, rowCount) {
|
|
56
|
+
if (addressRow < 0 || addressRow >= rowCount)
|
|
57
|
+
return null;
|
|
58
|
+
return addressRow;
|
|
59
|
+
}
|
|
60
|
+
function columnIndex(letters) {
|
|
61
|
+
let value = 0;
|
|
62
|
+
for (const char of letters) {
|
|
63
|
+
const code = char.charCodeAt(0) - 64;
|
|
64
|
+
if (code < 1 || code > 26)
|
|
65
|
+
return null;
|
|
66
|
+
value = value * 26 + code;
|
|
67
|
+
}
|
|
68
|
+
return value - 1;
|
|
69
|
+
}
|
|
70
|
+
export function clampColumnWidth(width) {
|
|
71
|
+
return Math.max(SHEET_MIN_COLUMN_WIDTH, Math.min(SHEET_MAX_COLUMN_WIDTH, Math.round(width)));
|
|
72
|
+
}
|
|
73
|
+
export function fittedColumnWidth(measure, texts) {
|
|
74
|
+
let widest = 0;
|
|
75
|
+
for (const text of texts)
|
|
76
|
+
widest = Math.max(widest, measure(text));
|
|
77
|
+
return clampColumnWidth(widest + SHEET_CELL_PADDING);
|
|
78
|
+
}
|
|
79
|
+
export function columnWidths(count, stored) {
|
|
80
|
+
return Array.from({ length: count }, (_, index) => {
|
|
81
|
+
const width = stored?.[index];
|
|
82
|
+
return width == null ? SHEET_DEFAULT_COLUMN_WIDTH : clampColumnWidth(width);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function readSessionWidths(path, count) {
|
|
86
|
+
return columnWidths(count, sessionWidths.get(path));
|
|
87
|
+
}
|
|
88
|
+
export function writeSessionWidths(path, widths) {
|
|
89
|
+
sessionWidths.set(path, widths.map((width) => clampColumnWidth(width)));
|
|
90
|
+
}
|
|
91
|
+
export function columnEdges(widths) {
|
|
92
|
+
const edges = [SHEET_GUTTER_WIDTH];
|
|
93
|
+
for (const width of widths) {
|
|
94
|
+
edges.push(edges[edges.length - 1] + width);
|
|
95
|
+
}
|
|
96
|
+
return edges;
|
|
97
|
+
}
|
|
98
|
+
export function chromeHeight(headerRow) {
|
|
99
|
+
return SHEET_LETTER_HEIGHT + (headerRow ? SHEET_HEADER_HEIGHT : 0);
|
|
100
|
+
}
|
|
101
|
+
export function sheetContentSize(widths, bodyCount, headerRow, bodyHeight) {
|
|
102
|
+
const edges = columnEdges(widths);
|
|
103
|
+
return {
|
|
104
|
+
width: edges[edges.length - 1] ?? SHEET_GUTTER_WIDTH,
|
|
105
|
+
height: chromeHeight(headerRow) + (bodyHeight ?? bodyCount * SHEET_ROW_HEIGHT),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function wrapLines(text, innerWidth, measure) {
|
|
109
|
+
if (text.length === 0 || innerWidth <= 0)
|
|
110
|
+
return [text];
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (const paragraph of text.split("\n")) {
|
|
113
|
+
const words = paragraph.split(/\s+/).filter((word) => word.length > 0);
|
|
114
|
+
if (words.length === 0) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let line = "";
|
|
119
|
+
for (const word of words) {
|
|
120
|
+
const next = line.length === 0 ? word : `${line} ${word}`;
|
|
121
|
+
if (measure(next) <= innerWidth) {
|
|
122
|
+
line = next;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (line.length > 0)
|
|
126
|
+
lines.push(line);
|
|
127
|
+
if (measure(word) <= innerWidth) {
|
|
128
|
+
line = word;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let chunk = "";
|
|
132
|
+
for (const char of word) {
|
|
133
|
+
const trial = chunk + char;
|
|
134
|
+
if (chunk.length === 0 || measure(trial) <= innerWidth)
|
|
135
|
+
chunk = trial;
|
|
136
|
+
else {
|
|
137
|
+
lines.push(chunk);
|
|
138
|
+
chunk = char;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
line = chunk;
|
|
142
|
+
}
|
|
143
|
+
if (line.length > 0)
|
|
144
|
+
lines.push(line);
|
|
145
|
+
}
|
|
146
|
+
return lines.length > 0 ? lines : [""];
|
|
147
|
+
}
|
|
148
|
+
export function wrappedRowHeight(lineCount) {
|
|
149
|
+
const lines = Math.max(1, Math.min(lineCount, SHEET_WRAP_MAX_LINES));
|
|
150
|
+
return Math.max(SHEET_ROW_HEIGHT, 12 + lines * SHEET_LINE_HEIGHT);
|
|
151
|
+
}
|
|
152
|
+
export function bodyOffsets(heights) {
|
|
153
|
+
const offsets = [0];
|
|
154
|
+
for (const height of heights) {
|
|
155
|
+
offsets.push((offsets[offsets.length - 1] ?? 0) + height);
|
|
156
|
+
}
|
|
157
|
+
return offsets;
|
|
158
|
+
}
|
|
159
|
+
export function rowIndexAt(offsets, position) {
|
|
160
|
+
const last = Math.max(0, offsets.length - 2);
|
|
161
|
+
if (last === 0 || position <= 0)
|
|
162
|
+
return 0;
|
|
163
|
+
let low = 0;
|
|
164
|
+
let high = last;
|
|
165
|
+
while (low < high) {
|
|
166
|
+
const mid = Math.ceil((low + high) / 2);
|
|
167
|
+
if ((offsets[mid] ?? 0) <= position)
|
|
168
|
+
low = mid;
|
|
169
|
+
else
|
|
170
|
+
high = mid - 1;
|
|
171
|
+
}
|
|
172
|
+
return low;
|
|
173
|
+
}
|
|
174
|
+
export function sheetCommands(target, position, bounds) {
|
|
175
|
+
const clear = { id: "clear", label: "Clear", disabled: false };
|
|
176
|
+
const remove = { id: "delete", label: "Delete", disabled: false };
|
|
177
|
+
if (target === "sheet")
|
|
178
|
+
return [clear, remove];
|
|
179
|
+
if (target === "column") {
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
id: "move-left",
|
|
183
|
+
label: "Move left",
|
|
184
|
+
disabled: position.col <= 0,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "move-right",
|
|
188
|
+
label: "Move right",
|
|
189
|
+
disabled: position.col >= bounds.cols - 1,
|
|
190
|
+
},
|
|
191
|
+
clear,
|
|
192
|
+
remove,
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
if (target === "row") {
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
id: "move-up",
|
|
199
|
+
label: "Move up",
|
|
200
|
+
disabled: position.row <= 0,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "move-down",
|
|
204
|
+
label: "Move down",
|
|
205
|
+
disabled: position.row >= bounds.rows - 1,
|
|
206
|
+
},
|
|
207
|
+
clear,
|
|
208
|
+
remove,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
return [clear, remove];
|
|
212
|
+
}
|
|
213
|
+
export function visibleBodyRange(options) {
|
|
214
|
+
const overscan = options.overscan ?? SHEET_OVERSCAN;
|
|
215
|
+
if (options.bodyCount <= 0)
|
|
216
|
+
return { start: 0, end: 0 };
|
|
217
|
+
if (options.offsets && options.offsets.length > 1) {
|
|
218
|
+
const start = Math.max(0, rowIndexAt(options.offsets, options.scrollTop) - overscan);
|
|
219
|
+
const endIndex = rowIndexAt(options.offsets, options.scrollTop + Math.max(options.viewportHeight, SHEET_ROW_HEIGHT));
|
|
220
|
+
return {
|
|
221
|
+
start,
|
|
222
|
+
end: Math.min(options.bodyCount, endIndex + 1 + overscan),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const start = Math.max(0, Math.floor(options.scrollTop / SHEET_ROW_HEIGHT) - overscan);
|
|
226
|
+
const span = Math.ceil(Math.max(options.viewportHeight, SHEET_ROW_HEIGHT) / SHEET_ROW_HEIGHT) +
|
|
227
|
+
overscan * 2;
|
|
228
|
+
return {
|
|
229
|
+
start,
|
|
230
|
+
end: Math.min(options.bodyCount, start + span),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function visibleColumnRange(options) {
|
|
234
|
+
const edges = columnEdges(options.widths);
|
|
235
|
+
const left = options.scrollLeft + SHEET_GUTTER_WIDTH;
|
|
236
|
+
const right = options.scrollLeft + Math.max(options.viewportWidth, 1);
|
|
237
|
+
let start = 0;
|
|
238
|
+
while (start < options.widths.length && edges[start + 1] < left)
|
|
239
|
+
start += 1;
|
|
240
|
+
let end = start;
|
|
241
|
+
while (end < options.widths.length && edges[end] < right)
|
|
242
|
+
end += 1;
|
|
243
|
+
return { start, end: Math.max(end, Math.min(options.widths.length, start + 1)) };
|
|
244
|
+
}
|
|
245
|
+
export function sheetMenuTarget(hit) {
|
|
246
|
+
switch (hit.kind) {
|
|
247
|
+
case "letter":
|
|
248
|
+
case "header":
|
|
249
|
+
case "sort":
|
|
250
|
+
case "resize":
|
|
251
|
+
return "column";
|
|
252
|
+
case "row":
|
|
253
|
+
return "row";
|
|
254
|
+
case "body":
|
|
255
|
+
return "cell";
|
|
256
|
+
case "corner":
|
|
257
|
+
return "sheet";
|
|
258
|
+
default:
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function inSortZone(documentX, edges, col) {
|
|
263
|
+
const right = edges[col + 1];
|
|
264
|
+
if (right == null)
|
|
265
|
+
return false;
|
|
266
|
+
const fromRight = right - documentX;
|
|
267
|
+
return fromRight > SHEET_RESIZE_SLOP && fromRight <= SHEET_SORT_HIT;
|
|
268
|
+
}
|
|
269
|
+
function columnAt(documentX, edges) {
|
|
270
|
+
if (documentX < edges[0])
|
|
271
|
+
return null;
|
|
272
|
+
for (let index = 0; index < edges.length - 1; index += 1) {
|
|
273
|
+
if (documentX < edges[index + 1])
|
|
274
|
+
return index;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
export function hitTestSheet(options) {
|
|
279
|
+
const { x, y, widths } = options;
|
|
280
|
+
if (x < 0 || y < 0 || widths.length === 0)
|
|
281
|
+
return null;
|
|
282
|
+
const edges = columnEdges(widths);
|
|
283
|
+
const chrome = chromeHeight(options.headerRow);
|
|
284
|
+
if (x >= SHEET_GUTTER_WIDTH) {
|
|
285
|
+
const documentX = x + options.scrollLeft;
|
|
286
|
+
for (let col = 0; col < widths.length; col += 1) {
|
|
287
|
+
if (Math.abs(documentX - edges[col + 1]) <= SHEET_RESIZE_SLOP) {
|
|
288
|
+
return { kind: "resize", col };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (x < SHEET_GUTTER_WIDTH && y < SHEET_LETTER_HEIGHT)
|
|
293
|
+
return { kind: "corner" };
|
|
294
|
+
if (y < SHEET_LETTER_HEIGHT) {
|
|
295
|
+
const col = columnAt(x + options.scrollLeft, edges);
|
|
296
|
+
if (col == null)
|
|
297
|
+
return null;
|
|
298
|
+
if (inSortZone(x + options.scrollLeft, edges, col))
|
|
299
|
+
return { kind: "sort", col };
|
|
300
|
+
return { kind: "letter", col };
|
|
301
|
+
}
|
|
302
|
+
if (options.headerRow && y < chrome) {
|
|
303
|
+
if (x < SHEET_GUTTER_WIDTH)
|
|
304
|
+
return { kind: "row", index: -1 };
|
|
305
|
+
const col = columnAt(x + options.scrollLeft, edges);
|
|
306
|
+
if (col == null)
|
|
307
|
+
return null;
|
|
308
|
+
return { kind: "header", col };
|
|
309
|
+
}
|
|
310
|
+
const bodyY = y - chrome + options.scrollTop;
|
|
311
|
+
const offsets = options.offsets;
|
|
312
|
+
let index;
|
|
313
|
+
if (offsets && offsets.length > 1) {
|
|
314
|
+
const total = offsets[offsets.length - 1] ?? 0;
|
|
315
|
+
if (bodyY < 0 || bodyY >= total)
|
|
316
|
+
return null;
|
|
317
|
+
index = rowIndexAt(offsets, bodyY);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
index = Math.floor(bodyY / SHEET_ROW_HEIGHT);
|
|
321
|
+
}
|
|
322
|
+
if (index < 0 || index >= options.bodyCount)
|
|
323
|
+
return null;
|
|
324
|
+
if (x < SHEET_GUTTER_WIDTH)
|
|
325
|
+
return { kind: "row", index };
|
|
326
|
+
const col = columnAt(x + options.scrollLeft, edges);
|
|
327
|
+
return col == null ? null : { kind: "body", index, col };
|
|
328
|
+
}
|
|
329
|
+
export function cellDocumentRect(options) {
|
|
330
|
+
const edges = columnEdges(options.widths);
|
|
331
|
+
const x = edges[options.col];
|
|
332
|
+
const next = edges[options.col + 1];
|
|
333
|
+
if (x == null || next == null)
|
|
334
|
+
return null;
|
|
335
|
+
return { x, y: options.top, width: next - x, height: options.height };
|
|
336
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type SheetPaintTheme = {
|
|
2
|
+
background: string;
|
|
3
|
+
header: string;
|
|
4
|
+
gutter: string;
|
|
5
|
+
text: string;
|
|
6
|
+
muted: string;
|
|
7
|
+
grid: string;
|
|
8
|
+
selection: string;
|
|
9
|
+
accent: string;
|
|
10
|
+
search: string;
|
|
11
|
+
columns: readonly string[];
|
|
12
|
+
headerTints: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
export type SheetPaintModel = {
|
|
15
|
+
widths: readonly number[];
|
|
16
|
+
headerRow: boolean;
|
|
17
|
+
headerLabels: readonly string[];
|
|
18
|
+
rowLabels: readonly string[];
|
|
19
|
+
body: readonly (readonly string[])[];
|
|
20
|
+
scrollLeft: number;
|
|
21
|
+
scrollTop: number;
|
|
22
|
+
viewportWidth: number;
|
|
23
|
+
viewportHeight: number;
|
|
24
|
+
active: {
|
|
25
|
+
index: number;
|
|
26
|
+
col: number;
|
|
27
|
+
header: boolean;
|
|
28
|
+
} | null;
|
|
29
|
+
selection: {
|
|
30
|
+
startIndex: number;
|
|
31
|
+
endIndex: number;
|
|
32
|
+
startCol: number;
|
|
33
|
+
endCol: number;
|
|
34
|
+
} | null;
|
|
35
|
+
search: {
|
|
36
|
+
index: number;
|
|
37
|
+
col: number;
|
|
38
|
+
header: boolean;
|
|
39
|
+
} | null;
|
|
40
|
+
theme: SheetPaintTheme;
|
|
41
|
+
wrap: boolean;
|
|
42
|
+
colours: boolean;
|
|
43
|
+
rowOffsets: readonly number[] | null;
|
|
44
|
+
};
|
|
45
|
+
export declare function paintSheet(canvas: HTMLCanvasElement, model: SheetPaintModel): void;
|