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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentType,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
lazy,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type Ref,
|
|
7
|
+
Suspense,
|
|
8
|
+
use,
|
|
9
|
+
useEffect,
|
|
10
|
+
useEffectEvent,
|
|
11
|
+
useImperativeHandle,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
useSyncExternalStore,
|
|
16
|
+
} from "react";
|
|
17
|
+
import type { GridDocument, GridSession } from "vastgrid-spec";
|
|
18
|
+
import type { CellHooks, ColumnGroup } from "../canvas/hooks.js";
|
|
19
|
+
import type { Transport } from "../core/client.js";
|
|
20
|
+
import { createGridStore, type GridStore } from "../core/store.js";
|
|
21
|
+
import type { GridError, InitialView, OpenOptions } from "../core/types.js";
|
|
22
|
+
import { GridContext, LocaleContext, SaveContext } from "./context.js";
|
|
23
|
+
import type { DetailPanelProps } from "./detail-panel.js";
|
|
24
|
+
import { type CellActionEvent, GridBody } from "./grid-body.js";
|
|
25
|
+
import { type GridMessages, messagesFor } from "./messages.js";
|
|
26
|
+
import { useSaveAction } from "./save.js";
|
|
27
|
+
import { StaticRows } from "./static-rows.js";
|
|
28
|
+
import { StatusBar } from "./status-bar.js";
|
|
29
|
+
import { Toolbar } from "./toolbar.js";
|
|
30
|
+
|
|
31
|
+
// The find bar and the detail panel load when they first open (§10 of react.md).
|
|
32
|
+
const FindBar = lazy(() => import("./find-bar.js").then((module) => ({ default: module.FindBar })));
|
|
33
|
+
const DetailPanel = lazy(() => import("./detail-panel.js").then((module) => ({ default: module.DetailPanel })));
|
|
34
|
+
|
|
35
|
+
/** What `ref` gives (§5.5 of react.md). */
|
|
36
|
+
export interface DataGridHandle {
|
|
37
|
+
store: GridStore;
|
|
38
|
+
dispatch: GridStore["dispatch"];
|
|
39
|
+
/** Moves the focus to the grid. */
|
|
40
|
+
focus(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DataGridProps {
|
|
44
|
+
/** Where the rows come from, with `table`; a new transport or table makes a new store. */
|
|
45
|
+
transport?: Transport;
|
|
46
|
+
/** The `ref` of the table to open (§8.2). */
|
|
47
|
+
table?: string;
|
|
48
|
+
/** The `read` options of `open`. */
|
|
49
|
+
open?: OpenOptions;
|
|
50
|
+
/** A store made outside, instead of `transport` and `table`. */
|
|
51
|
+
store?: GridStore;
|
|
52
|
+
/** The view and its columns (§5.1), controlled. */
|
|
53
|
+
document?: GridDocument;
|
|
54
|
+
defaultDocument?: GridDocument;
|
|
55
|
+
onDocumentChange?: (document: GridDocument) => void;
|
|
56
|
+
/** Where the person is (§5.2), controlled. */
|
|
57
|
+
session?: GridSession;
|
|
58
|
+
defaultSession?: GridSession;
|
|
59
|
+
onSessionChange?: (session: GridSession) => void;
|
|
60
|
+
/** Rows read on a server, shown before the first read; a promise streams them (§7). */
|
|
61
|
+
initial?: InitialView | Promise<InitialView>;
|
|
62
|
+
toolbar?: boolean;
|
|
63
|
+
statusBar?: boolean;
|
|
64
|
+
/** A check box for each row in the gutter, and Space to check the active row (§5.2.1). */
|
|
65
|
+
checkboxes?: boolean;
|
|
66
|
+
/** Groups of columns named in header rows above the column names; nested groups add rows. */
|
|
67
|
+
columnGroups?: readonly ColumnGroup[];
|
|
68
|
+
/** The height of each row of groups, instead of `--vg-group-header-height`. */
|
|
69
|
+
groupHeaderHeight?: number;
|
|
70
|
+
/** How many lines rows have while a displayed column wraps its text (2 by default). */
|
|
71
|
+
wrapLines?: number;
|
|
72
|
+
/** The text of a cell instead of the grid's (§5.1: functions win over the document's formats). */
|
|
73
|
+
cellText?: CellHooks["cellText"];
|
|
74
|
+
/** The colors of a cell instead of the column's style rules. */
|
|
75
|
+
cellStyle?: CellHooks["cellStyle"];
|
|
76
|
+
/** Draws cells on the canvas, with the grid's drawing at hand (`drawDefault`). */
|
|
77
|
+
drawCell?: CellHooks["drawCell"];
|
|
78
|
+
/** Makes cells links or buttons; a click or Space on them calls `onCellAction`. */
|
|
79
|
+
cellAction?: CellHooks["cellAction"];
|
|
80
|
+
onCellAction?: (event: CellActionEvent) => void;
|
|
81
|
+
/** Shows the active row in the detail panel your own way, instead of a list of its columns. */
|
|
82
|
+
detailPanel?: ComponentType<DetailPanelProps>;
|
|
83
|
+
rowHeight?: number;
|
|
84
|
+
headerHeight?: number;
|
|
85
|
+
/** The language of the UI, a BCP 47 tag; the browser's by default. */
|
|
86
|
+
locale?: string;
|
|
87
|
+
messages?: Partial<GridMessages>;
|
|
88
|
+
/** Failures that belong to no command (§4.2). */
|
|
89
|
+
onError?: (error: GridError) => void;
|
|
90
|
+
ref?: Ref<DataGridHandle>;
|
|
91
|
+
className?: string;
|
|
92
|
+
style?: CSSProperties;
|
|
93
|
+
"aria-label"?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const transportIds = new WeakMap<object, number>();
|
|
97
|
+
let nextTransportId = 1;
|
|
98
|
+
|
|
99
|
+
/** A number for each transport, for the key that makes a new store when the transport changes. */
|
|
100
|
+
function transportId(transport: object) {
|
|
101
|
+
let id = transportIds.get(transport);
|
|
102
|
+
if (id === undefined) {
|
|
103
|
+
id = nextTransportId++;
|
|
104
|
+
transportIds.set(transport, id);
|
|
105
|
+
}
|
|
106
|
+
return id;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPromise<T>(value: T | Promise<T> | undefined): value is Promise<T> {
|
|
110
|
+
return typeof (value as { then?: unknown } | undefined)?.then === "function";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const noSubscription = () => () => {};
|
|
114
|
+
const browserLocale = () => navigator.language;
|
|
115
|
+
const serverLocale = () => undefined;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A data grid for tables of any size (§5.1 of react.md): it draws on a canvas, reads rows through a
|
|
119
|
+
* transport as you scroll, and is driven by the commands of the specification.
|
|
120
|
+
*/
|
|
121
|
+
export function DataGrid(props: DataGridProps) {
|
|
122
|
+
if (props.store) return <Frame {...props} store={props.store} />;
|
|
123
|
+
const { transport, table } = props;
|
|
124
|
+
if (!transport || table === undefined) throw new Error("DataGrid needs a store, or a transport and a table.");
|
|
125
|
+
const key = `${transportId(transport)}\u0000${table}`;
|
|
126
|
+
if (isPromise(props.initial)) {
|
|
127
|
+
return (
|
|
128
|
+
<Suspense fallback={<div className="vg-root" aria-busy="true" />}>
|
|
129
|
+
<OwnStore key={key} {...props} transport={transport} table={table} />
|
|
130
|
+
</Suspense>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return <OwnStore key={key} {...props} transport={transport} table={table} />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A grid with a store of its own, made once and connected while it is mounted. */
|
|
137
|
+
function OwnStore(props: DataGridProps & { transport: Transport; table: string }) {
|
|
138
|
+
const initial = isPromise(props.initial) ? use(props.initial) : props.initial;
|
|
139
|
+
const [store] = useState(() =>
|
|
140
|
+
createGridStore({
|
|
141
|
+
transport: props.transport,
|
|
142
|
+
table: props.table,
|
|
143
|
+
...(props.open ? { open: props.open } : {}),
|
|
144
|
+
...((props.document ?? props.defaultDocument) ? { document: props.document ?? props.defaultDocument } : {}),
|
|
145
|
+
...((props.session ?? props.defaultSession) ? { session: props.session ?? props.defaultSession } : {}),
|
|
146
|
+
...(initial ? { initial } : {}),
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
useEffect(() => store.connect(), [store]);
|
|
150
|
+
return <Frame {...props} store={store} />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Makes a part of the state controlled by a prop (§5.2), and reports the changes that commands make. */
|
|
154
|
+
function useControlled<T extends GridDocument | GridSession>(
|
|
155
|
+
store: GridStore,
|
|
156
|
+
part: "document" | "session",
|
|
157
|
+
value: T | undefined,
|
|
158
|
+
onChange: ((value: T) => void) | undefined,
|
|
159
|
+
) {
|
|
160
|
+
const controlled = value !== undefined;
|
|
161
|
+
const report = useEffectEvent((next: T) => onChange?.(next));
|
|
162
|
+
useLayoutEffect(() => {
|
|
163
|
+
store.control(part, controlled);
|
|
164
|
+
}, [store, part, controlled]);
|
|
165
|
+
// In the commit that brings the prop, before the canvas draws.
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
if (value === undefined) return;
|
|
168
|
+
if (part === "document") store.setDocument(value as GridDocument);
|
|
169
|
+
else store.setSession(value as GridSession);
|
|
170
|
+
}, [store, part, value]);
|
|
171
|
+
useEffect(() => store.on(part, (next) => report(next as T)), [store, part]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Frame({
|
|
175
|
+
store,
|
|
176
|
+
toolbar = true,
|
|
177
|
+
statusBar = true,
|
|
178
|
+
checkboxes = false,
|
|
179
|
+
columnGroups,
|
|
180
|
+
groupHeaderHeight,
|
|
181
|
+
wrapLines,
|
|
182
|
+
cellText,
|
|
183
|
+
cellStyle,
|
|
184
|
+
drawCell,
|
|
185
|
+
cellAction,
|
|
186
|
+
onCellAction,
|
|
187
|
+
detailPanel,
|
|
188
|
+
className,
|
|
189
|
+
style,
|
|
190
|
+
ref,
|
|
191
|
+
rowHeight,
|
|
192
|
+
headerHeight,
|
|
193
|
+
locale: given,
|
|
194
|
+
messages: replaced,
|
|
195
|
+
document,
|
|
196
|
+
onDocumentChange,
|
|
197
|
+
session,
|
|
198
|
+
onSessionChange,
|
|
199
|
+
onError,
|
|
200
|
+
"aria-label": ariaLabel,
|
|
201
|
+
}: DataGridProps & { store: GridStore }) {
|
|
202
|
+
const detected = useSyncExternalStore(noSubscription, browserLocale, serverLocale);
|
|
203
|
+
const locale = given ?? detected;
|
|
204
|
+
const base = messagesFor(locale);
|
|
205
|
+
const messages = replaced ? { ...base, ...replaced } : base;
|
|
206
|
+
const language = { messages, locale, number: new Intl.NumberFormat(locale) };
|
|
207
|
+
const label = ariaLabel ?? messages.grid;
|
|
208
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
209
|
+
const [notice, setNotice] = useState("");
|
|
210
|
+
const [finding, setFinding] = useState(false);
|
|
211
|
+
const detailOpen = useGridSelectorOf(store, (snapshot) => snapshot.session.detail?.open === true);
|
|
212
|
+
const saving = useSaveAction(store, language, setNotice);
|
|
213
|
+
const status = useGridSelectorOf(store, (snapshot) => snapshot.status);
|
|
214
|
+
useControlled(store, "document", document, onDocumentChange);
|
|
215
|
+
useControlled(store, "session", session, onSessionChange);
|
|
216
|
+
const reportError = useEffectEvent((error: GridError) => onError?.(error));
|
|
217
|
+
useEffect(() => store.on("error", (error) => reportError(error)), [store]);
|
|
218
|
+
useImperativeHandle(
|
|
219
|
+
ref,
|
|
220
|
+
() => ({
|
|
221
|
+
store,
|
|
222
|
+
dispatch: store.dispatch,
|
|
223
|
+
focus: () => scrollerRef.current?.focus(),
|
|
224
|
+
}),
|
|
225
|
+
[store],
|
|
226
|
+
);
|
|
227
|
+
return (
|
|
228
|
+
<GridContext value={store}>
|
|
229
|
+
<LocaleContext value={language}>
|
|
230
|
+
<SaveContext value={saving}>
|
|
231
|
+
<div className={className ? `vg-root ${className}` : "vg-root"} style={style} data-status={status}>
|
|
232
|
+
{toolbar && <Toolbar announce={setNotice} />}
|
|
233
|
+
{finding && (
|
|
234
|
+
<Suspense fallback={null}>
|
|
235
|
+
<FindBar onClose={() => setFinding(false)} scrollerRef={scrollerRef} />
|
|
236
|
+
</Suspense>
|
|
237
|
+
)}
|
|
238
|
+
<div className="vg-main">
|
|
239
|
+
<Suspense fallback={<StaticRows label={label} />}>
|
|
240
|
+
<GridBody
|
|
241
|
+
rowHeight={rowHeight}
|
|
242
|
+
headerHeight={headerHeight}
|
|
243
|
+
checkboxes={checkboxes}
|
|
244
|
+
columnGroups={columnGroups}
|
|
245
|
+
groupHeaderHeight={groupHeaderHeight}
|
|
246
|
+
wrapLines={wrapLines}
|
|
247
|
+
hooks={{ cellText, cellStyle, drawCell, cellAction }}
|
|
248
|
+
onCellAction={onCellAction}
|
|
249
|
+
onFind={() => setFinding(true)}
|
|
250
|
+
label={label}
|
|
251
|
+
scrollerRef={scrollerRef}
|
|
252
|
+
announce={setNotice}
|
|
253
|
+
/>
|
|
254
|
+
</Suspense>
|
|
255
|
+
{detailOpen && (
|
|
256
|
+
<Suspense fallback={null}>
|
|
257
|
+
<DetailPanel
|
|
258
|
+
custom={detailPanel}
|
|
259
|
+
onClosed={() => scrollerRef.current?.focus({ preventScroll: true })}
|
|
260
|
+
/>
|
|
261
|
+
</Suspense>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
{statusBar && (
|
|
265
|
+
<StatusBar announce={setNotice} onDone={() => scrollerRef.current?.focus({ preventScroll: true })} />
|
|
266
|
+
)}
|
|
267
|
+
<div className="vg-sr-only" aria-live="polite">
|
|
268
|
+
{notice}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</SaveContext>
|
|
272
|
+
</LocaleContext>
|
|
273
|
+
</GridContext>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** `useGridSelector` for a store given directly, before the context holds it. */
|
|
278
|
+
function useGridSelectorOf<T>(store: GridStore, selector: (snapshot: ReturnType<GridStore["getSnapshot"]>) => T): T {
|
|
279
|
+
const read = () => selector(store.getSnapshot());
|
|
280
|
+
return useSyncExternalStore(store.subscribe, read, read);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Gives a store to the hooks of the components inside (§5.4 of react.md). */
|
|
284
|
+
export function GridProvider({ store, children }: { store: GridStore; children?: ReactNode }) {
|
|
285
|
+
return <GridContext value={store}>{children}</GridContext>;
|
|
286
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type ComponentType, use } from "react";
|
|
2
|
+
import { type Column, type JsonValue, type RowKey, textOf } from "vastgrid-spec";
|
|
3
|
+
import { formatterFor } from "../core/format.js";
|
|
4
|
+
import { LocaleContext } from "./context.js";
|
|
5
|
+
import { useGridRows, useGridSelector, useGridStore } from "./hooks.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The detail panel of the active row (§5.2 of the specification, `detail`): every column of the view, hidden ones
|
|
9
|
+
* included, with its label and its value in full, by the column's format. It follows the active cell, reads the
|
|
10
|
+
* row's values that the canvas did not (`useGridRows`), and is opened and closed by `detail.open` and
|
|
11
|
+
* `detail.close`; the focus stays in the grid, and goes back to it when the panel is closed from inside (× or Esc).
|
|
12
|
+
*/
|
|
13
|
+
export function DetailPanel({
|
|
14
|
+
custom,
|
|
15
|
+
onClosed,
|
|
16
|
+
}: {
|
|
17
|
+
custom?: ComponentType<DetailPanelProps>;
|
|
18
|
+
/** Called when the panel is closed from inside it, to give the focus back to the grid. */
|
|
19
|
+
onClosed: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
const open = useGridSelector((snapshot) => snapshot.session.detail?.open === true);
|
|
22
|
+
// Closed, it follows no rows: scrolling commits nothing (§10 of react.md).
|
|
23
|
+
return open ? <OpenDetail Custom={custom} onClosed={onClosed} /> : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What a host's own detail panel (`detailPanel`) gets: the active row's position, the view's columns and values. */
|
|
27
|
+
export interface DetailPanelProps {
|
|
28
|
+
row: number;
|
|
29
|
+
columns: readonly Column[];
|
|
30
|
+
values: readonly JsonValue[];
|
|
31
|
+
/** The row's key (§5.3), when it is read. */
|
|
32
|
+
rowKey: RowKey | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function OpenDetail({ Custom, onClosed }: { Custom?: ComponentType<DetailPanelProps>; onClosed: () => void }) {
|
|
36
|
+
const store = useGridStore();
|
|
37
|
+
const { messages, number, locale } = use(LocaleContext);
|
|
38
|
+
const row = useGridSelector((snapshot) => snapshot.session.active?.row ?? 0);
|
|
39
|
+
const rowCount = useGridSelector((snapshot) => snapshot.view?.rowCount ?? 0);
|
|
40
|
+
const columns = useGridSelector((snapshot) => snapshot.view?.columns);
|
|
41
|
+
const settings = useGridSelector((snapshot) => snapshot.document.columns);
|
|
42
|
+
const names = columns && rowCount > 0 ? columns.map((column) => column.name) : [];
|
|
43
|
+
const [values] = useGridRows({ start: row, end: names.length > 0 ? row + 1 : row, columns: names });
|
|
44
|
+
if (!columns) return null;
|
|
45
|
+
const byName = new Map((settings ?? []).map((setting) => [setting.name, setting]));
|
|
46
|
+
const text = (value: JsonValue | undefined, index: number) => {
|
|
47
|
+
if (value === undefined) return messages.loading;
|
|
48
|
+
if (value === null) return "";
|
|
49
|
+
const column = columns[index];
|
|
50
|
+
const format = byName.get(column.name)?.format;
|
|
51
|
+
return formatterFor(column, format, locale)?.(value) ?? textOf(value, column) ?? "";
|
|
52
|
+
};
|
|
53
|
+
const title = rowCount > 0 ? messages.detailOf(number.format(row + 1)) : messages.details;
|
|
54
|
+
const close = () => {
|
|
55
|
+
void store.dispatch({ command: "detail.close" });
|
|
56
|
+
onClosed();
|
|
57
|
+
};
|
|
58
|
+
return (
|
|
59
|
+
<aside
|
|
60
|
+
className="vg-detail"
|
|
61
|
+
aria-label={title}
|
|
62
|
+
onKeyDown={(event) => {
|
|
63
|
+
if (event.key !== "Escape" || event.nativeEvent.isComposing) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
close();
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<header className="vg-detail__header">
|
|
69
|
+
<h2 className="vg-detail__title">{title}</h2>
|
|
70
|
+
<button type="button" className="vg-button vg-button--small" aria-label={messages.closeDetails} onClick={close}>
|
|
71
|
+
×
|
|
72
|
+
</button>
|
|
73
|
+
</header>
|
|
74
|
+
{rowCount > 0 && Custom && values?.every((value) => value !== undefined) ? (
|
|
75
|
+
<Custom row={row} columns={columns} values={values as JsonValue[]} rowKey={store.rowKey(row)} />
|
|
76
|
+
) : rowCount > 0 && Custom ? (
|
|
77
|
+
<p className="vg-filter__note">{messages.loading}</p>
|
|
78
|
+
) : (
|
|
79
|
+
rowCount > 0 && (
|
|
80
|
+
<dl className="vg-detail__list">
|
|
81
|
+
{columns.map((column, index) => (
|
|
82
|
+
<div key={column.name} className="vg-detail__item">
|
|
83
|
+
<dt>{byName.get(column.name)?.format?.label ?? column.name}</dt>
|
|
84
|
+
<dd>{text(values?.[index], index)}</dd>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</dl>
|
|
88
|
+
)
|
|
89
|
+
)}
|
|
90
|
+
</aside>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type ToggleEvent, use, useId, useRef, useState, useTransition } from "react";
|
|
2
|
+
import { CommandError } from "vastgrid-spec";
|
|
3
|
+
import { LocaleContext } from "./context.js";
|
|
4
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
5
|
+
|
|
6
|
+
/** The media types of the formats of §8.2, for files that an engine answers as data. */
|
|
7
|
+
const MEDIA_TYPES: Record<string, string> = {
|
|
8
|
+
csv: "text/csv;charset=utf-8",
|
|
9
|
+
tsv: "text/tab-separated-values;charset=utf-8",
|
|
10
|
+
json: "application/json;charset=utf-8",
|
|
11
|
+
parquet: "application/vnd.apache.parquet",
|
|
12
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Downloads a file at a URL, as a link with `download` does: under `name`, or the name the server gives. */
|
|
16
|
+
function download(url: string, name = "") {
|
|
17
|
+
const link = document.createElement("a");
|
|
18
|
+
link.href = url;
|
|
19
|
+
link.download = name;
|
|
20
|
+
link.rel = "noopener";
|
|
21
|
+
document.body.append(link);
|
|
22
|
+
link.click();
|
|
23
|
+
link.remove();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The toolbar's export menu (§6.6 of the specification): the rows of the view or the whole table, with the hidden
|
|
28
|
+
* columns or not, in one of the formats the engine exports. It runs as an Action: the button shows that it runs, and
|
|
29
|
+
* the outcome is announced. An export without a target is downloaded from where the transport gives it
|
|
30
|
+
* (`Transport.exportUrl`), or, from an engine that answers the file's bytes (`delivery` `data`), as a file made of
|
|
31
|
+
* them.
|
|
32
|
+
*/
|
|
33
|
+
export function ExportMenu({ announce }: { announce: (text: string) => void }) {
|
|
34
|
+
const store = useGridStore();
|
|
35
|
+
const { messages, number } = use(LocaleContext);
|
|
36
|
+
const formats = useGridSelector((snapshot) => snapshot.table?.capabilities.export?.formats);
|
|
37
|
+
const [rows, setRows] = useState<"view" | "table">("view");
|
|
38
|
+
const [allColumns, setAllColumns] = useState(false);
|
|
39
|
+
const [pending, startExport] = useTransition();
|
|
40
|
+
const id = useId();
|
|
41
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
42
|
+
if (!formats || formats.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
// The menu opens under its button, its right edge at the button's (popovers open in the middle otherwise).
|
|
45
|
+
const place = (event: ToggleEvent<HTMLDivElement>) => {
|
|
46
|
+
const button = buttonRef.current;
|
|
47
|
+
if (event.newState !== "open" || !button) return;
|
|
48
|
+
const rect = button.getBoundingClientRect();
|
|
49
|
+
const menu = event.currentTarget;
|
|
50
|
+
menu.style.top = `${rect.bottom + 4}px`;
|
|
51
|
+
menu.style.left = `${Math.max(8, rect.right - menu.offsetWidth)}px`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const run = (format: string) => {
|
|
55
|
+
document.getElementById(id)?.hidePopover?.();
|
|
56
|
+
startExport(async () => {
|
|
57
|
+
const outcome = await store.dispatch({
|
|
58
|
+
command: "export",
|
|
59
|
+
format,
|
|
60
|
+
rows,
|
|
61
|
+
columns: allColumns ? "all" : "displayed",
|
|
62
|
+
});
|
|
63
|
+
if (!outcome.ok) {
|
|
64
|
+
const reason = outcome.error instanceof CommandError ? outcome.error.reason : null;
|
|
65
|
+
announce(messages.failed(reason === "not_loaded" ? messages.loading : outcome.error.message));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const result = outcome.output as { target: string | null; rows: number; data?: { bytes: Uint8Array } };
|
|
69
|
+
if (result.data) {
|
|
70
|
+
const url = URL.createObjectURL(
|
|
71
|
+
new Blob([result.data.bytes as Uint8Array<ArrayBuffer>], { type: MEDIA_TYPES[format] ?? "" }),
|
|
72
|
+
);
|
|
73
|
+
download(url, store.exportName(format));
|
|
74
|
+
// The link has started the download; the browser keeps the file's bytes until it is done.
|
|
75
|
+
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
76
|
+
} else {
|
|
77
|
+
const url = result.target === null ? null : store.exportUrl(result.target);
|
|
78
|
+
if (url) download(url);
|
|
79
|
+
}
|
|
80
|
+
announce(messages.exported(number.format(result.rows)));
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<button ref={buttonRef} type="button" className="vg-button" popoverTarget={id} disabled={pending}>
|
|
87
|
+
{pending ? messages.exporting : messages.export}
|
|
88
|
+
</button>
|
|
89
|
+
<div id={id} popover="auto" role="dialog" className="vg-menu" aria-label={messages.export} onToggle={place}>
|
|
90
|
+
<fieldset className="vg-menu__group">
|
|
91
|
+
<label>
|
|
92
|
+
<input type="radio" name={`${id}-rows`} checked={rows === "view"} onChange={() => setRows("view")} />
|
|
93
|
+
{messages.exportView}
|
|
94
|
+
</label>
|
|
95
|
+
<label>
|
|
96
|
+
<input type="radio" name={`${id}-rows`} checked={rows === "table"} onChange={() => setRows("table")} />
|
|
97
|
+
{messages.exportTable}
|
|
98
|
+
</label>
|
|
99
|
+
<label>
|
|
100
|
+
<input type="checkbox" checked={allColumns} onChange={(event) => setAllColumns(event.target.checked)} />
|
|
101
|
+
{messages.exportAllColumns}
|
|
102
|
+
</label>
|
|
103
|
+
</fieldset>
|
|
104
|
+
{formats.map((format) => (
|
|
105
|
+
<button key={format} type="button" className="vg-menu__item" onClick={() => run(format)}>
|
|
106
|
+
{messages.exportFormats[format] ?? format}
|
|
107
|
+
</button>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type KeyboardEvent, type RefObject, use, useLayoutEffect, useRef, useState, useTransition } from "react";
|
|
2
|
+
import { LocaleContext } from "./context.js";
|
|
3
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The find bar (⌘F or Ctrl+F on the grid): finds the next cell of the view whose text contains what is typed, from
|
|
7
|
+
* the active cell, in the displayed columns (`GridStore.find`, `find` of §8.2), and makes it the active cell. Enter
|
|
8
|
+
* finds the next one, Shift+Enter the one before, and Esc closes the bar and gives the focus back to the grid.
|
|
9
|
+
*/
|
|
10
|
+
export function FindBar({
|
|
11
|
+
onClose,
|
|
12
|
+
scrollerRef,
|
|
13
|
+
}: {
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
scrollerRef: RefObject<HTMLDivElement | null>;
|
|
16
|
+
}) {
|
|
17
|
+
const store = useGridStore();
|
|
18
|
+
const { messages } = use(LocaleContext);
|
|
19
|
+
const finds = useGridSelector((snapshot) => snapshot.table?.capabilities.find === true);
|
|
20
|
+
const [text, setText] = useState("");
|
|
21
|
+
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
22
|
+
const [status, setStatus] = useState("");
|
|
23
|
+
const [pending, startFind] = useTransition();
|
|
24
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
25
|
+
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
inputRef.current?.focus();
|
|
28
|
+
inputRef.current?.select();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const close = () => {
|
|
32
|
+
onClose();
|
|
33
|
+
scrollerRef.current?.focus({ preventScroll: true });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const find = (direction: 1 | -1) => {
|
|
37
|
+
if (text === "") return;
|
|
38
|
+
startFind(async () => {
|
|
39
|
+
const { session, displayed } = store.getSnapshot();
|
|
40
|
+
const from = session.active ?? { row: 0, column: displayed[0]?.column.name ?? "" };
|
|
41
|
+
try {
|
|
42
|
+
const found = await store.find(text, { from, direction, caseSensitive });
|
|
43
|
+
if (!found) {
|
|
44
|
+
setStatus(messages.noMatches);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setStatus("");
|
|
48
|
+
await store.dispatch({ command: "select", row: found.row, column: found.column });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
setStatus(messages.failed(error instanceof Error ? error.message : String(error)));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
56
|
+
// Keys that end an IME's composition are the IME's.
|
|
57
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
58
|
+
if (event.key === "Escape") {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
close();
|
|
61
|
+
} else if (event.key === "Enter" && event.target === inputRef.current) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
find(event.shiftKey ? -1 : 1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (!finds) return null;
|
|
68
|
+
return (
|
|
69
|
+
<search className="vg-find" aria-label={messages.find} onKeyDown={onKeyDown}>
|
|
70
|
+
<input
|
|
71
|
+
ref={inputRef}
|
|
72
|
+
type="search"
|
|
73
|
+
className="vg-input vg-find__input"
|
|
74
|
+
aria-label={messages.find}
|
|
75
|
+
placeholder={messages.find}
|
|
76
|
+
value={text}
|
|
77
|
+
onChange={(event) => {
|
|
78
|
+
setText(event.target.value);
|
|
79
|
+
setStatus("");
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
<label className="vg-filter__value">
|
|
83
|
+
<input type="checkbox" checked={caseSensitive} onChange={(event) => setCaseSensitive(event.target.checked)} />
|
|
84
|
+
<span>{messages.matchCase}</span>
|
|
85
|
+
</label>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="vg-button vg-button--small"
|
|
89
|
+
aria-label={messages.findPrevious}
|
|
90
|
+
disabled={pending || text === ""}
|
|
91
|
+
onClick={() => find(-1)}
|
|
92
|
+
>
|
|
93
|
+
↑
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="vg-button vg-button--small"
|
|
98
|
+
aria-label={messages.findNext}
|
|
99
|
+
disabled={pending || text === ""}
|
|
100
|
+
onClick={() => find(1)}
|
|
101
|
+
>
|
|
102
|
+
↓
|
|
103
|
+
</button>
|
|
104
|
+
<span className="vg-find__status" role="status">
|
|
105
|
+
{status}
|
|
106
|
+
</span>
|
|
107
|
+
<button type="button" className="vg-button vg-button--small" aria-label={messages.closeFind} onClick={close}>
|
|
108
|
+
×
|
|
109
|
+
</button>
|
|
110
|
+
</search>
|
|
111
|
+
);
|
|
112
|
+
}
|