vastgrid 0.1.1 → 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/README.md +175 -13
- package/dist/canvas/geometry.d.ts +92 -0
- package/dist/canvas/geometry.d.ts.map +1 -0
- package/dist/canvas/geometry.js +197 -0
- package/dist/canvas/geometry.js.map +1 -0
- package/dist/canvas/grid-canvas.d.ts +161 -0
- package/dist/canvas/grid-canvas.d.ts.map +1 -0
- package/dist/canvas/grid-canvas.js +1146 -0
- package/dist/canvas/grid-canvas.js.map +1 -0
- package/dist/canvas/hooks.d.ts +61 -0
- package/dist/canvas/hooks.d.ts.map +1 -0
- package/dist/canvas/hooks.js +2 -0
- package/dist/canvas/hooks.js.map +1 -0
- package/dist/canvas/keys.d.ts +26 -0
- package/dist/canvas/keys.d.ts.map +1 -0
- package/dist/canvas/keys.js +133 -0
- package/dist/canvas/keys.js.map +1 -0
- package/dist/canvas/layout.d.ts +26 -0
- package/dist/canvas/layout.d.ts.map +1 -0
- package/dist/canvas/layout.js +73 -0
- package/dist/canvas/layout.js.map +1 -0
- package/dist/canvas/scroll.d.ts +31 -0
- package/dist/canvas/scroll.d.ts.map +1 -0
- package/dist/canvas/scroll.js +111 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/text.d.ts +23 -0
- package/dist/canvas/text.d.ts.map +1 -0
- package/dist/canvas/text.js +142 -0
- package/dist/canvas/text.js.map +1 -0
- package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
- package/dist/canvas/theme.d.ts.map +1 -0
- package/dist/canvas/theme.js +62 -0
- package/dist/canvas/theme.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +222 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checked.d.ts +20 -0
- package/dist/core/checked.d.ts.map +1 -0
- package/dist/core/checked.js +61 -0
- package/dist/core/checked.js.map +1 -0
- package/dist/core/client.d.ts +33 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +103 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/conditions.d.ts +48 -0
- package/dist/core/conditions.d.ts.map +1 -0
- package/dist/core/conditions.js +105 -0
- package/dist/core/conditions.js.map +1 -0
- package/dist/core/evaluate.d.ts +20 -0
- package/dist/core/evaluate.d.ts.map +1 -0
- package/dist/core/evaluate.js +210 -0
- package/dist/core/evaluate.js.map +1 -0
- package/dist/core/filters.d.ts +51 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +303 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/format.d.ts +14 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +303 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/measures.d.ts +26 -0
- package/dist/core/measures.d.ts.map +1 -0
- package/dist/core/measures.js +105 -0
- package/dist/core/measures.js.map +1 -0
- package/dist/core/request-ids.d.ts +20 -0
- package/dist/core/request-ids.d.ts.map +1 -0
- package/dist/core/request-ids.js +55 -0
- package/dist/core/request-ids.js.map +1 -0
- package/dist/core/rows.d.ts +79 -0
- package/dist/core/rows.d.ts.map +1 -0
- package/dist/core/rows.js +173 -0
- package/dist/core/rows.js.map +1 -0
- package/dist/core/store.d.ts +233 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +1162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/totals.d.ts +24 -0
- package/dist/core/totals.d.ts.map +1 -0
- package/dist/core/totals.js +49 -0
- package/dist/core/totals.js.map +1 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transports.d.ts +50 -0
- package/dist/core/transports.d.ts.map +1 -0
- package/dist/core/transports.js +160 -0
- package/dist/core/transports.js.map +1 -0
- package/dist/core/types.d.ts +151 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +45 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +11 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/react/accessible-rows.d.ts +13 -0
- package/dist/react/accessible-rows.d.ts.map +1 -0
- package/dist/react/accessible-rows.js +320 -0
- package/dist/react/accessible-rows.js.map +1 -0
- package/dist/react/cell-editor.d.ts +22 -0
- package/dist/react/cell-editor.d.ts.map +1 -0
- package/dist/react/cell-editor.js +431 -0
- package/dist/react/cell-editor.js.map +1 -0
- package/dist/react/cell-menu.d.ts +18 -0
- package/dist/react/cell-menu.d.ts.map +1 -0
- package/dist/react/cell-menu.js +299 -0
- package/dist/react/cell-menu.js.map +1 -0
- package/dist/react/column-menu.d.ts +16 -0
- package/dist/react/column-menu.d.ts.map +1 -0
- package/dist/react/column-menu.js +1311 -0
- package/dist/react/column-menu.js.map +1 -0
- package/dist/react/columns-menu.d.ts +7 -0
- package/dist/react/columns-menu.d.ts.map +1 -0
- package/dist/react/columns-menu.js +249 -0
- package/dist/react/columns-menu.js.map +1 -0
- package/dist/react/conditions-menu.d.ts +10 -0
- package/dist/react/conditions-menu.d.ts.map +1 -0
- package/dist/react/conditions-menu.js +795 -0
- package/dist/react/conditions-menu.js.map +1 -0
- package/dist/react/context.d.ts +29 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +18 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/data-grid.d.ts +79 -0
- package/dist/react/data-grid.d.ts.map +1 -0
- package/dist/react/data-grid.js +653 -0
- package/dist/react/data-grid.js.map +1 -0
- package/dist/react/detail-panel.d.ts +22 -0
- package/dist/react/detail-panel.d.ts.map +1 -0
- package/dist/react/detail-panel.js +242 -0
- package/dist/react/detail-panel.js.map +1 -0
- package/dist/react/export-menu.d.ts +11 -0
- package/dist/react/export-menu.d.ts.map +1 -0
- package/dist/react/export-menu.js +304 -0
- package/dist/react/export-menu.js.map +1 -0
- package/dist/react/find-bar.d.ts +11 -0
- package/dist/react/find-bar.d.ts.map +1 -0
- package/dist/react/find-bar.js +303 -0
- package/dist/react/find-bar.js.map +1 -0
- package/dist/react/format-form.d.ts +12 -0
- package/dist/react/format-form.d.ts.map +1 -0
- package/dist/react/format-form.js +863 -0
- package/dist/react/format-form.js.map +1 -0
- package/dist/react/formula-menu.d.ts +6 -0
- package/dist/react/formula-menu.d.ts.map +1 -0
- package/dist/react/formula-menu.js +343 -0
- package/dist/react/formula-menu.js.map +1 -0
- package/dist/react/grid-body.d.ts +33 -0
- package/dist/react/grid-body.d.ts.map +1 -0
- package/dist/react/grid-body.js +1389 -0
- package/dist/react/grid-body.js.map +1 -0
- package/dist/react/group-menu.d.ts +7 -0
- package/dist/react/group-menu.d.ts.map +1 -0
- package/dist/react/group-menu.js +508 -0
- package/dist/react/group-menu.js.map +1 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +161 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/messages.d.ts +180 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +414 -0
- package/dist/react/messages.js.map +1 -0
- package/dist/react/save.d.ts +9 -0
- package/dist/react/save.d.ts.map +1 -0
- package/dist/react/save.js +102 -0
- package/dist/react/save.js.map +1 -0
- package/dist/react/static-rows.d.ts +9 -0
- package/dist/react/static-rows.d.ts.map +1 -0
- package/dist/react/static-rows.js +176 -0
- package/dist/react/static-rows.js.map +1 -0
- package/dist/react/status-bar.d.ts +12 -0
- package/dist/react/status-bar.d.ts.map +1 -0
- package/dist/react/status-bar.js +413 -0
- package/dist/react/status-bar.js.map +1 -0
- package/dist/react/toolbar-popover.d.ts +14 -0
- package/dist/react/toolbar-popover.d.ts.map +1 -0
- package/dist/react/toolbar-popover.js +143 -0
- package/dist/react/toolbar-popover.js.map +1 -0
- package/dist/react/toolbar.d.ts +13 -0
- package/dist/react/toolbar.d.ts.map +1 -0
- package/dist/react/toolbar.js +344 -0
- package/dist/react/toolbar.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +496 -316
- package/package.json +42 -17
- package/src/canvas/geometry.ts +197 -0
- package/src/canvas/grid-canvas.ts +1222 -0
- package/src/canvas/hooks.ts +61 -0
- package/src/canvas/keys.ts +96 -0
- package/src/canvas/layout.ts +77 -0
- package/src/canvas/scroll.ts +120 -0
- package/src/canvas/text.ts +155 -0
- package/src/{theme.ts → canvas/theme.ts} +14 -1
- package/src/core/cache.ts +247 -0
- package/src/core/checked.ts +64 -0
- package/src/core/client.ts +103 -0
- package/src/core/conditions.ts +89 -0
- package/src/core/evaluate.ts +221 -0
- package/src/core/filters.ts +268 -0
- package/src/core/format.ts +309 -0
- package/src/core/measures.ts +99 -0
- package/src/core/request-ids.ts +54 -0
- package/src/core/rows.ts +213 -0
- package/src/core/store.ts +1122 -0
- package/src/core/totals.ts +59 -0
- package/src/core/trace.ts +44 -0
- package/src/core/transports.ts +177 -0
- package/src/core/types.ts +169 -0
- package/src/core.ts +39 -0
- package/src/index.ts +24 -12
- package/src/react/accessible-rows.tsx +242 -0
- package/src/react/cell-editor.tsx +216 -0
- package/src/react/cell-menu.tsx +166 -0
- package/src/react/column-menu.tsx +619 -0
- package/src/react/columns-menu.tsx +116 -0
- package/src/react/conditions-menu.tsx +289 -0
- package/src/react/context.ts +33 -0
- package/src/react/data-grid.tsx +286 -0
- package/src/react/detail-panel.tsx +92 -0
- package/src/react/export-menu.tsx +112 -0
- package/src/react/find-bar.tsx +112 -0
- package/src/react/format-form.tsx +421 -0
- package/src/react/formula-menu.tsx +163 -0
- package/src/react/grid-body.tsx +744 -0
- package/src/react/group-menu.tsx +214 -0
- package/src/react/hooks.ts +104 -0
- package/src/react/messages.ts +556 -0
- package/src/react/save.ts +42 -0
- package/src/react/static-rows.tsx +59 -0
- package/src/react/status-bar.tsx +175 -0
- package/src/react/toolbar-popover.tsx +69 -0
- package/src/react/toolbar.tsx +152 -0
- package/src/server.ts +58 -0
- package/src/styles.css +496 -316
- package/dist/components/bars.d.ts +0 -14
- package/dist/components/bars.d.ts.map +0 -1
- package/dist/components/bars.js +0 -68
- package/dist/components/bars.js.map +0 -1
- package/dist/components/cell-inspector.d.ts +0 -11
- package/dist/components/cell-inspector.d.ts.map +0 -1
- package/dist/components/cell-inspector.js +0 -37
- package/dist/components/cell-inspector.js.map +0 -1
- package/dist/components/columns-panel.d.ts +0 -12
- package/dist/components/columns-panel.d.ts.map +0 -1
- package/dist/components/columns-panel.js +0 -11
- package/dist/components/columns-panel.js.map +0 -1
- package/dist/components/filter-panel.d.ts +0 -22
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -169
- package/dist/components/filter-panel.js.map +0 -1
- package/dist/components/find-bar.d.ts +0 -9
- package/dist/components/find-bar.d.ts.map +0 -1
- package/dist/components/find-bar.js +0 -46
- package/dist/components/find-bar.js.map +0 -1
- package/dist/components/popover.d.ts +0 -38
- package/dist/components/popover.d.ts.map +0 -1
- package/dist/components/popover.js +0 -104
- package/dist/components/popover.js.map +0 -1
- package/dist/components/stats-panel.d.ts +0 -12
- package/dist/components/stats-panel.d.ts.map +0 -1
- package/dist/components/stats-panel.js +0 -40
- package/dist/components/stats-panel.js.map +0 -1
- package/dist/controller.d.ts +0 -227
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -845
- package/dist/controller.js.map +0 -1
- package/dist/data-grid.d.ts +0 -51
- package/dist/data-grid.d.ts.map +0 -1
- package/dist/data-grid.js +0 -704
- package/dist/data-grid.js.map +0 -1
- package/dist/labels.d.ts +0 -9
- package/dist/labels.d.ts.map +0 -1
- package/dist/labels.js +0 -38
- package/dist/labels.js.map +0 -1
- package/dist/messages.d.ts +0 -99
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js +0 -276
- package/dist/messages.js.map +0 -1
- package/dist/renderer.d.ts +0 -52
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -522
- package/dist/renderer.js.map +0 -1
- package/dist/text.d.ts +0 -14
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js +0 -63
- package/dist/text.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -54
- package/dist/theme.js.map +0 -1
- package/src/components/bars.tsx +0 -180
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -293
- package/src/components/find-bar.tsx +0 -104
- package/src/components/popover.tsx +0 -194
- package/src/components/stats-panel.tsx +0 -153
- package/src/controller.ts +0 -945
- package/src/data-grid.tsx +0 -1035
- package/src/labels.ts +0 -46
- package/src/messages.ts +0 -390
- package/src/renderer.ts +0 -535
- package/src/text.ts +0 -55
package/src/data-grid.tsx
DELETED
|
@@ -1,1035 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type CSSProperties,
|
|
3
|
-
forwardRef,
|
|
4
|
-
type KeyboardEvent,
|
|
5
|
-
type MouseEvent,
|
|
6
|
-
type PointerEvent,
|
|
7
|
-
useCallback,
|
|
8
|
-
useEffect,
|
|
9
|
-
useId,
|
|
10
|
-
useImperativeHandle,
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
useSyncExternalStore,
|
|
15
|
-
} from "react";
|
|
16
|
-
import { type CellValue, plainText, type TableSource, type ViewQuery } from "vastgrid-core";
|
|
17
|
-
import { StatusBar, Toolbar } from "./components/bars.js";
|
|
18
|
-
import { CellInspector } from "./components/cell-inspector.js";
|
|
19
|
-
import { ColumnsPanel } from "./components/columns-panel.js";
|
|
20
|
-
import { FilterPanel } from "./components/filter-panel.js";
|
|
21
|
-
import { FindBar } from "./components/find-bar.js";
|
|
22
|
-
import { type Anchor, type MenuItem, MenuList, Popover } from "./components/popover.js";
|
|
23
|
-
import { StatsPanel } from "./components/stats-panel.js";
|
|
24
|
-
import { CopyTooLargeError, GridController, type Hit } from "./controller.js";
|
|
25
|
-
import { countFormat, numberFormat } from "./labels.js";
|
|
26
|
-
import { type Messages, messagesFor } from "./messages.js";
|
|
27
|
-
import { CanvasRenderer } from "./renderer.js";
|
|
28
|
-
import { cssLength, resolveTheme } from "./theme.js";
|
|
29
|
-
|
|
30
|
-
export interface DataGridProps {
|
|
31
|
-
/** The table to show. A new source starts a new grid. */
|
|
32
|
-
source: TableSource;
|
|
33
|
-
/** Locale for numbers and the built-in texts (`ja` or `en`). Defaults to the browser's language. */
|
|
34
|
-
locale?: string;
|
|
35
|
-
/** Replaces some of the built-in texts. */
|
|
36
|
-
messages?: Partial<Messages>;
|
|
37
|
-
/** Row height in pixels. Defaults to `--vg-row-height` or 24. */
|
|
38
|
-
rowHeight?: number;
|
|
39
|
-
/** Header height in pixels. Defaults to `--vg-header-height` or 32. */
|
|
40
|
-
headerHeight?: number;
|
|
41
|
-
/** Text shown for null cells. Defaults to an empty cell. */
|
|
42
|
-
nullText?: string;
|
|
43
|
-
/** Columns frozen at the left when the table opens. */
|
|
44
|
-
frozenColumns?: number;
|
|
45
|
-
/** Shows the search box, active filters and row count. Defaults to `true`. */
|
|
46
|
-
toolbar?: boolean;
|
|
47
|
-
/** Shows the active cell and the selection summary. Defaults to `true`. */
|
|
48
|
-
statusBar?: boolean;
|
|
49
|
-
/** Changing this value reads the table again, for example when the file changed on disk. */
|
|
50
|
-
reloadKey?: unknown;
|
|
51
|
-
/** Cells that can be copied at once. Defaults to 1,000,000. */
|
|
52
|
-
copyLimit?: number;
|
|
53
|
-
onQueryChange?: (query: ViewQuery) => void;
|
|
54
|
-
className?: string;
|
|
55
|
-
style?: CSSProperties;
|
|
56
|
-
"aria-label"?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface DataGridHandle {
|
|
60
|
-
focus(): void;
|
|
61
|
-
reload(): void;
|
|
62
|
-
getQuery(): ViewQuery;
|
|
63
|
-
setQuery(query: ViewQuery): void;
|
|
64
|
-
/** Scrolls to a row of the current view and makes it active. */
|
|
65
|
-
goToRow(row: number): void;
|
|
66
|
-
readonly controller: GridController;
|
|
67
|
-
/** The canvas renderer, for diagnostics such as `renderer.timings()`. Null before the grid mounts. */
|
|
68
|
-
readonly renderer: CanvasRenderer | null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
type Overlay =
|
|
72
|
-
| { kind: "columnMenu"; col: number; anchor: Anchor }
|
|
73
|
-
| { kind: "contextMenu"; row: number; col: number; anchor: Anchor }
|
|
74
|
-
| { kind: "filter"; column: number; anchor: Anchor }
|
|
75
|
-
| { kind: "stats"; col: number; anchor: Anchor }
|
|
76
|
-
| { kind: "inspector"; row: number; col: number; anchor: Anchor }
|
|
77
|
-
| { kind: "columns"; anchor: Anchor };
|
|
78
|
-
|
|
79
|
-
type Drag =
|
|
80
|
-
| { kind: "cells" | "rows" | "cols"; pointerId: number; x: number; y: number }
|
|
81
|
-
| { kind: "resize"; pointerId: number; col: number; startX: number; startWidth: number }
|
|
82
|
-
| { kind: "move"; pointerId: number; col: number; x: number; y: number; moved: boolean };
|
|
83
|
-
|
|
84
|
-
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
85
|
-
|
|
86
|
-
async function writeClipboard(text: Promise<string>, html: Promise<string | null> | null) {
|
|
87
|
-
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard;
|
|
88
|
-
if (!clipboard) throw new Error("The clipboard is not available.");
|
|
89
|
-
if (clipboard.write && typeof ClipboardItem !== "undefined") {
|
|
90
|
-
const items: Record<string, Promise<Blob>> = {
|
|
91
|
-
"text/plain": text.then((value) => new Blob([value], { type: "text/plain" })),
|
|
92
|
-
};
|
|
93
|
-
if (html) items["text/html"] = html.then((value) => new Blob([value ?? ""], { type: "text/html" }));
|
|
94
|
-
await clipboard.write([new ClipboardItem(items)]);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
await clipboard.writeText(await text);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* A data grid for very large tables. Draws on a canvas, reads rows through a `TableSource` as you
|
|
102
|
-
* scroll, and offers spreadsheet selection and keyboard navigation, copying, sorting, filters,
|
|
103
|
-
* search, column statistics and a selection summary.
|
|
104
|
-
*/
|
|
105
|
-
export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(function DataGrid(props, ref) {
|
|
106
|
-
const {
|
|
107
|
-
source,
|
|
108
|
-
locale,
|
|
109
|
-
rowHeight,
|
|
110
|
-
headerHeight,
|
|
111
|
-
nullText = "",
|
|
112
|
-
frozenColumns = 0,
|
|
113
|
-
toolbar = true,
|
|
114
|
-
statusBar = true,
|
|
115
|
-
reloadKey,
|
|
116
|
-
copyLimit = 1_000_000,
|
|
117
|
-
onQueryChange,
|
|
118
|
-
className,
|
|
119
|
-
style,
|
|
120
|
-
} = props;
|
|
121
|
-
const resolvedLocale = locale ?? (typeof navigator === "undefined" ? undefined : navigator.language);
|
|
122
|
-
const messages = useMemo(
|
|
123
|
-
() => ({ ...messagesFor(resolvedLocale), ...props.messages }),
|
|
124
|
-
[resolvedLocale, props.messages],
|
|
125
|
-
);
|
|
126
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
127
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
128
|
-
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
129
|
-
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
130
|
-
const sizerRef = useRef<HTMLDivElement>(null);
|
|
131
|
-
const rendererRef = useRef<CanvasRenderer | null>(null);
|
|
132
|
-
const dragRef = useRef<Drag | null>(null);
|
|
133
|
-
const autoScrollRef = useRef(0);
|
|
134
|
-
const cellId = useId();
|
|
135
|
-
const [overlay, setOverlay] = useState<Overlay | null>(null);
|
|
136
|
-
const [findOpen, setFindOpen] = useState(false);
|
|
137
|
-
const [notice, setNotice] = useState<string | null>(null);
|
|
138
|
-
const noticeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
139
|
-
|
|
140
|
-
// One controller per source. Options that can change later are applied in effects below.
|
|
141
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: the other options are applied without recreating the grid.
|
|
142
|
-
const controller = useMemo(
|
|
143
|
-
() =>
|
|
144
|
-
new GridController(source, {
|
|
145
|
-
rowHeight: rowHeight ?? 24,
|
|
146
|
-
headerHeight: headerHeight ?? 32,
|
|
147
|
-
locale: resolvedLocale,
|
|
148
|
-
nullText,
|
|
149
|
-
frozenColumns,
|
|
150
|
-
minColumnWidth: 36,
|
|
151
|
-
maxAutoWidth: 360,
|
|
152
|
-
copyLimit,
|
|
153
|
-
}),
|
|
154
|
-
[source],
|
|
155
|
-
);
|
|
156
|
-
useSyncExternalStore(controller.subscribe, controller.getVersion, controller.getVersion);
|
|
157
|
-
|
|
158
|
-
const count = useMemo(() => countFormat(resolvedLocale), [resolvedLocale]);
|
|
159
|
-
const number = useMemo(() => numberFormat(resolvedLocale), [resolvedLocale]);
|
|
160
|
-
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (resolvedLocale !== controller.options.locale || nullText !== controller.options.nullText) {
|
|
163
|
-
controller.setFormat(resolvedLocale, nullText);
|
|
164
|
-
}
|
|
165
|
-
}, [controller, resolvedLocale, nullText]);
|
|
166
|
-
|
|
167
|
-
const announce = useCallback((text: string) => {
|
|
168
|
-
setNotice(text);
|
|
169
|
-
clearTimeout(noticeTimer.current);
|
|
170
|
-
noticeTimer.current = setTimeout(() => setNotice(null), 3000);
|
|
171
|
-
}, []);
|
|
172
|
-
useEffect(() => () => clearTimeout(noticeTimer.current), []);
|
|
173
|
-
|
|
174
|
-
// Canvas, scrolling, sizes and theme.
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
const root = rootRef.current;
|
|
177
|
-
const canvas = canvasRef.current;
|
|
178
|
-
const scroller = scrollerRef.current;
|
|
179
|
-
const sizer = sizerRef.current;
|
|
180
|
-
if (!root || !canvas || !scroller || !sizer) return;
|
|
181
|
-
controller.options.rowHeight = rowHeight ?? cssLength(root, "--vg-row-height", 24);
|
|
182
|
-
controller.options.headerHeight = headerHeight ?? cssLength(root, "--vg-header-height", 32);
|
|
183
|
-
const renderer = new CanvasRenderer(canvas, controller, resolveTheme(root));
|
|
184
|
-
rendererRef.current = renderer;
|
|
185
|
-
controller.attach(scroller, sizer);
|
|
186
|
-
const stopRender = controller.onRender(() => renderer.schedule());
|
|
187
|
-
const resize = () => controller.setViewport(scroller.clientWidth, scroller.clientHeight);
|
|
188
|
-
resize();
|
|
189
|
-
const observer = new ResizeObserver(resize);
|
|
190
|
-
observer.observe(scroller);
|
|
191
|
-
const onScroll = () => controller.handleScroll();
|
|
192
|
-
scroller.addEventListener("scroll", onScroll, { passive: true });
|
|
193
|
-
// Without a text selection, Chromium and Safari disable the copy command unless "beforecopy" is
|
|
194
|
-
// cancelled; then ⌘C and the Edit menu fire "copy", which the grid answers while it has focus.
|
|
195
|
-
const onBeforeCopy = (event: Event) => {
|
|
196
|
-
if (document.activeElement === scroller) event.preventDefault();
|
|
197
|
-
};
|
|
198
|
-
const onCopy = (event: ClipboardEvent) => {
|
|
199
|
-
if (document.activeElement === scroller) copyHandler.current(event);
|
|
200
|
-
};
|
|
201
|
-
document.addEventListener("beforecopy", onBeforeCopy);
|
|
202
|
-
document.addEventListener("copy", onCopy);
|
|
203
|
-
const refreshTheme = () => renderer.setTheme(resolveTheme(root));
|
|
204
|
-
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
205
|
-
media.addEventListener("change", refreshTheme);
|
|
206
|
-
const themeObserver = new MutationObserver(refreshTheme);
|
|
207
|
-
themeObserver.observe(document.documentElement, {
|
|
208
|
-
attributes: true,
|
|
209
|
-
attributeFilter: ["class", "style", "data-theme"],
|
|
210
|
-
});
|
|
211
|
-
void document.fonts?.ready.then(refreshTheme);
|
|
212
|
-
controller.start();
|
|
213
|
-
renderer.schedule();
|
|
214
|
-
return () => {
|
|
215
|
-
controller.stop();
|
|
216
|
-
stopRender();
|
|
217
|
-
observer.disconnect();
|
|
218
|
-
scroller.removeEventListener("scroll", onScroll);
|
|
219
|
-
document.removeEventListener("beforecopy", onBeforeCopy);
|
|
220
|
-
document.removeEventListener("copy", onCopy);
|
|
221
|
-
media.removeEventListener("change", refreshTheme);
|
|
222
|
-
themeObserver.disconnect();
|
|
223
|
-
renderer.dispose();
|
|
224
|
-
rendererRef.current = null;
|
|
225
|
-
};
|
|
226
|
-
}, [controller, rowHeight, headerHeight]);
|
|
227
|
-
|
|
228
|
-
// Theme changes through the grid's own class or style.
|
|
229
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: className and style are the triggers.
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
const root = rootRef.current;
|
|
232
|
-
if (root && rendererRef.current) rendererRef.current.setTheme(resolveTheme(root));
|
|
233
|
-
}, [className, style]);
|
|
234
|
-
|
|
235
|
-
const firstReload = useRef(true);
|
|
236
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: reloadKey is the trigger.
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (firstReload.current) {
|
|
239
|
-
firstReload.current = false;
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
controller.reload();
|
|
243
|
-
}, [reloadKey, controller]);
|
|
244
|
-
|
|
245
|
-
const lastQuery = useRef(controller.query);
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
if (lastQuery.current !== controller.query) {
|
|
248
|
-
lastQuery.current = controller.query;
|
|
249
|
-
onQueryChange?.(controller.query);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
useImperativeHandle(
|
|
254
|
-
ref,
|
|
255
|
-
() => ({
|
|
256
|
-
focus: () => scrollerRef.current?.focus(),
|
|
257
|
-
reload: () => controller.reload(),
|
|
258
|
-
getQuery: () => controller.query,
|
|
259
|
-
setQuery: (query) => void controller.setQuery(query),
|
|
260
|
-
goToRow: (row) => {
|
|
261
|
-
controller.selection.select({ row, col: controller.selection.active.col });
|
|
262
|
-
controller.selectionChanged({ row, col: controller.selection.active.col });
|
|
263
|
-
},
|
|
264
|
-
controller,
|
|
265
|
-
get renderer() {
|
|
266
|
-
return rendererRef.current;
|
|
267
|
-
},
|
|
268
|
-
}),
|
|
269
|
-
[controller],
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
// ----- helpers -----
|
|
273
|
-
|
|
274
|
-
function local(event: { clientX: number; clientY: number }) {
|
|
275
|
-
const rect = scrollerRef.current?.getBoundingClientRect();
|
|
276
|
-
return { x: event.clientX - (rect?.left ?? 0), y: event.clientY - (rect?.top ?? 0) };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** A popover anchor at a point of the grid body. */
|
|
280
|
-
function anchorAt(x: number, y: number, height = 0): Anchor {
|
|
281
|
-
const offsetX = bodyRef.current?.offsetLeft ?? 0;
|
|
282
|
-
const offsetY = bodyRef.current?.offsetTop ?? 0;
|
|
283
|
-
return { x: x + offsetX, y: y + offsetY, height };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function headerAnchor(col: number): Anchor {
|
|
287
|
-
const x = Math.max(controller.gutterWidth, controller.columnX(col));
|
|
288
|
-
return anchorAt(x, 0, controller.options.headerHeight);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function cellAnchor(row: number, col: number): Anchor {
|
|
292
|
-
const x = Math.max(controller.gutterWidth, controller.columnX(col));
|
|
293
|
-
const y = Math.max(controller.options.headerHeight, Math.min(controller.height - 8, controller.rowY(row)));
|
|
294
|
-
return anchorAt(x, y, controller.options.rowHeight);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function focusGrid() {
|
|
298
|
-
scrollerRef.current?.focus({ preventScroll: true });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const copy = useCallback(
|
|
302
|
-
async (options: { headers?: boolean; markdown?: boolean }) => {
|
|
303
|
-
const task = controller.copySelection(options);
|
|
304
|
-
try {
|
|
305
|
-
await writeClipboard(
|
|
306
|
-
task.then((result) => result.text),
|
|
307
|
-
options.markdown ? null : task.then((result) => result.html),
|
|
308
|
-
);
|
|
309
|
-
const result = await task;
|
|
310
|
-
announce(messages.copied(count(result.cells)));
|
|
311
|
-
} catch (error) {
|
|
312
|
-
if (error instanceof CopyTooLargeError) announce(messages.copyTooLarge(count(error.limit)));
|
|
313
|
-
else announce(messages.copyFailed);
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
[announce, controller, count, messages],
|
|
317
|
-
);
|
|
318
|
-
/** Options of a copy started from the keyboard, answered by the browser's own copy event. */
|
|
319
|
-
const pendingCopy = useRef<{ headers?: boolean } | null>(null);
|
|
320
|
-
const copyHandler = useRef<(event: ClipboardEvent) => void>(() => undefined);
|
|
321
|
-
|
|
322
|
-
// ----- pointer -----
|
|
323
|
-
|
|
324
|
-
function selectMode(event: { shiftKey: boolean; metaKey: boolean; ctrlKey: boolean }) {
|
|
325
|
-
if (event.shiftKey) return "extend" as const;
|
|
326
|
-
if (isMac ? event.metaKey : event.ctrlKey) return "add" as const;
|
|
327
|
-
return "replace" as const;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function dragTo(x: number, y: number) {
|
|
331
|
-
const drag = dragRef.current;
|
|
332
|
-
if (!drag || (drag.kind !== "cells" && drag.kind !== "rows" && drag.kind !== "cols")) return;
|
|
333
|
-
const c = controller;
|
|
334
|
-
const clampedX = Math.min(Math.max(x, c.gutterWidth + 1), c.width - 1);
|
|
335
|
-
const clampedY = Math.min(Math.max(y, c.options.headerHeight + 1), c.height - 1);
|
|
336
|
-
const col = c.columnAt(clampedX);
|
|
337
|
-
const row = c.rowAt(clampedY);
|
|
338
|
-
if (drag.kind === "rows" && row >= 0) c.selection.selectRows(row, row, "extend");
|
|
339
|
-
else if (drag.kind === "cols" && col >= 0) c.selection.selectCols(col, col, "extend");
|
|
340
|
-
else if (drag.kind === "cells" && row >= 0 && col >= 0) c.selection.select({ row, col }, "extend");
|
|
341
|
-
c.selectionChanged();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function autoScroll() {
|
|
345
|
-
const drag = dragRef.current;
|
|
346
|
-
if (!drag || (drag.kind !== "cells" && drag.kind !== "rows" && drag.kind !== "cols")) {
|
|
347
|
-
autoScrollRef.current = 0;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const c = controller;
|
|
351
|
-
const speed = (distance: number) => Math.sign(distance) * Math.min(80, Math.abs(distance) * 0.6);
|
|
352
|
-
let dx = 0;
|
|
353
|
-
let dy = 0;
|
|
354
|
-
if (drag.y < c.options.headerHeight) dy = speed(drag.y - c.options.headerHeight);
|
|
355
|
-
else if (drag.y > c.height) dy = speed(drag.y - c.height);
|
|
356
|
-
if (drag.kind !== "rows") {
|
|
357
|
-
if (drag.x < c.gutterWidth + c.frozenWidth && c.scrollX.position > 0)
|
|
358
|
-
dx = speed(drag.x - c.gutterWidth - c.frozenWidth);
|
|
359
|
-
else if (drag.x > c.width) dx = speed(drag.x - c.width);
|
|
360
|
-
}
|
|
361
|
-
if (dx !== 0 || dy !== 0) {
|
|
362
|
-
c.scrollToVirtual(dx === 0 ? null : c.scrollX.position + dx, dy === 0 ? null : c.scrollY.position + dy);
|
|
363
|
-
dragTo(drag.x, drag.y);
|
|
364
|
-
}
|
|
365
|
-
autoScrollRef.current = requestAnimationFrame(autoScroll);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function onPointerDown(event: PointerEvent<HTMLDivElement>) {
|
|
369
|
-
const scroller = scrollerRef.current;
|
|
370
|
-
if (!scroller || event.button !== 0) return;
|
|
371
|
-
const { x, y } = local(event);
|
|
372
|
-
// Leave the native scrollbars alone.
|
|
373
|
-
if (x > scroller.clientWidth || y > scroller.clientHeight) return;
|
|
374
|
-
focusGrid();
|
|
375
|
-
const c = controller;
|
|
376
|
-
const hit = c.hitTest(x, y);
|
|
377
|
-
const mode = selectMode(event);
|
|
378
|
-
if (hit.resize >= 0) {
|
|
379
|
-
event.preventDefault();
|
|
380
|
-
scroller.setPointerCapture(event.pointerId);
|
|
381
|
-
dragRef.current = {
|
|
382
|
-
kind: "resize",
|
|
383
|
-
pointerId: event.pointerId,
|
|
384
|
-
col: hit.resize,
|
|
385
|
-
startX: x,
|
|
386
|
-
startWidth: c.layout.width(hit.resize),
|
|
387
|
-
};
|
|
388
|
-
c.resizing = hit.resize;
|
|
389
|
-
c.requestRender();
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
if (hit.menu && hit.col >= 0) {
|
|
393
|
-
event.preventDefault();
|
|
394
|
-
setOverlay({ kind: "columnMenu", col: hit.col, anchor: headerAnchor(hit.col) });
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
switch (hit.zone) {
|
|
398
|
-
case "cell":
|
|
399
|
-
c.selection.select({ row: hit.row, col: hit.col }, mode);
|
|
400
|
-
dragRef.current = { kind: "cells", pointerId: event.pointerId, x, y };
|
|
401
|
-
break;
|
|
402
|
-
case "header":
|
|
403
|
-
if (mode === "replace" && c.selection.colSelected(hit.col)) {
|
|
404
|
-
dragRef.current = { kind: "move", pointerId: event.pointerId, col: hit.col, x, y, moved: false };
|
|
405
|
-
} else {
|
|
406
|
-
c.selection.selectCols(hit.col, hit.col, mode);
|
|
407
|
-
dragRef.current = { kind: "cols", pointerId: event.pointerId, x, y };
|
|
408
|
-
}
|
|
409
|
-
break;
|
|
410
|
-
case "gutter":
|
|
411
|
-
c.selection.selectRows(hit.row, hit.row, mode);
|
|
412
|
-
dragRef.current = { kind: "rows", pointerId: event.pointerId, x, y };
|
|
413
|
-
break;
|
|
414
|
-
case "corner":
|
|
415
|
-
c.selection.selectAll();
|
|
416
|
-
break;
|
|
417
|
-
default:
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
event.preventDefault();
|
|
421
|
-
scroller.setPointerCapture(event.pointerId);
|
|
422
|
-
c.selectionChanged();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
|
|
426
|
-
const scroller = scrollerRef.current;
|
|
427
|
-
if (!scroller) return;
|
|
428
|
-
const { x, y } = local(event);
|
|
429
|
-
const c = controller;
|
|
430
|
-
const drag = dragRef.current;
|
|
431
|
-
if (drag && drag.pointerId === event.pointerId) {
|
|
432
|
-
if (drag.kind === "resize") {
|
|
433
|
-
c.resizeColumn(drag.col, drag.startWidth + (x - drag.startX));
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (drag.kind === "move") {
|
|
437
|
-
drag.moved = drag.moved || Math.abs(x - drag.x) > 4;
|
|
438
|
-
drag.x = x;
|
|
439
|
-
drag.y = y;
|
|
440
|
-
scroller.style.cursor = drag.moved ? "grabbing" : "";
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
drag.x = x;
|
|
444
|
-
drag.y = y;
|
|
445
|
-
dragTo(x, y);
|
|
446
|
-
if (!autoScrollRef.current) autoScrollRef.current = requestAnimationFrame(autoScroll);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const hit: Hit =
|
|
450
|
-
x > scroller.clientWidth || y > scroller.clientHeight
|
|
451
|
-
? { zone: "outside", row: -1, col: -1, resize: -1, menu: false }
|
|
452
|
-
: c.hitTest(x, y);
|
|
453
|
-
const previous = c.hover;
|
|
454
|
-
c.hover = hit;
|
|
455
|
-
scroller.style.cursor = hit.resize >= 0 ? "col-resize" : hit.menu ? "pointer" : "";
|
|
456
|
-
if (previous?.zone !== hit.zone || previous.col !== hit.col || previous.menu !== hit.menu) c.requestRender();
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function endDrag(event: PointerEvent<HTMLDivElement>) {
|
|
460
|
-
const drag = dragRef.current;
|
|
461
|
-
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
462
|
-
dragRef.current = null;
|
|
463
|
-
cancelAnimationFrame(autoScrollRef.current);
|
|
464
|
-
autoScrollRef.current = 0;
|
|
465
|
-
const scroller = scrollerRef.current;
|
|
466
|
-
if (scroller?.hasPointerCapture(event.pointerId)) scroller.releasePointerCapture(event.pointerId);
|
|
467
|
-
if (scroller) scroller.style.cursor = "";
|
|
468
|
-
const c = controller;
|
|
469
|
-
if (drag.kind === "resize") {
|
|
470
|
-
c.resizing = -1;
|
|
471
|
-
c.requestRender();
|
|
472
|
-
} else if (drag.kind === "move") {
|
|
473
|
-
const target = c.columnAt(local(event).x);
|
|
474
|
-
if (drag.moved && target >= 0 && target !== drag.col) {
|
|
475
|
-
c.moveColumn(drag.col, target);
|
|
476
|
-
c.selection.selectCols(target, target);
|
|
477
|
-
c.selectionChanged();
|
|
478
|
-
} else if (!drag.moved) {
|
|
479
|
-
c.selection.selectCols(drag.col, drag.col);
|
|
480
|
-
c.selectionChanged();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function onPointerLeave() {
|
|
486
|
-
if (controller.hover && !dragRef.current) {
|
|
487
|
-
controller.hover = null;
|
|
488
|
-
controller.requestRender();
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function onDoubleClick(event: MouseEvent<HTMLDivElement>) {
|
|
493
|
-
const { x, y } = local(event);
|
|
494
|
-
const hit = controller.hitTest(x, y);
|
|
495
|
-
if (hit.resize >= 0) {
|
|
496
|
-
controller.autoFit(hit.resize);
|
|
497
|
-
controller.emit();
|
|
498
|
-
} else if (hit.zone === "cell") {
|
|
499
|
-
setOverlay({ kind: "inspector", row: hit.row, col: hit.col, anchor: cellAnchor(hit.row, hit.col) });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function onContextMenu(event: MouseEvent<HTMLDivElement>) {
|
|
504
|
-
const { x, y } = local(event);
|
|
505
|
-
const c = controller;
|
|
506
|
-
const hit = c.hitTest(x, y);
|
|
507
|
-
if (hit.zone === "header") {
|
|
508
|
-
event.preventDefault();
|
|
509
|
-
setOverlay({ kind: "columnMenu", col: hit.col, anchor: anchorAt(x, y) });
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (hit.zone !== "cell") return;
|
|
513
|
-
event.preventDefault();
|
|
514
|
-
if (!c.selection.contains(hit.row, hit.col)) {
|
|
515
|
-
c.selection.select({ row: hit.row, col: hit.col });
|
|
516
|
-
c.selectionChanged();
|
|
517
|
-
}
|
|
518
|
-
setOverlay({ kind: "contextMenu", row: hit.row, col: hit.col, anchor: anchorAt(x, y) });
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// ----- keyboard -----
|
|
522
|
-
|
|
523
|
-
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
524
|
-
if (event.target !== scrollerRef.current) return;
|
|
525
|
-
const c = controller;
|
|
526
|
-
const primary = isMac ? event.metaKey : event.ctrlKey;
|
|
527
|
-
const extend = event.shiftKey;
|
|
528
|
-
const lastRow = Math.max(0, c.rowCount - 1);
|
|
529
|
-
const lastCol = Math.max(0, c.visible.length - 1);
|
|
530
|
-
const focus = extend ? c.selection.focus : c.selection.active;
|
|
531
|
-
let target: { row: number; col: number } | null = null;
|
|
532
|
-
switch (event.key) {
|
|
533
|
-
case "ArrowDown":
|
|
534
|
-
if (event.altKey) {
|
|
535
|
-
setOverlay({ kind: "columnMenu", col: c.selection.active.col, anchor: headerAnchor(c.selection.active.col) });
|
|
536
|
-
break;
|
|
537
|
-
}
|
|
538
|
-
target = c.selection.moveTo({ row: primary ? lastRow : focus.row + 1, col: focus.col }, extend);
|
|
539
|
-
break;
|
|
540
|
-
case "ArrowUp":
|
|
541
|
-
target = c.selection.moveTo({ row: primary ? 0 : focus.row - 1, col: focus.col }, extend);
|
|
542
|
-
break;
|
|
543
|
-
case "ArrowRight":
|
|
544
|
-
target = c.selection.moveTo({ row: focus.row, col: primary ? lastCol : focus.col + 1 }, extend);
|
|
545
|
-
break;
|
|
546
|
-
case "ArrowLeft":
|
|
547
|
-
target = c.selection.moveTo({ row: focus.row, col: primary ? 0 : focus.col - 1 }, extend);
|
|
548
|
-
break;
|
|
549
|
-
case "PageDown":
|
|
550
|
-
target = event.altKey
|
|
551
|
-
? c.selection.moveTo({ row: focus.row, col: focus.col + 5 }, extend)
|
|
552
|
-
: c.selection.moveTo({ row: focus.row + c.pageRows, col: focus.col }, extend);
|
|
553
|
-
break;
|
|
554
|
-
case "PageUp":
|
|
555
|
-
target = event.altKey
|
|
556
|
-
? c.selection.moveTo({ row: focus.row, col: focus.col - 5 }, extend)
|
|
557
|
-
: c.selection.moveTo({ row: focus.row - c.pageRows, col: focus.col }, extend);
|
|
558
|
-
break;
|
|
559
|
-
case "Home":
|
|
560
|
-
target = c.selection.moveTo({ row: event.ctrlKey || event.metaKey ? 0 : focus.row, col: 0 }, extend);
|
|
561
|
-
break;
|
|
562
|
-
case "End":
|
|
563
|
-
target = c.selection.moveTo(
|
|
564
|
-
{ row: event.ctrlKey || event.metaKey ? lastRow : focus.row, col: lastCol },
|
|
565
|
-
extend,
|
|
566
|
-
);
|
|
567
|
-
break;
|
|
568
|
-
case "Tab":
|
|
569
|
-
target = c.selection.advance(0, event.shiftKey ? -1 : 1);
|
|
570
|
-
break;
|
|
571
|
-
case "Enter":
|
|
572
|
-
target = c.selection.advance(event.shiftKey ? -1 : 1, 0);
|
|
573
|
-
break;
|
|
574
|
-
case " ":
|
|
575
|
-
if (event.shiftKey) c.selection.selectRows(c.selection.active.row, c.selection.active.row);
|
|
576
|
-
else if (event.ctrlKey) c.selection.selectCols(c.selection.active.col, c.selection.active.col);
|
|
577
|
-
else return;
|
|
578
|
-
c.selectionChanged();
|
|
579
|
-
break;
|
|
580
|
-
case "F2":
|
|
581
|
-
setOverlay({
|
|
582
|
-
kind: "inspector",
|
|
583
|
-
...c.selection.active,
|
|
584
|
-
anchor: cellAnchor(c.selection.active.row, c.selection.active.col),
|
|
585
|
-
});
|
|
586
|
-
break;
|
|
587
|
-
case "Escape":
|
|
588
|
-
if (findOpen) setFindOpen(false);
|
|
589
|
-
else if (!c.selection.single) {
|
|
590
|
-
c.selection.select(c.selection.active);
|
|
591
|
-
c.selectionChanged();
|
|
592
|
-
} else return;
|
|
593
|
-
break;
|
|
594
|
-
case "ContextMenu":
|
|
595
|
-
setOverlay({
|
|
596
|
-
kind: "contextMenu",
|
|
597
|
-
...c.selection.active,
|
|
598
|
-
anchor: cellAnchor(c.selection.active.row, c.selection.active.col),
|
|
599
|
-
});
|
|
600
|
-
break;
|
|
601
|
-
default:
|
|
602
|
-
if (primary && event.key.toLowerCase() === "a") {
|
|
603
|
-
c.selection.selectAll();
|
|
604
|
-
c.selectionChanged();
|
|
605
|
-
} else if (primary && event.key.toLowerCase() === "c") {
|
|
606
|
-
// Let the browser run its own copy command: it needs no clipboard permission, and onCopy answers
|
|
607
|
-
// it with the loaded cells. Shortcuts the browser does not treat as copying fall back below.
|
|
608
|
-
const options = { headers: event.shiftKey };
|
|
609
|
-
pendingCopy.current = options;
|
|
610
|
-
setTimeout(() => {
|
|
611
|
-
if (pendingCopy.current !== options) return;
|
|
612
|
-
pendingCopy.current = null;
|
|
613
|
-
void copy(options);
|
|
614
|
-
}, 0);
|
|
615
|
-
return;
|
|
616
|
-
} else if (primary && event.key.toLowerCase() === "f") {
|
|
617
|
-
setFindOpen(true);
|
|
618
|
-
} else if (event.shiftKey && event.key === "F10") {
|
|
619
|
-
setOverlay({
|
|
620
|
-
kind: "contextMenu",
|
|
621
|
-
...c.selection.active,
|
|
622
|
-
anchor: cellAnchor(c.selection.active.row, c.selection.active.col),
|
|
623
|
-
});
|
|
624
|
-
} else {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
event.preventDefault();
|
|
629
|
-
if (target) c.selectionChanged(target);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/** The browser's own copy command (Edit menu): answer synchronously when the cells are loaded. */
|
|
633
|
-
/**
|
|
634
|
-
* The browser's copy command (⌘C, or Copy in the app's Edit menu) while the grid has focus. Loaded
|
|
635
|
-
* cells are written here, synchronously, which needs no clipboard permission; larger selections are
|
|
636
|
-
* read first and written with the asynchronous clipboard API.
|
|
637
|
-
*/
|
|
638
|
-
copyHandler.current = (event: ClipboardEvent) => {
|
|
639
|
-
if (!event.clipboardData) return;
|
|
640
|
-
event.preventDefault();
|
|
641
|
-
const options = pendingCopy.current ?? {};
|
|
642
|
-
pendingCopy.current = null;
|
|
643
|
-
const result = controller.copySelectionSync(options);
|
|
644
|
-
if (result) {
|
|
645
|
-
event.clipboardData.setData("text/plain", result.text);
|
|
646
|
-
if (result.html) event.clipboardData.setData("text/html", result.html);
|
|
647
|
-
announce(messages.copied(count(result.cells)));
|
|
648
|
-
} else {
|
|
649
|
-
void copy(options);
|
|
650
|
-
}
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
// ----- menus -----
|
|
654
|
-
|
|
655
|
-
const c = controller;
|
|
656
|
-
const capabilities = c.info?.capabilities;
|
|
657
|
-
const ready = c.info?.status === "ready";
|
|
658
|
-
|
|
659
|
-
function columnMenuItems(col: number): MenuItem[] {
|
|
660
|
-
const state = c.visible[col];
|
|
661
|
-
const schema = c.columnSchema(col);
|
|
662
|
-
if (!state || !schema) return [];
|
|
663
|
-
const sort = c.sortOf(state.index);
|
|
664
|
-
const otherSorts = (c.query.sort ?? []).filter((key) => key.column !== state.index).length > 0;
|
|
665
|
-
const items: MenuItem[] = [
|
|
666
|
-
{
|
|
667
|
-
label: messages.sortAscending,
|
|
668
|
-
onSelect: () => c.sortBy(col, "asc"),
|
|
669
|
-
disabled: !ready || !capabilities?.sort,
|
|
670
|
-
checked: sort?.direction === "asc" && !otherSorts,
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
label: messages.sortDescending,
|
|
674
|
-
onSelect: () => c.sortBy(col, "desc"),
|
|
675
|
-
disabled: !ready || !capabilities?.sort,
|
|
676
|
-
checked: sort?.direction === "desc" && !otherSorts,
|
|
677
|
-
},
|
|
678
|
-
];
|
|
679
|
-
if ((c.query.sort?.length ?? 0) > 0 && !(sort && !otherSorts)) {
|
|
680
|
-
items.push(
|
|
681
|
-
{ label: messages.addSortAscending, onSelect: () => c.sortBy(col, "asc", true), disabled: !ready },
|
|
682
|
-
{ label: messages.addSortDescending, onSelect: () => c.sortBy(col, "desc", true), disabled: !ready },
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
if ((c.query.sort?.length ?? 0) > 0) items.push({ label: messages.clearSort, onSelect: () => c.clearSort() });
|
|
686
|
-
items.push(
|
|
687
|
-
{
|
|
688
|
-
label: messages.filter,
|
|
689
|
-
separator: true,
|
|
690
|
-
disabled: !ready || !capabilities?.filter,
|
|
691
|
-
onSelect: () => setOverlay({ kind: "filter", column: state.index, anchor: headerAnchor(col) }),
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
label: messages.statistics,
|
|
695
|
-
disabled: !ready || !capabilities?.stats,
|
|
696
|
-
onSelect: () => setOverlay({ kind: "stats", col, anchor: headerAnchor(col) }),
|
|
697
|
-
},
|
|
698
|
-
{
|
|
699
|
-
label: messages.autoFit,
|
|
700
|
-
separator: true,
|
|
701
|
-
onSelect: () => {
|
|
702
|
-
c.autoFit(col);
|
|
703
|
-
c.emit();
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
col < c.frozen && c.frozen > 0
|
|
707
|
-
? { label: messages.unfreeze, onSelect: () => c.freezeUpTo(-1) }
|
|
708
|
-
: { label: messages.freezeUpTo, onSelect: () => c.freezeUpTo(col) },
|
|
709
|
-
{ label: messages.hideColumn, disabled: c.visible.length <= 1, onSelect: () => c.hideColumn(col) },
|
|
710
|
-
{
|
|
711
|
-
label: messages.copyColumnName,
|
|
712
|
-
onSelect: () =>
|
|
713
|
-
void writeClipboard(Promise.resolve(schema.name), null).then(
|
|
714
|
-
() => announce(messages.copied("1")),
|
|
715
|
-
(error: unknown) => announce(error instanceof Error ? error.message : String(error)),
|
|
716
|
-
),
|
|
717
|
-
},
|
|
718
|
-
);
|
|
719
|
-
return items;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function contextMenuItems(row: number, col: number): MenuItem[] {
|
|
723
|
-
const state = c.visible[col];
|
|
724
|
-
const value: CellValue | undefined = c.value(row, col);
|
|
725
|
-
const items: MenuItem[] = [
|
|
726
|
-
{ label: messages.copy, shortcut: isMac ? "⌘C" : "Ctrl+C", onSelect: () => void copy({}) },
|
|
727
|
-
{
|
|
728
|
-
label: messages.copyWithHeaders,
|
|
729
|
-
shortcut: isMac ? "⇧⌘C" : "Ctrl+Shift+C",
|
|
730
|
-
onSelect: () => void copy({ headers: true }),
|
|
731
|
-
},
|
|
732
|
-
{ label: messages.copyAsMarkdown, onSelect: () => void copy({ markdown: true }) },
|
|
733
|
-
];
|
|
734
|
-
if (state && value !== undefined) {
|
|
735
|
-
items.push(
|
|
736
|
-
{
|
|
737
|
-
label: messages.filterToValue,
|
|
738
|
-
separator: true,
|
|
739
|
-
disabled: !ready || !capabilities?.filter,
|
|
740
|
-
onSelect: () => void c.filterByValue(row, col),
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
label: messages.excludeValue,
|
|
744
|
-
disabled: !ready || !capabilities?.filter,
|
|
745
|
-
onSelect: () => void c.filterByValue(row, col, true),
|
|
746
|
-
},
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
items.push(
|
|
750
|
-
{
|
|
751
|
-
label: messages.sortAscending,
|
|
752
|
-
separator: true,
|
|
753
|
-
disabled: !ready || !capabilities?.sort,
|
|
754
|
-
onSelect: () => c.sortBy(col, "asc"),
|
|
755
|
-
},
|
|
756
|
-
{
|
|
757
|
-
label: messages.sortDescending,
|
|
758
|
-
disabled: !ready || !capabilities?.sort,
|
|
759
|
-
onSelect: () => c.sortBy(col, "desc"),
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
label: messages.inspectCell,
|
|
763
|
-
separator: true,
|
|
764
|
-
shortcut: "F2",
|
|
765
|
-
onSelect: () => setOverlay({ kind: "inspector", row, col, anchor: cellAnchor(row, col) }),
|
|
766
|
-
},
|
|
767
|
-
);
|
|
768
|
-
return items;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const closeOverlay = useCallback(() => setOverlay(null), []);
|
|
772
|
-
const loadDistinct = useCallback(
|
|
773
|
-
(search: string, signal: AbortSignal) =>
|
|
774
|
-
overlay?.kind === "filter" ? controller.distinct(overlay.column, search, signal) : null,
|
|
775
|
-
[controller, overlay],
|
|
776
|
-
);
|
|
777
|
-
const loadStats = useCallback(
|
|
778
|
-
(signal: AbortSignal) => (overlay?.kind === "stats" ? controller.stats(overlay.col, signal) : null),
|
|
779
|
-
[controller, overlay],
|
|
780
|
-
);
|
|
781
|
-
const loadCell = useCallback(
|
|
782
|
-
(signal: AbortSignal) =>
|
|
783
|
-
overlay?.kind === "inspector" ? controller.fullValue(overlay.row, overlay.col, signal) : Promise.resolve(null),
|
|
784
|
-
[controller, overlay],
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
// ----- accessibility -----
|
|
788
|
-
|
|
789
|
-
const active = c.selection.active;
|
|
790
|
-
const activeSchema = c.columnSchema(active.col);
|
|
791
|
-
const activeValue = c.value(active.row, active.col);
|
|
792
|
-
const activeText =
|
|
793
|
-
activeValue === undefined ? messages.loading : activeValue === null ? messages.blank : plainText(activeValue);
|
|
794
|
-
|
|
795
|
-
let overlayMessage: string | null = null;
|
|
796
|
-
if (c.loadError) overlayMessage = `${messages.error} ${c.loadError}`;
|
|
797
|
-
else if (c.info?.status === "error") overlayMessage = `${messages.error} ${c.info.error ?? ""}`;
|
|
798
|
-
else if (c.viewError) overlayMessage = c.viewError;
|
|
799
|
-
else if (!c.info || (c.info.status === "loading" && c.readableRows === 0)) overlayMessage = messages.loading;
|
|
800
|
-
else if (c.rowCount === 0 && !c.viewPending) overlayMessage = messages.noRows;
|
|
801
|
-
// Retrying reads the table again after a load error, and builds the view again after a view error.
|
|
802
|
-
const retry =
|
|
803
|
-
c.loadError || c.info?.status === "error"
|
|
804
|
-
? () => controller.reload()
|
|
805
|
-
: c.viewError
|
|
806
|
-
? () => void controller.setQuery(controller.query)
|
|
807
|
-
: null;
|
|
808
|
-
|
|
809
|
-
return (
|
|
810
|
-
<div ref={rootRef} className={`vg-root${className ? ` ${className}` : ""}`} style={style} data-status={c.status}>
|
|
811
|
-
{toolbar && (
|
|
812
|
-
<Toolbar
|
|
813
|
-
controller={c}
|
|
814
|
-
messages={messages}
|
|
815
|
-
onColumns={(button) => {
|
|
816
|
-
const root = rootRef.current?.getBoundingClientRect();
|
|
817
|
-
const rect = button.getBoundingClientRect();
|
|
818
|
-
setOverlay({
|
|
819
|
-
kind: "columns",
|
|
820
|
-
anchor: { x: rect.right - (root?.left ?? 0) - 260, y: rect.top - (root?.top ?? 0), height: rect.height },
|
|
821
|
-
});
|
|
822
|
-
}}
|
|
823
|
-
/>
|
|
824
|
-
)}
|
|
825
|
-
{(c.viewPending || c.info?.status === "loading") && (
|
|
826
|
-
<div
|
|
827
|
-
className="vg-progress"
|
|
828
|
-
role="progressbar"
|
|
829
|
-
aria-label={messages.loading}
|
|
830
|
-
aria-valuemin={0}
|
|
831
|
-
aria-valuemax={100}
|
|
832
|
-
aria-valuenow={c.info?.progress ? Math.round(c.info.progress * 100) : undefined}
|
|
833
|
-
>
|
|
834
|
-
<span
|
|
835
|
-
style={c.info?.progress && !c.viewPending ? { width: `${Math.round(c.info.progress * 100)}%` } : undefined}
|
|
836
|
-
data-indeterminate={c.viewPending || !c.info?.progress || undefined}
|
|
837
|
-
/>
|
|
838
|
-
</div>
|
|
839
|
-
)}
|
|
840
|
-
<div ref={bodyRef} className="vg-body">
|
|
841
|
-
<canvas ref={canvasRef} className="vg-canvas" />
|
|
842
|
-
{/* biome-ignore lint/a11y/useSemanticElements: a canvas grid cannot use table elements; ARIA grid roles describe it. */}
|
|
843
|
-
<div
|
|
844
|
-
ref={scrollerRef}
|
|
845
|
-
className="vg-scroller"
|
|
846
|
-
role="grid"
|
|
847
|
-
tabIndex={0}
|
|
848
|
-
aria-label={props["aria-label"] ?? messages.gridLabel}
|
|
849
|
-
aria-rowcount={c.rowCount + 1}
|
|
850
|
-
aria-colcount={c.visible.length + 1}
|
|
851
|
-
aria-multiselectable="true"
|
|
852
|
-
aria-activedescendant={c.rowCount > 0 && activeSchema ? cellId : undefined}
|
|
853
|
-
onPointerDown={onPointerDown}
|
|
854
|
-
onPointerMove={onPointerMove}
|
|
855
|
-
onPointerUp={endDrag}
|
|
856
|
-
onPointerCancel={endDrag}
|
|
857
|
-
onPointerLeave={onPointerLeave}
|
|
858
|
-
onDoubleClick={onDoubleClick}
|
|
859
|
-
onContextMenu={onContextMenu}
|
|
860
|
-
onKeyDown={onKeyDown}
|
|
861
|
-
>
|
|
862
|
-
<div ref={sizerRef} className="vg-sizer" />
|
|
863
|
-
{/*
|
|
864
|
-
The canvas has no accessible content. Screen readers follow the active cell through
|
|
865
|
-
aria-activedescendant on the grid; these hidden elements describe that cell and its column.
|
|
866
|
-
Focus stays on the grid, so the rows and cells are not focusable themselves.
|
|
867
|
-
*/}
|
|
868
|
-
{activeSchema && c.rowCount > 0 && (
|
|
869
|
-
// biome-ignore lint/a11y/useSemanticElements: ARIA grid pattern over a canvas (see above).
|
|
870
|
-
<div className="vg-sr-only" role="rowgroup">
|
|
871
|
-
{/* biome-ignore lint/a11y/useSemanticElements: ARIA grid pattern over a canvas. */}
|
|
872
|
-
{/* biome-ignore lint/a11y/useFocusableInteractive: focus stays on the grid (aria-activedescendant). */}
|
|
873
|
-
<div role="row" aria-rowindex={1}>
|
|
874
|
-
{/* biome-ignore lint/a11y/useSemanticElements: ARIA grid pattern over a canvas. */}
|
|
875
|
-
{/* biome-ignore lint/a11y/useFocusableInteractive: focus stays on the grid (aria-activedescendant). */}
|
|
876
|
-
<div role="columnheader" aria-colindex={active.col + 2}>
|
|
877
|
-
{activeSchema.name}
|
|
878
|
-
</div>
|
|
879
|
-
</div>
|
|
880
|
-
{/* biome-ignore lint/a11y/useSemanticElements: ARIA grid pattern over a canvas. */}
|
|
881
|
-
{/* biome-ignore lint/a11y/useFocusableInteractive: focus stays on the grid (aria-activedescendant). */}
|
|
882
|
-
<div role="row" aria-rowindex={active.row + 2}>
|
|
883
|
-
{/* biome-ignore lint/a11y/useSemanticElements: ARIA grid pattern over a canvas. */}
|
|
884
|
-
{/* biome-ignore lint/a11y/useFocusableInteractive: focus stays on the grid (aria-activedescendant). */}
|
|
885
|
-
<div role="gridcell" id={cellId} aria-colindex={active.col + 2} aria-selected="true">
|
|
886
|
-
{activeText}
|
|
887
|
-
</div>
|
|
888
|
-
</div>
|
|
889
|
-
</div>
|
|
890
|
-
)}
|
|
891
|
-
</div>
|
|
892
|
-
{overlayMessage && (
|
|
893
|
-
<div
|
|
894
|
-
className="vg-message"
|
|
895
|
-
role={c.loadError || c.viewError || c.info?.status === "error" ? "alert" : "status"}
|
|
896
|
-
>
|
|
897
|
-
<span>{overlayMessage}</span>
|
|
898
|
-
{retry && (
|
|
899
|
-
<button type="button" className="vg-button" onClick={retry}>
|
|
900
|
-
{messages.retry}
|
|
901
|
-
</button>
|
|
902
|
-
)}
|
|
903
|
-
</div>
|
|
904
|
-
)}
|
|
905
|
-
{findOpen && (
|
|
906
|
-
<FindBar
|
|
907
|
-
messages={messages}
|
|
908
|
-
initial={c.findText}
|
|
909
|
-
onFind={(text, direction, caseSensitive, signal) => controller.find(text, direction, caseSensitive, signal)}
|
|
910
|
-
onClose={() => {
|
|
911
|
-
setFindOpen(false);
|
|
912
|
-
controller.findText = "";
|
|
913
|
-
controller.requestRender();
|
|
914
|
-
focusGrid();
|
|
915
|
-
}}
|
|
916
|
-
/>
|
|
917
|
-
)}
|
|
918
|
-
{notice && (
|
|
919
|
-
<div className="vg-toast" role="status">
|
|
920
|
-
{notice}
|
|
921
|
-
</div>
|
|
922
|
-
)}
|
|
923
|
-
</div>
|
|
924
|
-
{statusBar && <StatusBar controller={c} messages={messages} />}
|
|
925
|
-
{overlay?.kind === "columnMenu" && (
|
|
926
|
-
<Popover
|
|
927
|
-
anchor={overlay.anchor}
|
|
928
|
-
container={rootRef}
|
|
929
|
-
onClose={closeOverlay}
|
|
930
|
-
returnFocus={scrollerRef}
|
|
931
|
-
label={c.columnSchema(overlay.col)?.name ?? ""}
|
|
932
|
-
role="menu"
|
|
933
|
-
>
|
|
934
|
-
<MenuList items={columnMenuItems(overlay.col)} onClose={closeOverlay} />
|
|
935
|
-
</Popover>
|
|
936
|
-
)}
|
|
937
|
-
{overlay?.kind === "contextMenu" && (
|
|
938
|
-
<Popover
|
|
939
|
-
anchor={overlay.anchor}
|
|
940
|
-
container={rootRef}
|
|
941
|
-
onClose={closeOverlay}
|
|
942
|
-
returnFocus={scrollerRef}
|
|
943
|
-
label={messages.copy}
|
|
944
|
-
role="menu"
|
|
945
|
-
>
|
|
946
|
-
<MenuList items={contextMenuItems(overlay.row, overlay.col)} onClose={closeOverlay} />
|
|
947
|
-
</Popover>
|
|
948
|
-
)}
|
|
949
|
-
{overlay?.kind === "filter" && c.schema[overlay.column] && (
|
|
950
|
-
<Popover
|
|
951
|
-
anchor={overlay.anchor}
|
|
952
|
-
container={rootRef}
|
|
953
|
-
onClose={closeOverlay}
|
|
954
|
-
returnFocus={scrollerRef}
|
|
955
|
-
label={messages.filterBy(c.schema[overlay.column].name)}
|
|
956
|
-
className="vg-popover--panel"
|
|
957
|
-
>
|
|
958
|
-
<FilterPanel
|
|
959
|
-
column={overlay.column}
|
|
960
|
-
schema={c.schema[overlay.column]}
|
|
961
|
-
filter={c.filterOf(overlay.column)}
|
|
962
|
-
messages={messages}
|
|
963
|
-
formatter={c.formatter}
|
|
964
|
-
count={count}
|
|
965
|
-
load={loadDistinct}
|
|
966
|
-
onApply={(filter) => {
|
|
967
|
-
c.setFilter(overlay.column, filter);
|
|
968
|
-
setOverlay(null);
|
|
969
|
-
focusGrid();
|
|
970
|
-
}}
|
|
971
|
-
/>
|
|
972
|
-
</Popover>
|
|
973
|
-
)}
|
|
974
|
-
{overlay?.kind === "stats" && c.columnSchema(overlay.col) && (
|
|
975
|
-
<Popover
|
|
976
|
-
anchor={overlay.anchor}
|
|
977
|
-
container={rootRef}
|
|
978
|
-
onClose={closeOverlay}
|
|
979
|
-
returnFocus={scrollerRef}
|
|
980
|
-
label={messages.statistics}
|
|
981
|
-
className="vg-popover--panel"
|
|
982
|
-
>
|
|
983
|
-
<StatsPanel
|
|
984
|
-
schema={c.columnSchema(overlay.col) as NonNullable<ReturnType<typeof c.columnSchema>>}
|
|
985
|
-
messages={messages}
|
|
986
|
-
formatter={c.formatter}
|
|
987
|
-
count={count}
|
|
988
|
-
number={number}
|
|
989
|
-
load={loadStats}
|
|
990
|
-
/>
|
|
991
|
-
</Popover>
|
|
992
|
-
)}
|
|
993
|
-
{overlay?.kind === "inspector" && c.columnSchema(overlay.col) && (
|
|
994
|
-
<Popover
|
|
995
|
-
anchor={overlay.anchor}
|
|
996
|
-
container={rootRef}
|
|
997
|
-
onClose={closeOverlay}
|
|
998
|
-
returnFocus={scrollerRef}
|
|
999
|
-
label={messages.inspectCell}
|
|
1000
|
-
className="vg-popover--panel vg-popover--wide"
|
|
1001
|
-
>
|
|
1002
|
-
<CellInspector
|
|
1003
|
-
schema={c.columnSchema(overlay.col) as NonNullable<ReturnType<typeof c.columnSchema>>}
|
|
1004
|
-
messages={messages}
|
|
1005
|
-
location={messages.cell(count(overlay.row + 1), c.columnSchema(overlay.col)?.name ?? "")}
|
|
1006
|
-
load={loadCell}
|
|
1007
|
-
onCopy={(text) =>
|
|
1008
|
-
void writeClipboard(Promise.resolve(text), null).then(
|
|
1009
|
-
() => announce(messages.copied("1")),
|
|
1010
|
-
(error: unknown) => announce(error instanceof Error ? error.message : String(error)),
|
|
1011
|
-
)
|
|
1012
|
-
}
|
|
1013
|
-
/>
|
|
1014
|
-
</Popover>
|
|
1015
|
-
)}
|
|
1016
|
-
{overlay?.kind === "columns" && (
|
|
1017
|
-
<Popover
|
|
1018
|
-
anchor={overlay.anchor}
|
|
1019
|
-
container={rootRef}
|
|
1020
|
-
onClose={closeOverlay}
|
|
1021
|
-
label={messages.columns}
|
|
1022
|
-
className="vg-popover--panel"
|
|
1023
|
-
>
|
|
1024
|
-
<ColumnsPanel
|
|
1025
|
-
columns={c.columns}
|
|
1026
|
-
schema={c.schema}
|
|
1027
|
-
messages={messages}
|
|
1028
|
-
onToggle={(index, hidden) => c.setColumnHidden(index, hidden)}
|
|
1029
|
-
onShowAll={() => c.showAllColumns()}
|
|
1030
|
-
/>
|
|
1031
|
-
</Popover>
|
|
1032
|
-
)}
|
|
1033
|
-
</div>
|
|
1034
|
-
);
|
|
1035
|
-
});
|