@lembryo/voxsheet 0.0.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/VoxSheet.d.ts +4 -0
- package/dist/VoxSheet.d.ts.map +1 -0
- package/dist/core/clipboard.d.ts +14 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/format.d.ts +15 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/range.d.ts +21 -0
- package/dist/core/range.d.ts.map +1 -0
- package/dist/hooks/useChunks.d.ts +35 -0
- package/dist/hooks/useChunks.d.ts.map +1 -0
- package/dist/hooks/useEditBuffer.d.ts +23 -0
- package/dist/hooks/useEditBuffer.d.ts.map +1 -0
- package/dist/icons.d.ts +4 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1226 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +5 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/platform.d.ts +21 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/types.d.ts +244 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/voxsheet.css +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mao <mao.lembryo@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @lembryo/voxsheet
|
|
2
|
+
|
|
3
|
+
Voxel-style virtual spreadsheet renderer for the web.
|
|
4
|
+
|
|
5
|
+
Render millions of rows smoothly with DOM-based virtual scrolling. Excel-like
|
|
6
|
+
selection, editing, autofill, clipboard, column resize, and host-controlled
|
|
7
|
+
sort / filter / search. React component, peer-dependency model, TypeScript-first,
|
|
8
|
+
no external CSS framework.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @lembryo/voxsheet
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`react` / `react-dom` (>=18) are peer dependencies.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
`VoxSheet` is **controlled and transport-agnostic**: you own the data source
|
|
21
|
+
(`fetchRows`) and the domain state (`sort` / `filters` / `search`), the grid owns
|
|
22
|
+
the viewport, selection, editing buffer, and keyboard.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useCallback, useState } from "react"
|
|
26
|
+
import { VoxSheet } from "@lembryo/voxsheet"
|
|
27
|
+
import type { Column, FetchResult, Query, SortSpec } from "@lembryo/voxsheet"
|
|
28
|
+
import "@lembryo/voxsheet/styles.css"
|
|
29
|
+
|
|
30
|
+
const columns: Column[] = [
|
|
31
|
+
{ name: "id", type: "number" },
|
|
32
|
+
{ name: "name", type: "string" },
|
|
33
|
+
{ name: "salary", type: "number", format: { kind: "number", options: { style: "currency", currency: "USD" } } },
|
|
34
|
+
{ name: "joinedAt", type: "date" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
export function App() {
|
|
38
|
+
const [sort, setSort] = useState<SortSpec[]>([])
|
|
39
|
+
const [total, setTotal] = useState(0)
|
|
40
|
+
|
|
41
|
+
// The grid calls this with a Query (offset/limit + the controlled sort/filters/search)
|
|
42
|
+
// and an AbortSignal it manages for stale-request cancellation.
|
|
43
|
+
const fetchRows = useCallback(async (query: Query, signal: AbortSignal): Promise<FetchResult> => {
|
|
44
|
+
const res = await fetch("/api/rows", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify(query),
|
|
48
|
+
signal,
|
|
49
|
+
})
|
|
50
|
+
const json: FetchResult = await res.json()
|
|
51
|
+
if (typeof json.total === "number") setTotal(json.total)
|
|
52
|
+
return json
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<VoxSheet
|
|
57
|
+
columns={columns}
|
|
58
|
+
totalRows={total}
|
|
59
|
+
fetchRows={fetchRows}
|
|
60
|
+
sort={sort}
|
|
61
|
+
onSortChange={setSort}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Data contract
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type CellValue = string | number | boolean | null
|
|
71
|
+
|
|
72
|
+
type Query = {
|
|
73
|
+
offset: number
|
|
74
|
+
limit: number
|
|
75
|
+
sort: SortSpec[] // multi-column, in priority order
|
|
76
|
+
filters: FilterSpec[] // AND-combined
|
|
77
|
+
search?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type FetchResult = {
|
|
81
|
+
data: CellValue[][] // data[i][j] = row i, column j (aligned to `columns`)
|
|
82
|
+
ids: number[] // stable row id (used to resolve edits on commit)
|
|
83
|
+
ordinals: number[] // display ordinal shown in the row-number gutter
|
|
84
|
+
total?: number // count after filters/search; syncs the scrollbar
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type FetchRowsFn = (query: Query, signal: AbortSignal) => Promise<FetchResult>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A `Column` declares the type that drives formatting, alignment, parsing on edit,
|
|
91
|
+
and sort comparison:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
type Column = {
|
|
95
|
+
name: string // identifier + display label
|
|
96
|
+
type?: "string" | "number" | "date" | "boolean"
|
|
97
|
+
align?: "left" | "right" | "center" // defaults derived from type
|
|
98
|
+
width?: number
|
|
99
|
+
format?: ColumnFormat // Intl options or a function
|
|
100
|
+
editable?: boolean | ((ctx: { row: number }) => boolean)
|
|
101
|
+
validate?: (value: CellValue, ctx: { row: number }) => boolean | string
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Key props
|
|
106
|
+
|
|
107
|
+
| prop | type | notes |
|
|
108
|
+
|---------------------------------|------------------------------------------|--------------------------------------------------------|
|
|
109
|
+
| `columns` | `Column[]` | required |
|
|
110
|
+
| `totalRows` | `number` | required; kept in sync via `FetchResult.total` |
|
|
111
|
+
| `fetchRows` | `FetchRowsFn` | required |
|
|
112
|
+
| `sort` / `filters` / `search` | controlled | reflected in the header / passed to `fetchRows` |
|
|
113
|
+
| `readOnly` | `boolean` | disables editing (copy / select / navigate still work) |
|
|
114
|
+
| `density` | `"compact" \| "normal" \| "comfortable"` | sets row height + font size |
|
|
115
|
+
| `rowHeight` | `number` | overrides density height |
|
|
116
|
+
| `theme` | `"light" \| "dark" \| "system"` | sets `data-vox-theme` |
|
|
117
|
+
| `labels` / `icons` / `platform` | partial overrides | i18n, icon set, clipboard/notify/confirm/saveFile |
|
|
118
|
+
|
|
119
|
+
### Callbacks (host events)
|
|
120
|
+
|
|
121
|
+
A button or affordance is **hidden when its callback is omitted** — e.g. the sort
|
|
122
|
+
button only appears when `onSortChange` is set, the filter button only when
|
|
123
|
+
`onFilterButtonClick` is set, the add-column button only when `onAddColumn` is set,
|
|
124
|
+
and header rename is enabled only when `onColumnRename` is set.
|
|
125
|
+
|
|
126
|
+
`onSortChange`, `onFilterButtonClick(col, anchorRect)`, `onColumnResize`,
|
|
127
|
+
`onColumnRename`, `onAddColumn`, `onCellChange`, `onDirtyChange`, `onAppendRow`,
|
|
128
|
+
`onInsertRow`, `onDeleteRows`, `onAutoFill`, `onSelectionChange`,
|
|
129
|
+
`onSelectionStats`, `onCellKeyDown`, `onError`.
|
|
130
|
+
|
|
131
|
+
### Imperative handle (`ref`)
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
type VoxSheetHandle = {
|
|
135
|
+
scrollToRow(row): void
|
|
136
|
+
scrollToCell(row, col): void
|
|
137
|
+
focusCell(row, col): void
|
|
138
|
+
getSelection(): Selection[]
|
|
139
|
+
setSelection(sel: Selection[]): void
|
|
140
|
+
startEdit(row, col): void
|
|
141
|
+
getLocalEdits(): CellEdit[] // pull uncommitted edits to persist
|
|
142
|
+
clearLocalEdits(): void // call after a successful commit
|
|
143
|
+
undo(): void
|
|
144
|
+
redo(): void
|
|
145
|
+
invalidate(): void // drop cache + refetch
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Commit flow: edits stay in a local layer (dirty highlight, `onCellChange` /
|
|
150
|
+
`onDirtyChange`). The host commits by reading `getLocalEdits()`, resolving each
|
|
151
|
+
`row` to a stable id via the `ids` it captured from `fetchRows`, persisting, then
|
|
152
|
+
calling `clearLocalEdits()`.
|
|
153
|
+
|
|
154
|
+
## Keyboard
|
|
155
|
+
|
|
156
|
+
Arrows / Tab / Enter / Esc navigation, Home/End, PageUp/PageDown, Ctrl+Home/End,
|
|
157
|
+
F2 / direct typing / Delete to edit, `Ctrl+A/C/X/V`, `Ctrl+Z/Y`, Shift+arrows /
|
|
158
|
+
Shift+click to extend, Ctrl+click for multiple ranges. IME is committed on
|
|
159
|
+
`compositionend`. In `readOnly` only copy / select-all / navigation are active.
|
|
160
|
+
|
|
161
|
+
## Styling
|
|
162
|
+
|
|
163
|
+
Self-contained styles under `vox-` classes and `--vox-*` CSS variables; import
|
|
164
|
+
`@lembryo/voxsheet/styles.css`. Override variables (e.g. `--vox-row-height`,
|
|
165
|
+
`--vox-color-accent`) or class rules to theme. Dark mode follows
|
|
166
|
+
`prefers-color-scheme` and can be forced via the `theme` prop.
|
|
167
|
+
|
|
168
|
+
> Not yet implemented in this version: `frozenRows` (the prop is accepted but does
|
|
169
|
+
> not yet render a frozen band) and `frozenColumns` (v2).
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoxSheet.d.ts","sourceRoot":"","sources":["../src/VoxSheet.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAKR,cAAc,EACd,aAAa,EAChB,MAAM,SAAS,CAAA;AAShB,OAAO,cAAc,CAAA;AA80CrB,eAAO,MAAM,QAAQ,0GAA2D,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rect } from "./range";
|
|
2
|
+
/**
|
|
3
|
+
* 選択レンジ群から TSV(Excel 互換タブ区切り)テキストを生成する。
|
|
4
|
+
* - 複数レンジで行範囲が一致 → 列方向に連結
|
|
5
|
+
* - 列範囲が一致 → 行方向に連結
|
|
6
|
+
* - それ以外 → アクティブレンジ(末尾)のみ
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildTsv: (rects: Rect[], getValue: (row: number, col: number) => string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* TSV/Excel 形式のクリップボードテキストを 2 次元配列にパースする。
|
|
11
|
+
* ダブルクオートで囲まれたセル内のタブ・改行・"" を正しく扱う。
|
|
12
|
+
*/
|
|
13
|
+
export declare const parseTsv: (text: string) => string[][];
|
|
14
|
+
//# sourceMappingURL=clipboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/core/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAUnC;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,IAAI,EAAE,EAAE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,KAAG,MAoCxF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAAM,EAAE,EAyD/C,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CellValue, Column, ColumnAlign, ColumnType } from '../types';
|
|
2
|
+
/** 型から既定の水平アラインを導出する。 */
|
|
3
|
+
export declare const defaultAlign: (type: ColumnType | undefined) => ColumnAlign;
|
|
4
|
+
/**
|
|
5
|
+
* セル値を表示文字列に整形する。column.format > 型別既定 の順で解決する。
|
|
6
|
+
*/
|
|
7
|
+
export declare const formatCellValue: (value: CellValue, column: Column, row: number) => string;
|
|
8
|
+
/**
|
|
9
|
+
* 編集の生入力文字列を列の型に応じた CellValue へパースする。
|
|
10
|
+
* パースできない場合は raw(文字列)を返し、検証はホスト/列 validate に委ねる。
|
|
11
|
+
*/
|
|
12
|
+
export declare const parseInputValue: (raw: string, type: ColumnType | undefined) => CellValue;
|
|
13
|
+
/** 数値として解釈できる場合に number を返す(統計用)。 */
|
|
14
|
+
export declare const numericValue: (value: CellValue) => number | null;
|
|
15
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/core/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3E,yBAAyB;AACzB,eAAO,MAAM,YAAY,GAAI,MAAM,UAAU,GAAG,SAAS,KAAG,WAK3D,CAAA;AASD;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,SAAS,EAAE,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,MA2B/E,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,MAAM,UAAU,GAAG,SAAS,KAAG,SAmB3E,CAAA;AAED,qCAAqC;AACrC,eAAO,MAAM,YAAY,GAAI,OAAO,SAAS,KAAG,MAAM,GAAG,IAOxD,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Selection } from "../types";
|
|
2
|
+
/** 正規化済みの矩形(r1<=r2, c1<=c2)。 */
|
|
3
|
+
export type Rect = {
|
|
4
|
+
r1: number;
|
|
5
|
+
r2: number;
|
|
6
|
+
c1: number;
|
|
7
|
+
c2: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const normalizeRect: (sel: Selection) => Rect;
|
|
10
|
+
export declare const normalizeRects: (selections: Selection[]) => Rect[];
|
|
11
|
+
export declare const rectContains: (rect: Rect, row: number, col: number) => boolean;
|
|
12
|
+
export declare const cellInRects: (rects: Rect[], row: number, col: number) => boolean;
|
|
13
|
+
/** 矩形が「行全体(全列を含む)」かどうか。 */
|
|
14
|
+
export declare const isFullRowRect: (rect: Rect, columnCount: number) => boolean;
|
|
15
|
+
/** 矩形が「列全体(全行を含む)」かどうか。 */
|
|
16
|
+
export declare const isFullColRect: (rect: Rect, totalRows: number) => boolean;
|
|
17
|
+
/** レンジ群に含まれる行の和集合(昇順)。 */
|
|
18
|
+
export declare const rowsInRects: (rects: Rect[]) => number[];
|
|
19
|
+
/** レンジ群の総セル数(未取得行も含む論理的なセル数)。 */
|
|
20
|
+
export declare const totalCellCount: (rects: Rect[]) => number;
|
|
21
|
+
//# sourceMappingURL=range.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"range.d.ts","sourceRoot":"","sources":["../../src/core/range.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC,gCAAgC;AAChC,MAAM,MAAM,IAAI,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,KAAG,IAK7C,CAAA;AAEF,eAAO,MAAM,cAAc,GAAI,YAAY,SAAS,EAAE,KAAG,IAAI,EAAmC,CAAA;AAEhG,eAAO,MAAM,YAAY,GAAI,MAAM,IAAI,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,OACI,CAAA;AAExE,eAAO,MAAM,WAAW,GAAI,OAAO,IAAI,EAAE,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,OAChB,CAAA;AAEtD,2BAA2B;AAC3B,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,EAAE,aAAa,MAAM,KAAG,OAChB,CAAA;AAEhD,2BAA2B;AAC3B,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,EAAE,WAAW,MAAM,KAAG,OAChB,CAAA;AAE9C,0BAA0B;AAC1B,eAAO,MAAM,WAAW,GAAI,OAAO,IAAI,EAAE,KAAG,MAAM,EAMjD,CAAA;AAED,iCAAiC;AACjC,eAAO,MAAM,cAAc,GAAI,OAAO,IAAI,EAAE,KAAG,MAC4C,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CellValue, Column, FetchRowsFn, FilterSpec, SortSpec } from "../types";
|
|
2
|
+
export type VisibleRow = {
|
|
3
|
+
values: CellValue[];
|
|
4
|
+
id: number;
|
|
5
|
+
ordinal: number;
|
|
6
|
+
};
|
|
7
|
+
export type ChunkStatus = "idle" | "loading" | "error";
|
|
8
|
+
type Params = {
|
|
9
|
+
fetchRows: FetchRowsFn;
|
|
10
|
+
totalRows: number;
|
|
11
|
+
columns: Column[];
|
|
12
|
+
sort: SortSpec[];
|
|
13
|
+
filters: FilterSpec[];
|
|
14
|
+
search: string | undefined;
|
|
15
|
+
queryKey: unknown;
|
|
16
|
+
frozenRows: number;
|
|
17
|
+
startRow: number;
|
|
18
|
+
endRow: number;
|
|
19
|
+
onError?: (error: unknown, ctx: {
|
|
20
|
+
phase: "fetch" | "commit";
|
|
21
|
+
}) => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* ビューポート範囲をチャンク単位で取得・キャッシュするフック。
|
|
25
|
+
* - sort/filters/search/columns/queryKey の変化でキャッシュを無効化して再取得する。
|
|
26
|
+
* - 取得は debounce し、各取得は AbortSignal で中断可能。
|
|
27
|
+
*/
|
|
28
|
+
export declare const useChunks: (params: Params) => {
|
|
29
|
+
rows: Map<number, VisibleRow>;
|
|
30
|
+
total: number;
|
|
31
|
+
status: ChunkStatus;
|
|
32
|
+
invalidate: () => void;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=useChunks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChunks.d.ts","sourceRoot":"","sources":["../../src/hooks/useChunks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAKpF,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7E,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAEtD,KAAK,MAAM,GAAG;IACV,SAAS,EAAE,WAAW,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,QAAQ,EAAE,CAAA;IAChB,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;QAAE,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;;;CAwIvC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CellEdit, CellValue } from "../types";
|
|
2
|
+
type Params = {
|
|
3
|
+
onCellChange?: (edit: CellEdit) => void;
|
|
4
|
+
onDirtyChange?: (hasChanges: boolean) => void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* ローカル編集レイヤー。未コミットの編集を保持し、Undo/Redo と dirty 追跡を行う。
|
|
8
|
+
* 永続化はしない(ホストが getLocalEdits を取り出してコミットする)。
|
|
9
|
+
*/
|
|
10
|
+
export declare const useEditBuffer: ({ onCellChange, onDirtyChange }: Params) => {
|
|
11
|
+
version: number;
|
|
12
|
+
applyEdits: (edits: CellEdit[], getBase: (r: number, c: number) => CellValue) => void;
|
|
13
|
+
undo: () => void;
|
|
14
|
+
redo: () => void;
|
|
15
|
+
getEditedValue: (row: number, col: number) => CellValue | undefined;
|
|
16
|
+
isDirty: (row: number, col: number) => boolean;
|
|
17
|
+
getLocalEdits: () => CellEdit[];
|
|
18
|
+
clear: () => void;
|
|
19
|
+
canUndo: () => boolean;
|
|
20
|
+
canRedo: () => boolean;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=useEditBuffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEditBuffer.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditBuffer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAMnD,KAAK,MAAM,GAAG;IACV,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;IACvC,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAA;CAChD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,iCAAiC,MAAM;;wBAsCrD,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,SAAS;;;0BAmC3B,MAAM,OAAO,MAAM,KAAG,SAAS,GAAG,SAAS;mBAM1E,MAAM,OAAO,MAAM,KAAG,OAAO;yBAID,QAAQ,EAAE;;;;CAuDnD,CAAA"}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/icons.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAa,YAAY,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAkDvE,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,QAAQ,EAAE,YAAY,CAMxD,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,YAAY,KAAK,KAAG,MAAM,CAAC,QAAQ,EAAE,YAAY,CACZ,CAAA"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("react/jsx-runtime"),o=require("react"),xe=200,xn=80,kn=n=>{const{fetchRows:d,totalRows:l,columns:i,sort:h,filters:u,search:p,queryKey:y,frozenRows:g,startRow:k,endRow:I,onError:O}=n,w=o.useRef(new Map),j=o.useRef(new Set),Y=o.useRef(new Set),[pe,ee]=o.useState(new Map),[K,te]=o.useState(l),[ce,v]=o.useState("idle"),b=JSON.stringify({columns:i.map(H=>H.name),sort:h,filters:u,search:p??null}),x=o.useCallback(()=>{for(const H of Y.current)H.abort();Y.current.clear(),w.current.clear(),j.current.clear(),ee(new Map)},[]);o.useEffect(()=>{x()},[b,y,x]);const W=o.useCallback(()=>{const H=new Map;for(const[ne,ke]of w.current)ke.forEach((Re,ie)=>H.set(ne+ie,Re));ee(H)},[]),N=o.useCallback(async()=>{const H=K>0?K:l,ne=[];g>0&&ne.push(0);const ke=Math.floor(k/xe)*xe,Re=Math.ceil(I/xe)*xe;for(let M=ke;M<Re;M+=xe)M>=0&&ne.push(M);const ie=ne.filter(M=>M<H&&!w.current.has(M)&&!j.current.has(M));if(ie.length===0)return;v("loading");const Ee=ie.map(async M=>{j.current.add(M);const ue=new AbortController;Y.current.add(ue);const Ae=xe;try{const P=await d({offset:M,limit:Ae,sort:h,filters:u,search:p},ue.signal),Se=P.data.map((de,F)=>({values:de,id:P.ids[F]??M+F+1,ordinal:P.ordinals[F]??M+F+1}));w.current.set(M,Se),typeof P.total=="number"&&te(P.total)}catch(P){ue.signal.aborted||(O?.(P,{phase:"fetch"}),v("error"))}finally{j.current.delete(M),Y.current.delete(ue)}});await Promise.all(Ee),W(),v(M=>M==="error"?M:"idle")},[d,h,u,p,k,I,g,K,l,O,W]);o.useEffect(()=>{const H=setTimeout(()=>{N()},xn);return()=>clearTimeout(H)},[N]),o.useEffect(()=>{te(l)},[l]);const ae=o.useCallback(()=>{x(),N()},[x,N]);return{rows:pe,total:K>0?K:l,status:ce,invalidate:ae}},Rn=200,rt=(n,d)=>`${n}:${d}`,En=({onCellChange:n,onDirtyChange:d})=>{const l=o.useRef(new Map),i=o.useRef(new Map),h=o.useRef([]),u=o.useRef([]),[p,y]=o.useState(0),g=o.useCallback(()=>{d?.(l.current.size>0)},[d]),k=o.useCallback((v,b,x,W)=>{const N=rt(v,b);i.current.has(N)||i.current.set(N,W(v,b));const ae=i.current.get(N)??null;x===ae?(l.current.delete(N),i.current.delete(N)):l.current.set(N,x),n?.({row:v,col:b,oldValue:ae,newValue:x})},[n]),I=o.useCallback((v,b)=>{if(v.length!==0){for(const x of v)k(x.row,x.col,x.newValue,b);h.current.push(v),h.current.length>Rn&&h.current.shift(),u.current=[],y(x=>x+1),g()}},[k,g]),O=o.useCallback(()=>null,[]),w=o.useCallback(()=>{const v=h.current.pop();if(v){for(let b=v.length-1;b>=0;b--){const x=v[b];k(x.row,x.col,x.oldValue,O)}u.current.push(v),y(b=>b+1),g()}},[k,O,g]),j=o.useCallback(()=>{const v=u.current.pop();if(v){for(const b of v)k(b.row,b.col,b.newValue,O);h.current.push(v),y(b=>b+1),g()}},[k,O,g]),Y=o.useCallback((v,b)=>{const x=rt(v,b);return l.current.has(x)?l.current.get(x):void 0},[]),pe=o.useCallback((v,b)=>l.current.has(rt(v,b)),[]),ee=o.useCallback(()=>{const v=[];for(const[b,x]of l.current){const[W,N]=b.split(":");v.push({row:Number(W),col:Number(N),oldValue:i.current.get(b)??null,newValue:x})}return v},[]),K=o.useCallback(()=>{l.current.clear(),i.current.clear(),h.current=[],u.current=[],y(v=>v+1),d?.(!1)},[d]),te=o.useCallback(()=>h.current.length>0,[]),ce=o.useCallback(()=>u.current.length>0,[]);return o.useMemo(()=>({version:p,applyEdits:I,undo:w,redo:j,getEditedValue:Y,isDirty:pe,getLocalEdits:ee,clear:K,canUndo:te,canRedo:ce}),[p,I,w,j,Y,pe,ee,K,te,ce])},Sn=n=>n==="number"||n==="date"?"right":n==="boolean"?"center":"left",Lt=n=>{if(n===null||n==="")return null;const d=typeof n=="number"?new Date(n):new Date(String(n));return Number.isNaN(d.getTime())?null:d},Mn=(n,d,l)=>{const i=d.format;if(typeof i=="function")return i(n,{column:d,row:l});if(n===null)return"";if(i&&i.kind==="number"&&typeof n=="number")return new Intl.NumberFormat(void 0,i.options).format(n);if(i&&i.kind==="date"){const h=Lt(n);if(h)return new Intl.DateTimeFormat(void 0,i.options).format(h)}switch(d.type){case"boolean":return n?"✓":"";case"date":{const h=Lt(n);return typeof n=="string"?n:h?h.toISOString().slice(0,10):""}case"number":return String(n);default:return String(n)}},At=(n,d)=>{if(n==="")return d==="string"||d===void 0?"":null;switch(d){case"number":{const l=Number(n.replace(/,/g,""));return Number.isNaN(l)?n:l}case"boolean":{const l=n.trim().toLowerCase();return["true","1","yes","y","✓"].includes(l)?!0:["false","0","no","n",""].includes(l)?!1:n}case"date":return n;default:return n}},Dn=n=>{if(typeof n=="number")return n;if(typeof n=="string"&&n.trim()!==""){const d=Number(n.replace(/,/g,""));return Number.isNaN(d)?null:d}return null},Nn=n=>({r1:Math.min(n.start.row,n.end.row),r2:Math.max(n.start.row,n.end.row),c1:Math.min(n.start.col,n.end.col),c2:Math.max(n.start.col,n.end.col)}),Tn=n=>n.map(Nn),jn=(n,d,l)=>d>=n.r1&&d<=n.r2&&l>=n.c1&&l<=n.c2,Ln=(n,d,l)=>n.some(i=>jn(i,d,l)),lt=n=>n.reduce((d,l)=>d+(l.r2-l.r1+1)*(l.c2-l.c1+1),0),Ht=n=>/[\t\n\r"]/.test(n)?`"${n.replace(/"/g,'""')}"`:n,An=(n,d)=>{if(n.length===0)return"";const l=i=>{const h=[];for(let u=i.r1;u<=i.r2;u++){const p=[];for(let y=i.c1;y<=i.c2;y++)p.push(Ht(d(u,y)));h.push(p.join(" "))}return h};if(n.length>1){const i=n[0];if(n.every(p=>p.r1===i.r1&&p.r2===i.r2)){const p=[...n].sort((g,k)=>g.c1-k.c1),y=[];for(let g=i.r1;g<=i.r2;g++){const k=[];for(const I of p)for(let O=I.c1;O<=I.c2;O++)k.push(Ht(d(g,O)));y.push(k.join(" "))}return y.join(`
|
|
2
|
+
`)}if(n.every(p=>p.c1===i.c1&&p.c2===i.c2))return[...n].sort((y,g)=>y.r1-g.r1).flatMap(l).join(`
|
|
3
|
+
`)}return l(n[n.length-1]).join(`
|
|
4
|
+
`)},Hn=n=>{const d=[];let l=[],i="",h=!1,u=0;const p=()=>{l.push(i),i=""},y=()=>{p(),d.push(l),l=[]};for(;u<n.length;){const g=n[u];if(h){if(g==='"'){if(n[u+1]==='"'){i+='"',u+=2;continue}h=!1,u++;continue}i+=g,u++;continue}if(g==='"'){h=!0,u++;continue}if(g===" "){p(),u++;continue}if(g===`
|
|
5
|
+
`||g==="\r"){g==="\r"&&n[u+1]===`
|
|
6
|
+
`&&u++,y(),u++;continue}i+=g,u++}return(i!==""||l.length>0)&&y(),d},Le=n=>({width:n.size??14,height:n.size??14,viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:1.6,strokeLinecap:"round",strokeLinejoin:"round",className:n.className,"aria-hidden":!0}),Vn=n=>m.jsx("svg",{...Le(n),children:m.jsx("path",{d:"M8 12V4M8 4l-3 3M8 4l3 3"})}),Fn=n=>m.jsx("svg",{...Le(n),children:m.jsx("path",{d:"M8 4v8M8 12l-3-3M8 12l3-3"})}),zn=n=>m.jsx("svg",{...Le(n),opacity:.45,children:m.jsx("path",{d:"M5 6l3-3 3 3M5 10l3 3 3-3"})}),Kn=n=>m.jsx("svg",{...Le(n),children:m.jsx("path",{d:"M2.5 3.5h11l-4.2 5v4l-2.6 1.3v-5.3z"})}),$n=n=>m.jsx("svg",{...Le(n),fill:"currentColor",stroke:"none",children:m.jsx("path",{d:"M2.5 3.5h11l-4.2 5v4l-2.6 1.3v-5.3z"})}),ct={sortAscending:Vn,sortDescending:Fn,sortUnsorted:zn,filter:Kn,filterActive:$n},On=n=>n?{...ct,...n}:ct,at={loading:"Loading…",empty:"No rows",contextCut:"Cut",contextCopy:"Copy",contextPaste:"Paste",contextInsertRowAbove:"Insert row above",contextInsertRowBelow:"Insert row below",contextDeleteRows:"Delete rows",contextUndo:"Undo",contextRedo:"Redo",confirmLargeCopyTitle:"Copy a large selection",confirmLargeCopyMessage:"The selection is large and copying may take a while. Continue?",confirmOk:"OK",confirmCancel:"Cancel"},Pn=n=>n?{...at,...n}:at,it=()=>typeof document<"u",Un=async()=>typeof navigator<"u"&&navigator.clipboard?navigator.clipboard.readText():"",Wn=async n=>{typeof navigator<"u"&&navigator.clipboard&&await navigator.clipboard.writeText(n)};let Bn=0;const _n=()=>{if(!it())return null;let n=document.querySelector(".vox-toast-container");return n||(n=document.createElement("div"),n.className="vox-toast-container",document.body.appendChild(n)),n},In=(n,d,l)=>{const i=_n(),h=l?.id??`vox-toast-${++Bn}`;if(!i)return h;let u=document.getElementById(h);u||(u=document.createElement("div"),u.id=h,i.appendChild(u)),u.className=`vox-toast vox-toast--${n}`,u.textContent=d;const p=l?.durationMs??(n==="loading"?0:3e3);return p>0&&window.setTimeout(()=>u?.remove(),p),h},qn=n=>it()?new Promise(d=>{const l=document.createElement("div");l.className="vox-modal-backdrop";const i=document.createElement("div");i.className="vox-modal",i.setAttribute("role","dialog"),i.setAttribute("aria-modal","true");const h=k=>{l.remove(),d(k)};if(n.title){const k=document.createElement("div");k.className="vox-modal-title",k.textContent=n.title,i.appendChild(k)}const u=document.createElement("div");u.className="vox-modal-body",u.textContent=n.message,i.appendChild(u);const p=document.createElement("div");p.className="vox-modal-footer";const y=document.createElement("button");y.className="vox-btn",y.textContent=n.cancelLabel??"Cancel",y.onclick=()=>h(!1);const g=document.createElement("button");g.className="vox-btn vox-btn--primary",g.textContent=n.confirmLabel??"OK",g.onclick=()=>h(!0),p.append(y,g),i.appendChild(p),l.appendChild(i),l.onclick=k=>{k.target===l&&h(!1)},document.body.appendChild(l),g.focus()}):Promise.resolve(!1),Xn=async n=>{if(!it())return;const d=typeof n.data=="string"?new Blob([n.data],{type:n.mimeType??"text/plain"}):n.data,l=URL.createObjectURL(d),i=document.createElement("a");i.href=l,i.download=n.suggestedName,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(l)},Yn=n=>({readText:n?.clipboard?.readText??Un,writeText:n?.clipboard?.writeText??Wn,notify:n?.notify??In,confirm:n?.confirm??qn,saveFile:n?.saveFile??Xn}),Vt={compact:{rowHeight:22,fontSize:12},normal:{rowHeight:28,fontSize:13},comfortable:{rowHeight:34,fontSize:15}},je=52,Ft=8,Jn=40,Qn=600,st=1e5,Gn=n=>n===null?"":String(n),Zn=(n,d)=>{const{columns:l,totalRows:i,fetchRows:h,queryKey:u,density:p="normal",defaultColumnWidth:y=120,frozenRows:g=0,theme:k="system",className:I,style:O,readOnly:w=!1,sort:j=[],filters:Y=[],search:pe,searchHighlights:ee,currentSearchHit:K,renderLoading:te,renderEmpty:ce,labels:v,icons:b,platform:x,onSortChange:W,onFilterButtonClick:N,onColumnResize:ae,onColumnRename:H,onAddColumn:ne,onCellChange:ke,onDirtyChange:Re,onAppendRow:ie,onInsertRow:Ee,onDeleteRows:M,onAutoFill:ue,onSelectionChange:Ae,onSelectionStats:P,onCellKeyDown:Se,onError:de}=n,F=o.useMemo(()=>Pn(v),[v]),Me=o.useMemo(()=>On(b),[b]),fe=o.useMemo(()=>Yn(x),[x]),E=n.rowHeight??Vt[p].rowHeight,Ue=Vt[p].fontSize,ut=o.useRef(null),B=o.useRef(null),We=o.useRef(null),dt=o.useRef(null),[ft,zt]=o.useState(0),[He,Kt]=o.useState(600),[$t,Ot]=o.useState(0),[ht,mt]=o.useState([]),pt=o.useRef(new Map),gt=o.useRef(null);o.useEffect(()=>{const e=pt.current,t=gt.current;t&&t.length===l.length&&t.forEach((r,c)=>{const a=l[c]?.name;a&&a!==r&&e.has(r)&&(e.set(a,e.get(r)),e.delete(r))}),gt.current=l.map(r=>r.name),mt(l.map(r=>e.get(r.name)??r.width??y))},[l,y]);const J=o.useCallback(e=>ht[e]??l[e]?.width??y,[ht,l,y]),Be=o.useCallback(e=>{let t=je;for(let r=0;r<e;r++)t+=J(r);return t},[J]),Ve=o.useCallback((e,t)=>{const r=Math.max(Jn,Math.round(t));mt(a=>{const s=[...a];return s[e]=r,s});const c=l[e]?.name;c&&pt.current.set(c,r),ae?.(e,r)},[l,ae]);o.useEffect(()=>{const e=B.current;if(!e)return;const t=()=>{Kt(e.clientHeight),Ot(e.offsetWidth-e.clientWidth)};t();const r=new ResizeObserver(()=>t());return r.observe(e),()=>r.disconnect()},[]);const Q=0,vt=Math.max(0,Math.floor(ft/E)-Ft),wt=Math.ceil((ft+He)/E)+Ft,{rows:ge,total:T,status:bt,invalidate:yt}=kn({fetchRows:h,totalRows:i,columns:l,sort:j,filters:Y,search:pe,queryKey:u,frozenRows:g,startRow:vt,endRow:wt,onError:de}),Pt=Math.min(vt,Math.max(0,T-1)),Ut=Math.min(T,wt),L=En({onCellChange:ke,onDirtyChange:Re}),Fe=o.useCallback((e,t)=>ge.get(e)?.values[t]??null,[ge]),A=o.useCallback((e,t)=>{const r=L.getEditedValue(e,t);return r!==void 0?r:Fe(e,t)},[L,Fe]),De=o.useCallback((e,t)=>{const r=l[t];return r?Mn(A(e,t),r,e):""},[l,A]),oe=o.useCallback(e=>L.applyEdits(e,Fe),[L,Fe]),[Z,ze]=o.useState([]),[C,G]=o.useState(null),S=o.useMemo(()=>Tn(Z),[Z]),q=S.length>0?S[S.length-1]:null,U=Z.length>0?Z[Z.length-1]:null,V=o.useCallback(e=>{ze(e?[e]:[])},[]),$=o.useCallback(e=>{ze(t=>t.length>0?[...t.slice(0,-1),e]:[e])},[]),ve=o.useCallback(e=>{ze(t=>[...t,e])},[]),_e=o.useRef(!1),Ke=o.useRef(!1),Ie=o.useRef(!1),re=o.useRef(null),we=o.useRef(null),[R,$e]=o.useState(null),[qe,Ct]=o.useState(""),Oe=o.useRef(null),Xe=o.useRef(!1),[X,Ye]=o.useState(null),[Je,xt]=o.useState(""),Pe=o.useRef(null),[be,kt]=o.useState(!1),[he,Rt]=o.useState(null),[Qe,Et]=o.useState(null),St=o.useRef(0),Mt=o.useRef(0),[Ge,ye]=o.useState(null),le=o.useCallback((e,t)=>{if(w)return!1;const r=l[e]?.editable;return r===void 0?!0:typeof r=="function"?r({row:t}):r},[l,w]),Wt=o.useMemo(()=>{const e=new Set;return ee?.forEach(t=>e.add(`${t.row}:${t.col}`)),e},[ee]);o.useEffect(()=>{const e=B.current;if(!K||!e)return;const t=Math.max(0,(K.row-Q)*E),r=e.clientHeight,c=e.scrollTop;(t<c||t>c+r-E)&&(e.scrollTop=Math.max(0,t-r/3))},[K,E,Q]),o.useEffect(()=>{Ae?.(Z)},[Z,Ae]),o.useEffect(()=>{if(!P)return;if(S.length===0){P(null);return}const e=lt(S);if(e>st){P({count:e,numericCount:0,sum:null,average:null});return}let t=0,r=0;for(const c of S)for(let a=c.r1;a<=c.r2;a++)for(let s=c.c1;s<=c.c2;s++){const f=Dn(A(a,s));f!==null&&(r+=f,t++)}P({count:e,numericCount:t,sum:t>0?r:null,average:t>0?r/t:null})},[S,ge,L.version,P,A]);const Bt=o.useCallback(()=>{const e=B.current;e&&(We.current&&(We.current.scrollLeft=e.scrollLeft),dt.current&&(dt.current.scrollLeft=e.scrollLeft),zt(e.scrollTop))},[]),me=o.useCallback((e,t,r)=>{le(t,e)&&(ye(null),$e({row:e,col:t}),Ct(r??Gn(A(e,t))))},[le,A]),Dt=o.useCallback(()=>$e(null),[]),_=o.useCallback(()=>{if(!R)return;const{row:e,col:t}=R,r=l[t];if($e(null),!r)return;const c=A(e,t),a=At(qe,r.type);if(r.validate){const s=r.validate(a,{row:e});if(s===!1||typeof s=="string"){fe.notify("error",typeof s=="string"?s:"Invalid value");return}}a!==c&&oe([{row:e,col:t,oldValue:c,newValue:a}])},[R,l,qe,A,oe,fe]);o.useEffect(()=>{R&&Oe.current&&(Oe.current.focus(),Oe.current.select())},[R]);const _t=o.useCallback((e,t,r)=>{R&&R.row===e&&R.col===t||(R&&_(),ye(null),r.shiftKey&&C?$({start:C,end:{row:e,col:t}}):r.ctrlKey||r.metaKey?(G({row:e,col:t}),ve({start:{row:e,col:t},end:{row:e,col:t}})):(G({row:e,col:t}),V({start:{row:e,col:t},end:{row:e,col:t}})),_e.current=!0)},[C,R,_,V,$,ve]),It=o.useCallback((e,t)=>{_e.current&&C&&$({start:C,end:{row:e,col:t}}),Ke.current&&re.current!==null&&$({start:{row:re.current,col:0},end:{row:e,col:l.length-1}}),be&&Rt(e)},[C,be,l.length,$]),Nt=o.useCallback(()=>{if(!U||he===null)return;const e=Math.min(U.start.row,U.end.row),t=Math.max(U.start.row,U.end.row),r=Math.min(U.start.col,U.end.col),c=Math.max(U.start.col,U.end.col),a=Math.max(t,he);if(a<=t)return;const s=[];for(let f=t+1;f<=a;f++){const D=e+(f-e)%(t-e+1);for(let se=r;se<=c;se++)le(se,f)&&s.push({row:f,col:se,oldValue:A(f,se),newValue:A(D,se)})}oe(s),V({start:{row:e,col:r},end:{row:a,col:c}}),ue?.({sourceRange:{start:{row:e,col:r},end:{row:t,col:c}},direction:"down",toEnd:!1})},[U,he,le,A,oe,V,ue]);o.useEffect(()=>{const e=()=>{_e.current=!1,Ke.current=!1,Ie.current=!1,be&&he!==null&&Nt(),kt(!1),Rt(null)};return window.addEventListener("mouseup",e),()=>window.removeEventListener("mouseup",e)},[be,he,Nt]);const qt=o.useCallback(e=>{e.preventDefault(),e.stopPropagation(),kt(!0)},[]),Ne=o.useCallback((e,t)=>({start:{row:e,col:0},end:{row:t,col:l.length-1}}),[l.length]),Xt=o.useCallback((e,t)=>{R&&_(),ye(null),t.shiftKey&&re.current!==null?$(Ne(re.current,e)):t.ctrlKey||t.metaKey?(re.current=e,G({row:e,col:0}),ve(Ne(e,e))):(re.current=e,G({row:e,col:0}),V(Ne(e,e))),Ke.current=!0},[R,_,Ne,V,$,ve]),Ce=o.useCallback((e,t)=>({start:{row:0,col:e},end:{row:T-1,col:t}}),[T]),Yt=o.useCallback((e,t)=>{X===null&&(R&&_(),ye(null),t.shiftKey&&we.current!==null?$(Ce(we.current,e)):t.ctrlKey||t.metaKey?(we.current=e,G({row:0,col:e}),ve(Ce(e,e))):(we.current=e,G({row:0,col:e}),V(Ce(e,e))),Ie.current=!0)},[X,R,_,Ce,V,$,ve]),Jt=o.useCallback(e=>{Ie.current&&we.current!==null&&$(Ce(we.current,e))},[Ce,$]),Ze=o.useCallback(()=>{R&&_(),G({row:0,col:0}),V({start:{row:0,col:0},end:{row:T-1,col:l.length-1}})},[R,_,T,l.length,V]),Qt=o.useCallback(e=>{H&&($e(null),V(null),G(null),Ye(e),xt(l[e]?.name??""))},[H,l,V]),Tt=o.useCallback(()=>{if(X===null)return;const e=Je.trim();e&&e!==l[X]?.name&&H?.(X,e),Ye(null)},[X,Je,l,H]);o.useEffect(()=>{X!==null&&Pe.current&&(Pe.current.focus(),Pe.current.select())},[X]);const Gt=o.useCallback(e=>{const t=l[e]?.name;if(!t||!W)return;const r=j.find(a=>a.column===t);let c;r?r.direction==="asc"?c=j.map(a=>a.column===t?{...a,direction:"desc"}:a):c=j.filter(a=>a.column!==t):c=[...j,{column:t,direction:"asc"}],W(c)},[l,W,j]),Zt=o.useCallback((e,t)=>{if(t.stopPropagation(),!N)return;const r=t.currentTarget.getBoundingClientRect();N(e,r)},[N]),en=o.useCallback((e,t)=>{t.preventDefault(),t.stopPropagation(),Et(e),St.current=t.clientX,Mt.current=J(e)},[J]);o.useEffect(()=>{if(Qe===null)return;const e=r=>{Ve(Qe,Mt.current+(r.clientX-St.current))},t=()=>Et(null);return window.addEventListener("mousemove",e),window.addEventListener("mouseup",t),()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)}},[Qe,Ve]);const et=o.useRef(null),tn=o.useCallback(e=>{et.current||(et.current=document.createElement("canvas"));const t=et.current.getContext("2d");if(!t)return;const r=B.current?.querySelector(".vox-cell");if(r){const a=getComputedStyle(r);t.font=`${a.fontWeight} ${a.fontSize} ${a.fontFamily}`}else t.font=`${Ue}px sans-serif`;let c=t.measureText(l[e]?.name??"").width+48;ge.forEach((a,s)=>{const f=De(s,e);f&&(c=Math.max(c,t.measureText(f).width))}),Ve(e,Math.min(Qn,c+16))},[l,ge,De,Ve,Ue]),Te=o.useCallback(async()=>{if(S.length!==0){if(lt(S)>st){fe.notify("error","Selection is too large to copy");return}try{await fe.writeText(An(S,De))}catch(e){de?.(e,{phase:"commit"})}}},[S,fe,De,de]),tt=o.useCallback(async()=>{if(w||(await Te(),S.length===0||lt(S)>st))return;const e=[],t=new Set;for(const r of S)for(let c=r.r1;c<=r.r2;c++)for(let a=r.c1;a<=r.c2;a++){const s=`${c}:${a}`;t.has(s)||!le(a,c)||(t.add(s),e.push({row:c,col:a,oldValue:A(c,a),newValue:null}))}oe(e)},[w,Te,S,le,A,oe]),nt=o.useCallback(async()=>{if(!(w||!C))try{const e=await fe.readText(),t=Hn(e),r=[];for(let c=0;c<t.length;c++){const a=t[c];for(let s=0;s<a.length;s++){const f=C.row+c,D=C.col+s;f>=T||D>=l.length||!le(D,f)||r.push({row:f,col:D,oldValue:A(f,D),newValue:At(a[s]??"",l[D]?.type)})}}oe(r)}catch(e){de?.(e,{phase:"commit"})}},[w,C,fe,T,l,le,A,oe,de]),z=o.useCallback((e,t,r)=>{const c=Math.max(0,Math.min(T-1,e)),a=Math.max(0,Math.min(l.length-1,t));G({row:c,col:a}),r&&U?$({start:U.start,end:{row:c,col:a}}):V({start:{row:c,col:a},end:{row:c,col:a}});const s=B.current;if(s){const f=(c-Q)*E;f<s.scrollTop?s.scrollTop=f:f+E>s.scrollTop+s.clientHeight&&(s.scrollTop=f+E-s.clientHeight)}},[T,l.length,U,$,V,Q,E]),nn=o.useCallback(e=>{if(X!==null)return;if(C&&Se){const s=l[C.col];if(s&&(Se(e,{row:C.row,col:C.col,value:A(C.row,C.col),column:s}),e.defaultPrevented))return}const t=e.ctrlKey||e.metaKey;if(t&&!R){if(e.key==="a"){e.preventDefault(),Ze();return}if(e.key==="c"){e.preventDefault(),Te();return}if(!w&&e.key==="x"){e.preventDefault(),tt();return}if(!w&&e.key==="v"){e.preventDefault(),nt();return}if(!w&&e.key==="z"){e.preventDefault(),L.undo();return}if(!w&&e.key==="y"){e.preventDefault(),L.redo();return}}if(R){e.key==="Enter"&&!Xe.current?(e.preventDefault(),_(),z(R.row+1,R.col,!1)):e.key==="Escape"?(e.preventDefault(),Dt()):e.key==="Tab"&&(e.preventDefault(),_(),z(R.row,R.col+(e.shiftKey?-1:1),!1));return}if(!C||w&&!e.key.startsWith("Arrow")&&e.key!=="Tab")return;const{row:r,col:c}=C,a=e.shiftKey&&e.key.startsWith("Arrow");e.key==="ArrowDown"?(e.preventDefault(),z(r+1,c,a)):e.key==="ArrowUp"?(e.preventDefault(),z(r-1,c,a)):e.key==="ArrowRight"?(e.preventDefault(),z(r,c+1,a)):e.key==="ArrowLeft"?(e.preventDefault(),z(r,c-1,a)):e.key==="Tab"?(e.preventDefault(),z(r,c+(e.shiftKey?-1:1),!1)):e.key==="Enter"?(e.preventDefault(),r>=T-1?ie?.(T):z(r+1,c,!1)):e.key==="Home"?(e.preventDefault(),z(r,0,a),t&&z(0,0,a)):e.key==="End"?(e.preventDefault(),z(t?T-1:r,l.length-1,a)):e.key==="PageDown"?(e.preventDefault(),z(r+Math.floor(He/E),c,a)):e.key==="PageUp"?(e.preventDefault(),z(r-Math.floor(He/E),c,a)):!w&&(e.key==="Delete"||e.key==="Backspace")?(e.preventDefault(),me(r,c,"")):!w&&e.key==="F2"?(e.preventDefault(),me(r,c)):!w&&e.key.length===1&&!t&&!e.altKey&&me(r,c,e.key)},[X,C,Se,l,A,R,w,Ze,Te,tt,nt,L,_,Dt,z,T,ie,He,E,me]),on=o.useCallback(e=>{if(w){e.preventDefault();return}e.preventDefault(),ye({x:e.clientX,y:e.clientY})},[w]),rn=o.useCallback(()=>{if(S.length===0)return;const e=new Set;for(const t of S)for(let r=t.r1;r<=t.r2;r++)e.add(r);M?.(Array.from(e).sort((t,r)=>t-r))},[S,M]);o.useImperativeHandle(d,()=>({scrollToRow:e=>{B.current&&(B.current.scrollTop=Math.max(0,(e-Q)*E))},scrollToCell:(e,t)=>{B.current&&(B.current.scrollTop=Math.max(0,(e-Q)*E));let r=0;for(let c=0;c<t;c++)r+=J(c);B.current&&(B.current.scrollLeft=r)},focusCell:(e,t)=>{G({row:e,col:t}),V({start:{row:e,col:t},end:{row:e,col:t}}),ut.current?.focus()},getSelection:()=>Z,setSelection:e=>ze(e),startEdit:(e,t)=>me(e,t),getLocalEdits:()=>L.getLocalEdits(),clearLocalEdits:()=>L.clear(),undo:()=>L.undo(),redo:()=>L.redo(),invalidate:()=>yt()}),[Q,E,J,Z,V,me,L,yt]);const ln=je+l.reduce((e,t,r)=>e+J(r),0),sn=Math.max(0,T-Q)*E,cn=q?q.r2:null,an=q?q.c2:null,un=e=>{if(w||!W&&!N)return null;const t=l[e]?.name??"",r=j.findIndex(D=>D.column===t),c=r>=0?j[r]:void 0,a=Y.some(D=>D.column===t),s=c?c.direction==="asc"?Me.sortAscending:Me.sortDescending:Me.sortUnsorted,f=a?Me.filterActive:Me.filter;return m.jsxs("span",{className:"vox-header-actions",onMouseDown:D=>D.stopPropagation(),children:[N&&m.jsx("button",{type:"button",className:"vox-header-btn",title:"Filter","aria-label":`Filter ${t}`,onClick:D=>Zt(e,D),children:m.jsx(f,{size:13})}),W&&m.jsxs("button",{type:"button",className:"vox-header-btn",title:"Sort","aria-label":`Sort ${t}`,onClick:D=>{D.stopPropagation(),Gt(e)},children:[m.jsx(s,{size:13}),c&&j.length>1&&m.jsx("sup",{children:r+1})]})]})},dn=(e,t)=>{const r=ge.get(e),c=S.some(s=>e>=s.r1&&e<=s.r2&&s.c1===0&&s.c2===l.length-1),a=r?Math.floor(r.ordinal):e+1;return m.jsxs("div",{className:"vox-row",style:{top:t,height:E},"aria-rowindex":e+1,children:[m.jsx("div",{className:`vox-row-header${c?" vox-row-header--selected":""}`,style:{width:je,height:E},onMouseDown:s=>Xt(e,s),onMouseEnter:()=>Ke.current&&re.current!==null&&$(Ne(re.current,e)),children:a.toLocaleString()}),l.map((s,f)=>{const D=Ln(S,e,f),se=C?.row===e&&C?.col===f,mn=R?.row===e&&R?.col===f,pn=L.isDirty(e,f),gn=Wt.has(`${e}:${f}`),vn=K?.row===e&&K?.col===f,wn=!be&&e===cn&&f===an&&q!==null&&!w,bn=be&&he!==null&&q!==null&&e>q.r2&&e<=he&&f>=q.c1&&f<=q.c2,yn=s.align??Sn(s.type),Cn="vox-cell"+(D?" vox-cell--selected":"")+(se?" vox-cell--active":"")+(pn?" vox-cell--dirty":"")+(bn?" vox-cell--selected":"")+(gn?" vox-cell--search-hit":"")+(vn?" vox-cell--search-current":"");return m.jsxs("div",{className:Cn,role:"gridcell","aria-selected":D,"aria-colindex":f+1,style:{width:J(f),height:E,justifyContent:eo(yn)},onMouseDown:ot=>_t(e,f,ot),onMouseEnter:()=>It(e,f),onDoubleClick:()=>me(e,f),onContextMenu:on,children:[mn?m.jsx("input",{ref:Oe,className:"vox-cell-input",value:qe,onChange:ot=>Ct(ot.target.value),onBlur:_,onCompositionStart:()=>Xe.current=!0,onCompositionEnd:()=>Xe.current=!1}):De(e,f),wn&&m.jsx("div",{className:"vox-fill-handle",onMouseDown:qt})]},f)})]},e)},jt=[];for(let e=Pt;e<Ut;e++)jt.push(dn(e,(e-Q)*E));const fn={"--vox-row-height":`${E}px`,"--vox-font-size":`${Ue}px`,"--vox-row-header-width":`${je}px`,...O},hn=T===0&&bt!=="loading";return m.jsxs("div",{ref:ut,className:`vox-sheet${I?` ${I}`:""}`,"data-vox-theme":k==="system"?void 0:k,role:"grid","aria-rowcount":T,"aria-colcount":l.length,"aria-readonly":w,tabIndex:0,style:fn,onKeyDown:nn,onContextMenu:w?e=>e.preventDefault():void 0,children:[m.jsxs("div",{className:"vox-header-wrap",style:{paddingRight:$t},children:[m.jsx("div",{className:"vox-corner",style:{width:je},onClick:Ze}),m.jsxs("div",{className:"vox-header",ref:We,role:"row",children:[l.map((e,t)=>{const r=S.some(s=>t>=s.c1&&t<=s.c2&&s.r1===0&&s.r2===T-1),c=e.name,a=j.find(s=>s.column===c);return m.jsxs("div",{className:"vox-header-cell",role:"columnheader","aria-colindex":t+1,"aria-sort":a?a.direction==="asc"?"ascending":"descending":"none",style:{width:J(t),background:r?"var(--vox-color-selection-bg)":void 0},onMouseDown:s=>Yt(t,s),onMouseEnter:()=>Jt(t),children:[X===t?m.jsx("input",{ref:Pe,className:"vox-cell-input",value:Je,onChange:s=>xt(s.target.value),onBlur:Tt,onMouseDown:s=>s.stopPropagation(),onKeyDown:s=>{s.key==="Enter"?(s.preventDefault(),Tt()):s.key==="Escape"&&(s.preventDefault(),Ye(null))}}):m.jsx("span",{className:"vox-header-label",onDoubleClick:H?s=>{s.stopPropagation(),Qt(t)}:void 0,children:e.name}),un(t),m.jsx("div",{className:"vox-resize-handle",onMouseDown:s=>en(t,s),onDoubleClick:s=>{s.preventDefault(),s.stopPropagation(),tn(t)}})]},t)}),ne&&m.jsx("button",{type:"button",className:"vox-add-column",title:"Add column",onClick:()=>ne(l.length),children:"+"})]})]}),m.jsx("div",{className:"vox-body",ref:B,onScroll:Bt,children:m.jsxs("div",{className:"vox-scroll-spacer",style:{height:sn,width:ln},children:[jt,S.map((e,t)=>m.jsx("div",{className:"vox-selection-outline",style:{left:Be(e.c1),top:(e.r1-Q)*E,width:Be(e.c2)+J(e.c2)-Be(e.c1),height:(e.r2-e.r1+1)*E}},`sel-${t}`))]})}),bt==="loading"&&T===0&&m.jsx("div",{className:"vox-overlay",children:te?te():F.loading}),hn&&m.jsx("div",{className:"vox-overlay",children:ce?ce():F.empty}),Ge&&m.jsx(to,{x:Ge.x,y:Ge.y,onClose:()=>ye(null),items:[{label:F.contextCut,action:()=>{tt()},disabled:!q},{label:F.contextCopy,action:()=>{Te()},disabled:!q},{label:F.contextPaste,action:()=>{nt()},disabled:!C},...Ee?["sep",{label:F.contextInsertRowAbove,action:()=>C&&Ee(C.row,"above"),disabled:!C},{label:F.contextInsertRowBelow,action:()=>C&&Ee(C.row,"below"),disabled:!C}]:[],...M?["sep",{label:F.contextDeleteRows,action:rn,disabled:S.length===0}]:[],"sep",{label:F.contextUndo,action:()=>L.undo(),disabled:!L.canUndo()},{label:F.contextRedo,action:()=>L.redo(),disabled:!L.canRedo()}]})]})},eo=n=>n==="right"?"flex-end":n==="center"?"center":"flex-start",to=({x:n,y:d,items:l,onClose:i})=>{const h=o.useRef(null);return o.useEffect(()=>{const u=p=>{h.current&&!h.current.contains(p.target)&&i()};return document.addEventListener("mousedown",u),()=>document.removeEventListener("mousedown",u)},[i]),m.jsx("div",{ref:h,className:"vox-context-menu",style:{left:n,top:d},role:"menu",children:l.map((u,p)=>u==="sep"?m.jsx("div",{className:"vox-context-separator"},p):m.jsx("button",{type:"button",className:"vox-context-item",role:"menuitem",disabled:u.disabled,onClick:()=>{u.action(),i()},children:u.label},p))})},no=o.forwardRef(Zn);exports.DEFAULT_ICONS=ct;exports.DEFAULT_LABELS=at;exports.VoxSheet=no;
|
|
7
|
+
//# sourceMappingURL=index.cjs.map
|