@inkbar/react 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -5
- package/dist/index.d.mts +325 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +969 -52
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { InkError, createMemorySignatureStore, createSignatureLibrary, createSnapshotHistory, isAbandonedNote, openDocument, readonly, repliesTo, signField, threadOf, threadRoot, writable } from "@inkbar/core";
|
|
4
|
-
import { COLOR_COLUMNS, FONT_FAMILIES, FORM_LABELS, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, attachHistoryKeys, colorMenuModel, createFocusReturn, createPluginHost, createSearchController, createSignatureDraft, createToolOptions, cssVariableDefaults, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, keepHistoryFocus, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, shapeMenuModel, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, viewerSearchOf, watchDetached } from "@inkbar/core/dom";
|
|
4
|
+
import { COLOR_COLUMNS, FONT_FAMILIES, FORM_LABELS, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, THUMBNAIL_WIDTH, TOOL_LABELS, ZOOM_LEVELS, attachFileKeys, attachHistoryKeys, colorMenuModel, createFocusReturn, createPluginHost, createRecordListController, createSearchController, createSidebarController, createSignatureDraft, createThumbnailPainter, createThumbnailsController, createToolOptions, cssVariableDefaults, downloadDocument, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleResultsFieldKey, handleTabKeys, highlightMenuModel, isBlankQuery, keepHistoryFocus, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, printDocument, recordsSource, searchSource, shapeMenuModel, shouldKeepOpenOnAnnotation, sidebarBadge, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, unknownTool, viewerSearchOf, watchDetached, withHostTools } from "@inkbar/core/dom";
|
|
5
5
|
import { Dialog, Popover, Select, Slot, Toolbar } from "radix-ui";
|
|
6
6
|
import { createPortal, flushSync } from "react-dom";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp,
|
|
8
|
+
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Download, ICON_ATTRIBUTES, Italic, Minus, Plus, Printer, Redo2, Search, Signature, SquareDashedMousePointer, TextCursor, Trash2, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
9
9
|
//#region src/context.ts
|
|
10
10
|
const InkContextObject = createContext(null);
|
|
11
11
|
/**
|
|
@@ -19,14 +19,14 @@ function useInkContext(consumer) {
|
|
|
19
19
|
}
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/use-readable.ts
|
|
22
|
-
const noop$
|
|
22
|
+
const noop$3 = () => void 0;
|
|
23
23
|
/**
|
|
24
24
|
* Mirrors a core `Readable` into React. A missing readable yields `fallback`, which must be a
|
|
25
25
|
* stable reference (a module constant): `useSyncExternalStore` compares snapshots by identity
|
|
26
26
|
* and loops on a fresh value. On the server the fallback is rendered.
|
|
27
27
|
*/
|
|
28
28
|
function useReadable(readable, fallback) {
|
|
29
|
-
const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop$
|
|
29
|
+
const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop$3, [readable]);
|
|
30
30
|
const get = useCallback(() => readable ? readable.get() : fallback, [readable, fallback]);
|
|
31
31
|
const server = useCallback(() => fallback, [fallback]);
|
|
32
32
|
return useSyncExternalStore(subscribe, get, server);
|
|
@@ -87,7 +87,7 @@ function useTool() {
|
|
|
87
87
|
//#region src/hooks/useToolStyle.ts
|
|
88
88
|
const NO_STYLE = {};
|
|
89
89
|
const NO_PROPS = /* @__PURE__ */ new Set();
|
|
90
|
-
const EMPTY = {};
|
|
90
|
+
const EMPTY$1 = {};
|
|
91
91
|
function useToolStyle(group) {
|
|
92
92
|
const style = useInkContext("useToolStyle()").toolStyle;
|
|
93
93
|
const scope = group && style ? style.scoped(group) : void 0;
|
|
@@ -98,7 +98,7 @@ function useToolStyle(group) {
|
|
|
98
98
|
current,
|
|
99
99
|
supported,
|
|
100
100
|
set: (patch, opts) => scope ? scope.set(patch) : style?.set(patch, opts),
|
|
101
|
-
get: (g) => style?.get(g) ?? EMPTY
|
|
101
|
+
get: (g) => style?.get(g) ?? EMPTY$1
|
|
102
102
|
}), [
|
|
103
103
|
current,
|
|
104
104
|
supported,
|
|
@@ -156,7 +156,7 @@ const CLOSED = {
|
|
|
156
156
|
current: -1,
|
|
157
157
|
scanning: false
|
|
158
158
|
};
|
|
159
|
-
const noop$
|
|
159
|
+
const noop$2 = () => void 0;
|
|
160
160
|
/**
|
|
161
161
|
* The document search for hosts building their own control: the state, and the actions the
|
|
162
162
|
* find bar uses. Search is set up once InkRoot has mounted, so until then (and on the server)
|
|
@@ -167,12 +167,13 @@ function useSearch() {
|
|
|
167
167
|
const search = useInkContext("useSearch()").search;
|
|
168
168
|
return {
|
|
169
169
|
state: useReadable(search?.state, CLOSED),
|
|
170
|
-
open: search ? (query) => search.open(query) : noop$
|
|
171
|
-
setQuery: search ? (query) => search.setQuery(query) : noop$
|
|
172
|
-
setMatchCase: search ? (on) => search.setMatchCase(on) : noop$
|
|
173
|
-
next: search ? () => search.next() : noop$
|
|
174
|
-
previous: search ? () => search.previous() : noop$
|
|
175
|
-
|
|
170
|
+
open: search ? (query) => search.open(query) : noop$2,
|
|
171
|
+
setQuery: search ? (query) => search.setQuery(query) : noop$2,
|
|
172
|
+
setMatchCase: search ? (on) => search.setMatchCase(on) : noop$2,
|
|
173
|
+
next: search ? () => search.next() : noop$2,
|
|
174
|
+
previous: search ? () => search.previous() : noop$2,
|
|
175
|
+
select: search ? (index) => search.select(index) : noop$2,
|
|
176
|
+
close: search ? () => search.close() : noop$2
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
//#endregion
|
|
@@ -209,9 +210,9 @@ function useHistory() {
|
|
|
209
210
|
}
|
|
210
211
|
//#endregion
|
|
211
212
|
//#region src/hooks/useEmbedded.ts
|
|
212
|
-
const NONE = [];
|
|
213
|
+
const NONE$1 = [];
|
|
213
214
|
function useEmbedded() {
|
|
214
|
-
const summary = useInkContext("useEmbedded()").document?.embedded ?? NONE;
|
|
215
|
+
const summary = useInkContext("useEmbedded()").document?.embedded ?? NONE$1;
|
|
215
216
|
return useMemo(() => ({
|
|
216
217
|
summary,
|
|
217
218
|
total: summary.reduce((n, s) => n + s.count, 0),
|
|
@@ -277,6 +278,41 @@ function useForm() {
|
|
|
277
278
|
]);
|
|
278
279
|
}
|
|
279
280
|
//#endregion
|
|
281
|
+
//#region src/hooks/useRecords.ts
|
|
282
|
+
const NONE = Object.freeze([]);
|
|
283
|
+
/**
|
|
284
|
+
* The records a plugin keeps under `key` (`<plugin>.<name>`), for host components: the live list
|
|
285
|
+
* for the open document, and the handle to read and write them, each write one undo step.
|
|
286
|
+
* Plugins are set up once InkRoot has mounted, so until then (and on the server) `handle` is
|
|
287
|
+
* null and `records` is empty. `T` is the plugin's record type.
|
|
288
|
+
*/
|
|
289
|
+
function useRecords(key) {
|
|
290
|
+
const { pluginHost } = useInkContext("useRecords()");
|
|
291
|
+
const handle = pluginHost?.records(key) ?? null;
|
|
292
|
+
return {
|
|
293
|
+
records: useReadable(handle?.list, NONE),
|
|
294
|
+
handle
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/hooks/useRecordSelection.ts
|
|
299
|
+
const noop$1 = () => void 0;
|
|
300
|
+
/**
|
|
301
|
+
* The selected record under `key` (`<plugin>.<name>`): the one selection the marks and every
|
|
302
|
+
* record list over those records share. `select` takes an id, or null to clear; an id that has
|
|
303
|
+
* no mark changes nothing. Until InkRoot has mounted (and on the server) nothing is selected
|
|
304
|
+
* and `select` does nothing.
|
|
305
|
+
*/
|
|
306
|
+
function useRecordSelection(key) {
|
|
307
|
+
const { pluginHost } = useInkContext("useRecordSelection()");
|
|
308
|
+
const selection = pluginHost?.selection(key) ?? null;
|
|
309
|
+
const select = useCallback((id) => selection ? selection.select(id) : noop$1(), [selection]);
|
|
310
|
+
return {
|
|
311
|
+
selected: useReadable(selection?.selected, null),
|
|
312
|
+
select
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
280
316
|
//#region src/use-latest.ts
|
|
281
317
|
/**
|
|
282
318
|
* A ref that always holds the latest render's value, for callbacks that outlive a render
|
|
@@ -292,6 +328,51 @@ function useLatest(value) {
|
|
|
292
328
|
return ref;
|
|
293
329
|
}
|
|
294
330
|
//#endregion
|
|
331
|
+
//#region src/hooks/useRegisteredTool.ts
|
|
332
|
+
/** `select-text` and `select`, then the host's tools, then the plugins' tools, by id. */
|
|
333
|
+
function toolIdsOf(hostTools, pluginHost) {
|
|
334
|
+
return withHostTools([...hostTools ?? [], ...pluginHost?.tools ?? []]).map((t) => t.id);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The tools a control may use: those in `tools` and the plugins now that the open viewer was
|
|
338
|
+
* also built with, so a control never offers a tool the viewer would refuse. A tool added to
|
|
339
|
+
* `tools` counts from the next open; one removed stops at once. With no viewer, `tools` and
|
|
340
|
+
* the plugins alone.
|
|
341
|
+
*/
|
|
342
|
+
function registeredToolIds(hostTools, pluginHost, opened) {
|
|
343
|
+
const ids = toolIdsOf(hostTools, pluginHost);
|
|
344
|
+
return opened ? ids.filter((id) => opened.includes(id)) : ids;
|
|
345
|
+
}
|
|
346
|
+
const idsIn = (ctx) => registeredToolIds(ctx.hostTools, ctx.pluginHost, ctx.openedToolIds);
|
|
347
|
+
/** Whether a tool by this id is registered, without logging anything when it is not. */
|
|
348
|
+
function useHasTool(id) {
|
|
349
|
+
return idsIn(useInkContext("useHasTool()")).includes(id);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Whether a tool by this id is registered: always for `select-text` and `select`, else when
|
|
353
|
+
* InkRoot's `tools` or a plugin has it. When it is not, once the plugins are set up, this logs
|
|
354
|
+
* `InkError('UnknownTool')` to the console, once per component.
|
|
355
|
+
*/
|
|
356
|
+
function useRegisteredTool(id) {
|
|
357
|
+
const ctx = useInkContext("useRegisteredTool()");
|
|
358
|
+
const ids = idsIn(ctx);
|
|
359
|
+
const registered = ids.includes(id);
|
|
360
|
+
const ready = ctx.pluginHost !== null;
|
|
361
|
+
const logged = useRef(false);
|
|
362
|
+
const latestIds = useLatest(ids);
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!ready || registered || logged.current) return;
|
|
365
|
+
logged.current = true;
|
|
366
|
+
console.error(unknownTool(id, latestIds.current));
|
|
367
|
+
}, [
|
|
368
|
+
ready,
|
|
369
|
+
registered,
|
|
370
|
+
id,
|
|
371
|
+
latestIds
|
|
372
|
+
]);
|
|
373
|
+
return registered;
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
295
376
|
//#region src/components/InkRoot.tsx
|
|
296
377
|
const IDLE$1 = { state: "idle" };
|
|
297
378
|
const NO_ANNOTATIONS = [];
|
|
@@ -304,7 +385,7 @@ function identity(src) {
|
|
|
304
385
|
if ("key" in src) return src.key;
|
|
305
386
|
return src;
|
|
306
387
|
}
|
|
307
|
-
function isDevelopment() {
|
|
388
|
+
function isDevelopment$1() {
|
|
308
389
|
try {
|
|
309
390
|
return process.env.NODE_ENV !== "production";
|
|
310
391
|
} catch {
|
|
@@ -312,7 +393,7 @@ function isDevelopment() {
|
|
|
312
393
|
}
|
|
313
394
|
}
|
|
314
395
|
function setTestHook(el, doc) {
|
|
315
|
-
if (el && isDevelopment()) el.__inkDocument = doc;
|
|
396
|
+
if (el && isDevelopment$1()) el.__inkDocument = doc;
|
|
316
397
|
}
|
|
317
398
|
function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsChange, onChange, fields, onFieldsChange, onFormAction, formHighlight, tool, onToolChange, tools: hostTools, plugins: pluginsProp, decorateAnnotation, fonts: fontsProp, signatures: signatureStore, signatureFonts: signatureFontsProp, page, onPageChange, zoom, onZoomChange, history, asChild = false, onReady, onError, onActivate, children, ref, ...rest }) {
|
|
318
399
|
const rootElRef = useRef(null);
|
|
@@ -567,15 +648,57 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
|
|
|
567
648
|
useLayoutEffect(() => {
|
|
568
649
|
if (fields && document) document.form.setControlledValue(fields);
|
|
569
650
|
}, [fields, document]);
|
|
651
|
+
const ready = pluginHost !== null;
|
|
652
|
+
/** The ids of the tools the current viewer was built with; InkViewport sets them. */
|
|
653
|
+
const [openedToolIds, setOpenedToolIds] = useState(null);
|
|
654
|
+
const toolKey = registeredToolIds(hostTools, pluginHost, openedToolIds).join("\n");
|
|
655
|
+
const toolIds = useMemo(() => toolKey.split("\n"), [toolKey]);
|
|
656
|
+
const registration = useLatest({
|
|
657
|
+
ready,
|
|
658
|
+
toolIds
|
|
659
|
+
});
|
|
660
|
+
const isRegistered = useCallback((id) => {
|
|
661
|
+
const r = registration.current;
|
|
662
|
+
return !r.ready || r.toolIds.includes(id);
|
|
663
|
+
}, [registration]);
|
|
664
|
+
const logUnknownTool = useCallback((id) => console.error(unknownTool(id, registration.current.toolIds)), [registration]);
|
|
665
|
+
const fallBack = useCallback((id) => {
|
|
666
|
+
logUnknownTool(id);
|
|
667
|
+
setInternalTool("select-text");
|
|
668
|
+
latest.current.onToolChange?.("select-text");
|
|
669
|
+
}, [latest, logUnknownTool]);
|
|
670
|
+
/** The `tool` prop last applied, so a host that keeps passing it does not apply it again. */
|
|
671
|
+
const appliedTool = useRef(tool);
|
|
570
672
|
useEffect(() => {
|
|
571
|
-
|
|
572
|
-
|
|
673
|
+
let next = internalTool;
|
|
674
|
+
if (tool !== appliedTool.current) {
|
|
675
|
+
appliedTool.current = tool;
|
|
676
|
+
if (tool) next = tool;
|
|
677
|
+
}
|
|
678
|
+
if (!isRegistered(next)) fallBack(next);
|
|
679
|
+
else if (next !== internalTool) setInternalTool(next);
|
|
680
|
+
}, [
|
|
681
|
+
tool,
|
|
682
|
+
ready,
|
|
683
|
+
toolIds,
|
|
684
|
+
internalTool,
|
|
685
|
+
isRegistered,
|
|
686
|
+
fallBack
|
|
687
|
+
]);
|
|
573
688
|
const setTool = useCallback((id) => {
|
|
689
|
+
if (!isRegistered(id)) {
|
|
690
|
+
logUnknownTool(id);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
574
693
|
const tc = latest.current.tools;
|
|
575
694
|
if (latest.current.tool === id && tc?.active.get() === id) tc.set(id);
|
|
576
695
|
setInternalTool(id);
|
|
577
696
|
latest.current.onToolChange?.(id);
|
|
578
|
-
}, [
|
|
697
|
+
}, [
|
|
698
|
+
latest,
|
|
699
|
+
isRegistered,
|
|
700
|
+
logUnknownTool
|
|
701
|
+
]);
|
|
579
702
|
const reportZoom = useCallback((z) => latest.current.onZoomChange?.(z), [latest]);
|
|
580
703
|
const reportPage = useCallback((p) => latest.current.onPageChange?.(p), [latest]);
|
|
581
704
|
const reportFormAction = useCallback((a) => {
|
|
@@ -636,9 +759,11 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
|
|
|
636
759
|
};
|
|
637
760
|
});
|
|
638
761
|
const stopFocus = keepHistoryFocus(rootEl, () => rootEl.querySelector("[data-ink-scroll]"));
|
|
762
|
+
const stopFileKeys = attachFileKeys(rootEl, () => documentRef.current);
|
|
639
763
|
return () => {
|
|
640
764
|
stopKeys();
|
|
641
765
|
stopFocus();
|
|
766
|
+
stopFileKeys();
|
|
642
767
|
};
|
|
643
768
|
}, [
|
|
644
769
|
rootEl,
|
|
@@ -707,6 +832,8 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
|
|
|
707
832
|
tool: internalTool,
|
|
708
833
|
setTool,
|
|
709
834
|
hostTools,
|
|
835
|
+
openedToolIds,
|
|
836
|
+
setOpenedToolIds,
|
|
710
837
|
decorateAnnotation: decorate,
|
|
711
838
|
onDecorateAnnotationChange,
|
|
712
839
|
fonts,
|
|
@@ -740,6 +867,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
|
|
|
740
867
|
internalTool,
|
|
741
868
|
setTool,
|
|
742
869
|
hostTools,
|
|
870
|
+
openedToolIds,
|
|
743
871
|
decorate,
|
|
744
872
|
onDecorateAnnotationChange,
|
|
745
873
|
fonts,
|
|
@@ -1005,7 +1133,7 @@ const IDLE = {
|
|
|
1005
1133
|
function SignatureForm({ fonts, select, onSave, onClose }) {
|
|
1006
1134
|
const ctx = useInkContext("InkSignatureDialog");
|
|
1007
1135
|
const { setTool } = useTool();
|
|
1008
|
-
const
|
|
1136
|
+
const hasSignatureTool = useHasTool("signature");
|
|
1009
1137
|
const [draft] = useState(() => createSignatureDraft({ fonts: fonts ?? ctx.signatureFonts }));
|
|
1010
1138
|
const s = useReadable(draft.state, IDLE);
|
|
1011
1139
|
const uid = useId();
|
|
@@ -1021,8 +1149,9 @@ function SignatureForm({ fonts, select, onSave, onClose }) {
|
|
|
1021
1149
|
const save = () => {
|
|
1022
1150
|
draft.save(ctx.signatures).then((t) => {
|
|
1023
1151
|
if (!t) return;
|
|
1024
|
-
if (select) {
|
|
1025
|
-
|
|
1152
|
+
if (select && hasSignatureTool) {
|
|
1153
|
+
const patch = { templateId: t.id };
|
|
1154
|
+
ctx.tools?.setOptions("signature", patch);
|
|
1026
1155
|
setTool("signature");
|
|
1027
1156
|
}
|
|
1028
1157
|
onSave?.(t);
|
|
@@ -1456,6 +1585,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
|
|
|
1456
1585
|
buffer: b,
|
|
1457
1586
|
custom: cu
|
|
1458
1587
|
};
|
|
1588
|
+
c.setOpenedToolIds(toolIdsOf(c.hostTools, c.pluginHost));
|
|
1459
1589
|
const e = mountViewer({
|
|
1460
1590
|
container: el,
|
|
1461
1591
|
document: doc,
|
|
@@ -1538,6 +1668,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
|
|
|
1538
1668
|
engine.current = null;
|
|
1539
1669
|
builtWith.current = null;
|
|
1540
1670
|
c.setViewport(null, null);
|
|
1671
|
+
c.setOpenedToolIds(null);
|
|
1541
1672
|
setPages((prev) => prev.length === 0 ? prev : []);
|
|
1542
1673
|
setSignRequest(null);
|
|
1543
1674
|
setAlert(null);
|
|
@@ -1680,21 +1811,23 @@ function InkToolbar({ label = "Document tools", asChild = false, className, chil
|
|
|
1680
1811
|
//#region src/components/InkToolButton.tsx
|
|
1681
1812
|
const ICONS = {
|
|
1682
1813
|
"select-text": TextCursor,
|
|
1683
|
-
select: SquareDashedMousePointer
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1814
|
+
select: SquareDashedMousePointer
|
|
1815
|
+
};
|
|
1816
|
+
const MISSING = {
|
|
1817
|
+
disabled: true,
|
|
1818
|
+
"aria-hidden": true,
|
|
1819
|
+
"aria-pressed": void 0,
|
|
1820
|
+
"aria-label": void 0,
|
|
1821
|
+
title: void 0,
|
|
1822
|
+
onClick: void 0
|
|
1691
1823
|
};
|
|
1692
1824
|
function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
|
|
1693
1825
|
const { hostTools, pluginHost } = useInkContext("InkToolButton");
|
|
1694
1826
|
const { tool: current, setTool } = useTool();
|
|
1695
1827
|
const active = current === tool;
|
|
1696
|
-
|
|
1697
|
-
|
|
1828
|
+
const registered = useRegisteredTool(tool);
|
|
1829
|
+
/** The registered tool by this id, for the name and icon core has none for. */
|
|
1830
|
+
const own = withHostTools([...hostTools ?? [], ...pluginHost?.tools ?? []]).find((t) => t.id === tool);
|
|
1698
1831
|
const name = label ?? TOOL_LABELS[tool] ?? own?.label ?? tool;
|
|
1699
1832
|
const icon = ICONS[tool] ?? own?.icon;
|
|
1700
1833
|
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
@@ -1711,7 +1844,8 @@ function InkToolButton({ tool, label, asChild = false, className, children, onCl
|
|
|
1711
1844
|
if (!e.defaultPrevented) setTool(tool);
|
|
1712
1845
|
},
|
|
1713
1846
|
...rest,
|
|
1714
|
-
|
|
1847
|
+
...registered ? {} : MISSING,
|
|
1848
|
+
children: !registered ? null : typeof children === "function" ? children({
|
|
1715
1849
|
active,
|
|
1716
1850
|
label: name
|
|
1717
1851
|
}) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
|
|
@@ -1750,6 +1884,21 @@ function InkCommand({ id, asChild = false, children, disabled, onClick, ...rest
|
|
|
1750
1884
|
});
|
|
1751
1885
|
}
|
|
1752
1886
|
//#endregion
|
|
1887
|
+
//#region src/components/InkToolbarButton.tsx
|
|
1888
|
+
/**
|
|
1889
|
+
* Your own button in an `InkToolbar`. It joins the toolbar's arrow-key focus and takes the
|
|
1890
|
+
* theme's toolbar button styles; what it shows and does is up to you.
|
|
1891
|
+
*/
|
|
1892
|
+
function InkToolbarButton({ asChild = false, type = "button", className, ...rest }) {
|
|
1893
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
1894
|
+
asChild,
|
|
1895
|
+
type,
|
|
1896
|
+
className: cx("ink-toolbar-button", className),
|
|
1897
|
+
"data-ink-toolbar-button": "",
|
|
1898
|
+
...rest
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
//#endregion
|
|
1753
1902
|
//#region src/components/InkToolbarSeparator.tsx
|
|
1754
1903
|
/**
|
|
1755
1904
|
* A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
|
|
@@ -1784,6 +1933,56 @@ function InkUndo({ asChild = false, className, children, disabled, onClick, ...r
|
|
|
1784
1933
|
});
|
|
1785
1934
|
}
|
|
1786
1935
|
//#endregion
|
|
1936
|
+
//#region src/components/InkDownloadButton.tsx
|
|
1937
|
+
/**
|
|
1938
|
+
* Saves the open document's file through the browser's download, under its `fileName` or
|
|
1939
|
+
* `filename`. Disabled until the document is ready. Today that is the file as it was opened,
|
|
1940
|
+
* without this session's annotations.
|
|
1941
|
+
*/
|
|
1942
|
+
function InkDownloadButton({ asChild = false, filename, className, children, disabled, onClick, ...rest }) {
|
|
1943
|
+
const { document: doc, status } = useInkContext("InkDownloadButton");
|
|
1944
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
1945
|
+
asChild,
|
|
1946
|
+
className: cx("ink-download", className),
|
|
1947
|
+
"data-ink-download": "",
|
|
1948
|
+
"aria-label": "Download",
|
|
1949
|
+
title: "Download",
|
|
1950
|
+
...rest,
|
|
1951
|
+
disabled: disabled === true || doc === null || status.state !== "ready",
|
|
1952
|
+
onClick: (e) => {
|
|
1953
|
+
onClick?.(e);
|
|
1954
|
+
if (e.defaultPrevented || !doc) return;
|
|
1955
|
+
downloadDocument(doc, filename === void 0 ? {} : { filename }).catch(() => void 0);
|
|
1956
|
+
},
|
|
1957
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Download })
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
//#endregion
|
|
1961
|
+
//#region src/components/InkPrintButton.tsx
|
|
1962
|
+
/**
|
|
1963
|
+
* Prints the open document's file with the browser's own PDF viewer (on iOS, in a new tab).
|
|
1964
|
+
* Disabled until the document is ready. Today that is the file as it was opened, without this
|
|
1965
|
+
* session's annotations.
|
|
1966
|
+
*/
|
|
1967
|
+
function InkPrintButton({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
1968
|
+
const { document: doc, status } = useInkContext("InkPrintButton");
|
|
1969
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
1970
|
+
asChild,
|
|
1971
|
+
className: cx("ink-print", className),
|
|
1972
|
+
"data-ink-print": "",
|
|
1973
|
+
"aria-label": "Print",
|
|
1974
|
+
title: "Print",
|
|
1975
|
+
...rest,
|
|
1976
|
+
disabled: disabled === true || doc === null || status.state !== "ready",
|
|
1977
|
+
onClick: (e) => {
|
|
1978
|
+
onClick?.(e);
|
|
1979
|
+
if (e.defaultPrevented || !doc) return;
|
|
1980
|
+
printDocument(doc).catch(() => void 0);
|
|
1981
|
+
},
|
|
1982
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Printer })
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
//#endregion
|
|
1787
1986
|
//#region src/components/InkRedo.tsx
|
|
1788
1987
|
function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
1789
1988
|
const { canRedo, redo } = useHistory();
|
|
@@ -1923,19 +2122,25 @@ function InkFindBar({ className, ...rest }) {
|
|
|
1923
2122
|
role: "search",
|
|
1924
2123
|
hidden: !state.open,
|
|
1925
2124
|
children: [
|
|
1926
|
-
/* @__PURE__ */
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2125
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2126
|
+
className: "ink-input-group",
|
|
2127
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2128
|
+
className: "ink-input-accessory",
|
|
2129
|
+
children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
|
|
2130
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
2131
|
+
ref: field,
|
|
2132
|
+
type: "text",
|
|
2133
|
+
"aria-label": "Find in document",
|
|
2134
|
+
autoComplete: "off",
|
|
2135
|
+
spellCheck: false,
|
|
2136
|
+
onInput: (e) => setQuery(e.currentTarget.value),
|
|
2137
|
+
onKeyDown: (e) => {
|
|
2138
|
+
if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
|
|
2139
|
+
e.preventDefault();
|
|
2140
|
+
if (e.shiftKey) previous();
|
|
2141
|
+
else next();
|
|
2142
|
+
}
|
|
2143
|
+
})]
|
|
1939
2144
|
}),
|
|
1940
2145
|
/* @__PURE__ */ jsx("span", {
|
|
1941
2146
|
className: "ink-find-count",
|
|
@@ -2072,7 +2277,7 @@ function InkZoomSelect({ className, ...rest }) {
|
|
|
2072
2277
|
value,
|
|
2073
2278
|
onValueChange,
|
|
2074
2279
|
children: [/* @__PURE__ */ jsxs(Select.Trigger, {
|
|
2075
|
-
className: cx("ink-
|
|
2280
|
+
className: cx("ink-select ink-select-trigger ink-zoom-select", className),
|
|
2076
2281
|
"data-ink-zoom-select": "",
|
|
2077
2282
|
"aria-label": "Zoom",
|
|
2078
2283
|
...rest,
|
|
@@ -2340,7 +2545,11 @@ function InkColorMenu({ property, label, className, children, standalone = false
|
|
|
2340
2545
|
}
|
|
2341
2546
|
//#endregion
|
|
2342
2547
|
//#region src/components/InkShapeMenu.tsx
|
|
2343
|
-
|
|
2548
|
+
/** Renders nothing while no `shape` tool is registered. */
|
|
2549
|
+
function InkShapeMenu(props) {
|
|
2550
|
+
return useRegisteredTool("shape") ? /* @__PURE__ */ jsx(ShapeMenu, { ...props }) : null;
|
|
2551
|
+
}
|
|
2552
|
+
function ShapeMenu({ className, children, ...rest }) {
|
|
2344
2553
|
const { tool, setTool } = useTool();
|
|
2345
2554
|
const [options, setOptions] = useToolOptions("shape");
|
|
2346
2555
|
const [open, setOpen] = useState(false);
|
|
@@ -2447,7 +2656,11 @@ function InkShapeMenu({ className, children, ...rest }) {
|
|
|
2447
2656
|
//#endregion
|
|
2448
2657
|
//#region src/components/InkSignatureMenu.tsx
|
|
2449
2658
|
const NO_TEMPLATES = [];
|
|
2450
|
-
|
|
2659
|
+
/** Renders nothing while no `signature` tool is registered. */
|
|
2660
|
+
function InkSignatureMenu(props) {
|
|
2661
|
+
return useRegisteredTool("signature") ? /* @__PURE__ */ jsx(SignatureMenu, { ...props }) : null;
|
|
2662
|
+
}
|
|
2663
|
+
function SignatureMenu({ className, children, fonts, ...rest }) {
|
|
2451
2664
|
const { tool, setTool } = useTool();
|
|
2452
2665
|
const [options, setOptions] = useToolOptions("signature");
|
|
2453
2666
|
const { signatures } = useInkContext("InkSignatureMenu");
|
|
@@ -2569,7 +2782,11 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
|
|
|
2569
2782
|
}
|
|
2570
2783
|
//#endregion
|
|
2571
2784
|
//#region src/components/InkHighlightMenu.tsx
|
|
2572
|
-
|
|
2785
|
+
/** Renders nothing while no `highlight` tool is registered. */
|
|
2786
|
+
function InkHighlightMenu(props) {
|
|
2787
|
+
return useRegisteredTool("highlight") ? /* @__PURE__ */ jsx(HighlightMenu, { ...props }) : null;
|
|
2788
|
+
}
|
|
2789
|
+
function HighlightMenu({ className, children, ...rest }) {
|
|
2573
2790
|
const { tool, setTool } = useTool();
|
|
2574
2791
|
const [options, setOptions] = useToolOptions("highlight");
|
|
2575
2792
|
const { document: doc } = useDocument();
|
|
@@ -3202,6 +3419,706 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
|
|
|
3202
3419
|
});
|
|
3203
3420
|
}
|
|
3204
3421
|
//#endregion
|
|
3205
|
-
|
|
3422
|
+
//#region src/dom-attrs.ts
|
|
3423
|
+
/**
|
|
3424
|
+
* Core's attributes (`rootAttrs()`, `triggerAttrs(id)`, `panelAttrs(id)` and the like) as React
|
|
3425
|
+
* props. Core names them as the DOM does; React wants `hidden` as a boolean and `tabindex` as
|
|
3426
|
+
* `tabIndex`. `aria-*`, `data-*`, `id`, `role`, `type` and `title` pass through as they are.
|
|
3427
|
+
* Every component that spreads core's attributes goes through this, so the mapping lives once.
|
|
3428
|
+
*/
|
|
3429
|
+
function domAttrs(attrs) {
|
|
3430
|
+
const out = {};
|
|
3431
|
+
for (const [name, value] of Object.entries(attrs)) if (name === "hidden") out.hidden = true;
|
|
3432
|
+
else if (name === "tabindex") out.tabIndex = Number(value);
|
|
3433
|
+
else out[name] = value;
|
|
3434
|
+
return out;
|
|
3435
|
+
}
|
|
3436
|
+
//#endregion
|
|
3437
|
+
//#region src/components/sidebar-context.ts
|
|
3438
|
+
const SidebarContextObject = createContext(null);
|
|
3439
|
+
/** The sidebar context: throws a readable error outside `InkSidebar`. */
|
|
3440
|
+
function useSidebarContext(consumer) {
|
|
3441
|
+
const ctx = useContext(SidebarContextObject);
|
|
3442
|
+
if (!ctx) throw new Error(`[inkbar] ${consumer} must be rendered inside <InkSidebar>.`);
|
|
3443
|
+
return ctx;
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* The controller's state, re-rendering as it changes. Unlike `useReadable` it reads the live
|
|
3447
|
+
* state on the server too: the sidebar works without a document, so its markup is known there.
|
|
3448
|
+
*/
|
|
3449
|
+
function useSidebarState(controller) {
|
|
3450
|
+
const { state } = controller;
|
|
3451
|
+
const subscribe = useCallback((onChange) => state.subscribe(() => onChange()), [state]);
|
|
3452
|
+
const get = useCallback(() => state.get(), [state]);
|
|
3453
|
+
return useSyncExternalStore(subscribe, get, get);
|
|
3454
|
+
}
|
|
3455
|
+
//#endregion
|
|
3456
|
+
//#region src/components/SidebarTrigger.tsx
|
|
3457
|
+
/**
|
|
3458
|
+
* A sidebar trigger's content: `createSidebar`'s markup, so the theme styles both alike. The
|
|
3459
|
+
* rail shows the icon alone (the trigger's `title` carries the label); tabs and stack headers
|
|
3460
|
+
* show the label too. The badge is hidden from assistive technology: the trigger's name has it.
|
|
3461
|
+
*/
|
|
3462
|
+
function SidebarTrigger({ panel, rail }) {
|
|
3463
|
+
const badge = sidebarBadge(panel.badge);
|
|
3464
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
3465
|
+
panel.icon ? /* @__PURE__ */ jsx(InkIcon, { icon: panel.icon }) : null,
|
|
3466
|
+
!panel.icon || !rail ? /* @__PURE__ */ jsx("span", {
|
|
3467
|
+
className: "ink-sidebar-label",
|
|
3468
|
+
children: panel.label
|
|
3469
|
+
}) : null,
|
|
3470
|
+
/* @__PURE__ */ jsx("span", {
|
|
3471
|
+
className: "ink-sidebar-badge",
|
|
3472
|
+
"aria-hidden": "true",
|
|
3473
|
+
hidden: badge === null,
|
|
3474
|
+
children: badge
|
|
3475
|
+
})
|
|
3476
|
+
] });
|
|
3477
|
+
}
|
|
3478
|
+
//#endregion
|
|
3479
|
+
//#region src/components/InkSidebar.tsx
|
|
3480
|
+
/** Sorts the panels by where their elements sit in the DOM. */
|
|
3481
|
+
function inDomOrder(entries) {
|
|
3482
|
+
return [...entries].sort((a, b) => {
|
|
3483
|
+
const x = a.element();
|
|
3484
|
+
const y = b.element();
|
|
3485
|
+
if (!x || !y || x === y) return 0;
|
|
3486
|
+
return x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* The panels' registry around the controller. Before the first mount, panels preregister as
|
|
3491
|
+
* they render, in render order, so the server and the browser's first render show the panel
|
|
3492
|
+
* core opens. From then on they register from layout effects, ordered by DOM position. A
|
|
3493
|
+
* removal is applied a microtask later: a panel remounted in the same commit (a new `key`) is
|
|
3494
|
+
* back by then, so its open state does not fall back. While the sidebar itself is unmounted
|
|
3495
|
+
* (on its way out, or StrictMode and `<Activity>` replaying effects) nothing is applied.
|
|
3496
|
+
*/
|
|
3497
|
+
function createRegistry(controller) {
|
|
3498
|
+
let mounted = false;
|
|
3499
|
+
let started = false;
|
|
3500
|
+
const early = [];
|
|
3501
|
+
let entries = [];
|
|
3502
|
+
const flush = () => {
|
|
3503
|
+
if (!mounted) return;
|
|
3504
|
+
controller.update({ panels: inDomOrder(entries).map((e) => e.info) });
|
|
3505
|
+
};
|
|
3506
|
+
const context = {
|
|
3507
|
+
controller,
|
|
3508
|
+
preregister(info) {
|
|
3509
|
+
if (started) return;
|
|
3510
|
+
const at = early.findIndex((p) => p.id === info.id);
|
|
3511
|
+
if (at < 0) early.push(info);
|
|
3512
|
+
else early[at] = info;
|
|
3513
|
+
controller.update({ panels: [...early] });
|
|
3514
|
+
},
|
|
3515
|
+
register(entry) {
|
|
3516
|
+
entries = [...entries, entry];
|
|
3517
|
+
flush();
|
|
3518
|
+
return () => {
|
|
3519
|
+
entries = entries.filter((e) => e !== entry);
|
|
3520
|
+
queueMicrotask(flush);
|
|
3521
|
+
};
|
|
3522
|
+
},
|
|
3523
|
+
changed: flush
|
|
3524
|
+
};
|
|
3525
|
+
/** Binds the triggers and ends the controller's pending state; returns the unbind. */
|
|
3526
|
+
function mount(el) {
|
|
3527
|
+
mounted = true;
|
|
3528
|
+
started = true;
|
|
3529
|
+
const unbind = controller.bindTriggers(el);
|
|
3530
|
+
controller.update({
|
|
3531
|
+
panels: inDomOrder(entries).map((e) => e.info),
|
|
3532
|
+
pending: false
|
|
3533
|
+
});
|
|
3534
|
+
return () => {
|
|
3535
|
+
mounted = false;
|
|
3536
|
+
unbind();
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
return {
|
|
3540
|
+
context,
|
|
3541
|
+
mount
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* A sidebar of `InkPanel`s: `createSidebar`'s markup on core's sidebar controller, which holds
|
|
3546
|
+
* the open state, the ARIA, the keys and the clicks. Rail and tabs render their triggers here;
|
|
3547
|
+
* in a stack each panel renders its own header. Panels take their place in DOM order, so a
|
|
3548
|
+
* panel rendered later slots in where it stands. A closed panel stays mounted and hidden, and
|
|
3549
|
+
* the panels' content stays in place across layouts, so its state survives switching.
|
|
3550
|
+
*
|
|
3551
|
+
* `open` with `onOpenChange` controls it; `defaultOpen` starts it uncontrolled; with neither,
|
|
3552
|
+
* the first panel starts open. It needs no document, so it works outside `InkRoot`. The server
|
|
3553
|
+
* renders the root, the panels (the open one shown) and the stack headers; rail and tab
|
|
3554
|
+
* buttons follow once mounted.
|
|
3555
|
+
*/
|
|
3556
|
+
function InkSidebar({ side = "left", layout = "rail", visible = true, open, defaultOpen, onOpenChange, className, children, ...rest }) {
|
|
3557
|
+
const controlled = open !== void 0;
|
|
3558
|
+
const [inner, setInner] = useState(defaultOpen);
|
|
3559
|
+
const value = controlled ? open : inner;
|
|
3560
|
+
const latest = useLatest({
|
|
3561
|
+
controlled,
|
|
3562
|
+
onOpenChange
|
|
3563
|
+
});
|
|
3564
|
+
const idPrefix = useId();
|
|
3565
|
+
const [{ context, mount }] = useState(() => createRegistry(createSidebarController({
|
|
3566
|
+
layout,
|
|
3567
|
+
side,
|
|
3568
|
+
visible,
|
|
3569
|
+
panels: [],
|
|
3570
|
+
...value !== void 0 && { open: value },
|
|
3571
|
+
pending: true,
|
|
3572
|
+
idPrefix,
|
|
3573
|
+
onOpenChange: (next) => {
|
|
3574
|
+
if (!latest.current.controlled) setInner(next);
|
|
3575
|
+
latest.current.onOpenChange?.(next);
|
|
3576
|
+
}
|
|
3577
|
+
})));
|
|
3578
|
+
const { controller } = context;
|
|
3579
|
+
const state = useSidebarState(controller);
|
|
3580
|
+
useLayoutEffect(() => {
|
|
3581
|
+
controller.update({
|
|
3582
|
+
layout,
|
|
3583
|
+
side,
|
|
3584
|
+
visible,
|
|
3585
|
+
...value !== void 0 && { open: value }
|
|
3586
|
+
});
|
|
3587
|
+
}, [
|
|
3588
|
+
controller,
|
|
3589
|
+
layout,
|
|
3590
|
+
side,
|
|
3591
|
+
visible,
|
|
3592
|
+
value
|
|
3593
|
+
]);
|
|
3594
|
+
const root = useRef(null);
|
|
3595
|
+
useLayoutEffect(() => mount(root.current), [mount]);
|
|
3596
|
+
const rail = state.layout === "rail";
|
|
3597
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3598
|
+
...rest,
|
|
3599
|
+
ref: root,
|
|
3600
|
+
className: cx("ink-sidebar", className),
|
|
3601
|
+
...domAttrs(controller.rootAttrs()),
|
|
3602
|
+
children: [state.layout !== "stack" ? /* @__PURE__ */ jsx("div", {
|
|
3603
|
+
className: rail ? "ink-sidebar-rail" : "ink-sidebar-tabs",
|
|
3604
|
+
...domAttrs(controller.tablistAttrs()),
|
|
3605
|
+
children: state.panels.map((panel) => /* @__PURE__ */ jsx("button", {
|
|
3606
|
+
...domAttrs(controller.triggerAttrs(panel.id)),
|
|
3607
|
+
children: /* @__PURE__ */ jsx(SidebarTrigger, {
|
|
3608
|
+
panel,
|
|
3609
|
+
rail
|
|
3610
|
+
})
|
|
3611
|
+
}, panel.id))
|
|
3612
|
+
}) : null, /* @__PURE__ */ jsx("div", {
|
|
3613
|
+
className: state.layout !== "stack" ? "ink-sidebar-panels" : void 0,
|
|
3614
|
+
children: /* @__PURE__ */ jsx(SidebarContextObject.Provider, {
|
|
3615
|
+
value: context,
|
|
3616
|
+
children
|
|
3617
|
+
})
|
|
3618
|
+
})]
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
//#endregion
|
|
3622
|
+
//#region src/components/InkPanel.tsx
|
|
3623
|
+
/**
|
|
3624
|
+
* One panel of an `InkSidebar`, holding `children` as its content. Closed, the panel stays
|
|
3625
|
+
* mounted and hidden. In a stack it renders its own header above its content.
|
|
3626
|
+
*/
|
|
3627
|
+
function InkPanel({ id, label, icon, badge, children }) {
|
|
3628
|
+
const ctx = useSidebarContext("InkPanel");
|
|
3629
|
+
const { controller } = ctx;
|
|
3630
|
+
const info = useMemo(() => ({
|
|
3631
|
+
id,
|
|
3632
|
+
label,
|
|
3633
|
+
...icon !== void 0 && { icon },
|
|
3634
|
+
...badge !== void 0 && { badge }
|
|
3635
|
+
}), [
|
|
3636
|
+
id,
|
|
3637
|
+
label,
|
|
3638
|
+
icon,
|
|
3639
|
+
badge
|
|
3640
|
+
]);
|
|
3641
|
+
ctx.preregister(info);
|
|
3642
|
+
const state = useSidebarState(controller);
|
|
3643
|
+
const wrapper = useRef(null);
|
|
3644
|
+
const [entry] = useState(() => ({
|
|
3645
|
+
info,
|
|
3646
|
+
element: () => wrapper.current
|
|
3647
|
+
}));
|
|
3648
|
+
useLayoutEffect(() => ctx.register(entry), [ctx, entry]);
|
|
3649
|
+
useLayoutEffect(() => {
|
|
3650
|
+
if (entry.info === info) return;
|
|
3651
|
+
entry.info = info;
|
|
3652
|
+
ctx.changed();
|
|
3653
|
+
}, [
|
|
3654
|
+
ctx,
|
|
3655
|
+
entry,
|
|
3656
|
+
info
|
|
3657
|
+
]);
|
|
3658
|
+
const stack = state.layout === "stack";
|
|
3659
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3660
|
+
ref: wrapper,
|
|
3661
|
+
className: cx("ink-sidebar-slot", stack && "ink-sidebar-section"),
|
|
3662
|
+
children: [stack ? /* @__PURE__ */ jsx("button", {
|
|
3663
|
+
className: "ink-sidebar-header",
|
|
3664
|
+
...domAttrs(controller.triggerAttrs(id)),
|
|
3665
|
+
children: /* @__PURE__ */ jsx(SidebarTrigger, {
|
|
3666
|
+
panel: info,
|
|
3667
|
+
rail: false
|
|
3668
|
+
})
|
|
3669
|
+
}) : null, /* @__PURE__ */ jsxs("div", {
|
|
3670
|
+
className: "ink-sidebar-panel",
|
|
3671
|
+
...domAttrs(controller.panelAttrs(id)),
|
|
3672
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3673
|
+
className: "ink-sidebar-panel-title",
|
|
3674
|
+
"aria-hidden": "true",
|
|
3675
|
+
children: label
|
|
3676
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
3677
|
+
className: "ink-sidebar-panel-body",
|
|
3678
|
+
children
|
|
3679
|
+
})]
|
|
3680
|
+
})]
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
//#endregion
|
|
3684
|
+
//#region src/to-readable.ts
|
|
3685
|
+
/**
|
|
3686
|
+
* A core `Readable` that follows a React value, for the core controllers and painters that
|
|
3687
|
+
* components build on. The same readable for the component's life; it takes each new value in
|
|
3688
|
+
* a layout effect, so declare it before the effects that hand it to core.
|
|
3689
|
+
*/
|
|
3690
|
+
function useToReadable(value) {
|
|
3691
|
+
const [store] = useState(() => writable(value));
|
|
3692
|
+
useLayoutEffect(() => {
|
|
3693
|
+
store.set(value);
|
|
3694
|
+
}, [store, value]);
|
|
3695
|
+
const [view] = useState(() => readonly(store));
|
|
3696
|
+
return view;
|
|
3697
|
+
}
|
|
3698
|
+
//#endregion
|
|
3699
|
+
//#region src/components/InkThumbnail.tsx
|
|
3700
|
+
/**
|
|
3701
|
+
* One page as a static image, with its live annotations and plugin marks: core's thumbnail
|
|
3702
|
+
* painter on this element, which sizes it, names it ("Page N" unless `label` says otherwise)
|
|
3703
|
+
* and draws it once it nears the visible area. It is not focusable and does nothing on click:
|
|
3704
|
+
* wrap it in the control you need. Inside `InkRoot`; it renders empty on the server.
|
|
3705
|
+
*/
|
|
3706
|
+
function InkThumbnail({ page, width, height, annotations = true, marks = true, label, className, ...rest }) {
|
|
3707
|
+
const ctx = useInkContext("InkThumbnail");
|
|
3708
|
+
const document = useToReadable(ctx.document);
|
|
3709
|
+
const pluginHost = useToReadable(ctx.pluginHost);
|
|
3710
|
+
const options = useMemo(() => ({
|
|
3711
|
+
page,
|
|
3712
|
+
width,
|
|
3713
|
+
height,
|
|
3714
|
+
annotations,
|
|
3715
|
+
marks,
|
|
3716
|
+
label
|
|
3717
|
+
}), [
|
|
3718
|
+
page,
|
|
3719
|
+
width,
|
|
3720
|
+
height,
|
|
3721
|
+
annotations,
|
|
3722
|
+
marks,
|
|
3723
|
+
label
|
|
3724
|
+
]);
|
|
3725
|
+
const latest = useLatest(options);
|
|
3726
|
+
const el = useRef(null);
|
|
3727
|
+
const painter = useRef(null);
|
|
3728
|
+
useLayoutEffect(() => {
|
|
3729
|
+
painter.current?.update(options);
|
|
3730
|
+
}, [options]);
|
|
3731
|
+
useLayoutEffect(() => {
|
|
3732
|
+
const p = createThumbnailPainter(el.current, {
|
|
3733
|
+
document,
|
|
3734
|
+
pluginHost
|
|
3735
|
+
}, latest.current);
|
|
3736
|
+
painter.current = p;
|
|
3737
|
+
return () => {
|
|
3738
|
+
p.destroy();
|
|
3739
|
+
painter.current = null;
|
|
3740
|
+
};
|
|
3741
|
+
}, [
|
|
3742
|
+
document,
|
|
3743
|
+
pluginHost,
|
|
3744
|
+
latest
|
|
3745
|
+
]);
|
|
3746
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3747
|
+
...rest,
|
|
3748
|
+
ref: el,
|
|
3749
|
+
className: cx("ink-thumbnail", className)
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
//#endregion
|
|
3753
|
+
//#region src/components/InkThumbnails.tsx
|
|
3754
|
+
/** Until the list mounts, and on the server: nothing listed. */
|
|
3755
|
+
const EMPTY = {
|
|
3756
|
+
pages: [],
|
|
3757
|
+
current: -1,
|
|
3758
|
+
window: {
|
|
3759
|
+
start: 0,
|
|
3760
|
+
end: 0
|
|
3761
|
+
},
|
|
3762
|
+
offsets: [0],
|
|
3763
|
+
width: THUMBNAIL_WIDTH
|
|
3764
|
+
};
|
|
3765
|
+
/**
|
|
3766
|
+
* The pages as a list of thumbnails: `createThumbnails`' markup on core's thumbnails
|
|
3767
|
+
* controller, which holds the current page, the keys, the clicks, the Tab stop and the window.
|
|
3768
|
+
* Only the items near the visible part are mounted, each placed at its offset over a spacer of
|
|
3769
|
+
* the full height, and each holds an `InkThumbnail`. An item is a button that scrolls the
|
|
3770
|
+
* viewport to its page; the current page's has `aria-current="page"`. Inside `InkRoot`; it
|
|
3771
|
+
* renders empty on the server.
|
|
3772
|
+
*/
|
|
3773
|
+
function InkThumbnails({ pages, width, annotations = true, marks = true, renderItem, className, ...rest }) {
|
|
3774
|
+
const ctx = useInkContext("InkThumbnails");
|
|
3775
|
+
const document = useToReadable(ctx.document);
|
|
3776
|
+
const pluginHost = useToReadable(ctx.pluginHost);
|
|
3777
|
+
const viewport = useToReadable(ctx.viewport);
|
|
3778
|
+
const options = useMemo(() => ({
|
|
3779
|
+
pages,
|
|
3780
|
+
width,
|
|
3781
|
+
annotations,
|
|
3782
|
+
marks
|
|
3783
|
+
}), [
|
|
3784
|
+
pages,
|
|
3785
|
+
width,
|
|
3786
|
+
annotations,
|
|
3787
|
+
marks
|
|
3788
|
+
]);
|
|
3789
|
+
const latest = useLatest(options);
|
|
3790
|
+
const scroller = useRef(null);
|
|
3791
|
+
const [controller, setController] = useState(null);
|
|
3792
|
+
useLayoutEffect(() => {
|
|
3793
|
+
controller?.update(options);
|
|
3794
|
+
}, [controller, options]);
|
|
3795
|
+
useLayoutEffect(() => {
|
|
3796
|
+
const c = createThumbnailsController({
|
|
3797
|
+
document,
|
|
3798
|
+
pluginHost,
|
|
3799
|
+
viewport
|
|
3800
|
+
}, latest.current);
|
|
3801
|
+
setController(c);
|
|
3802
|
+
c.attach(scroller.current);
|
|
3803
|
+
return () => {
|
|
3804
|
+
c.destroy();
|
|
3805
|
+
setController(null);
|
|
3806
|
+
};
|
|
3807
|
+
}, [
|
|
3808
|
+
document,
|
|
3809
|
+
pluginHost,
|
|
3810
|
+
viewport,
|
|
3811
|
+
latest
|
|
3812
|
+
]);
|
|
3813
|
+
const state = useReadable(controller?.state, EMPTY);
|
|
3814
|
+
const { window: win, offsets } = state;
|
|
3815
|
+
const items = state.pages.slice(win.start, win.end).map((page, k) => {
|
|
3816
|
+
const top = offsets[win.start + k];
|
|
3817
|
+
const bottom = offsets[win.start + k + 1];
|
|
3818
|
+
const attrs = controller?.itemAttrs(win.start + k) ?? {};
|
|
3819
|
+
return {
|
|
3820
|
+
page,
|
|
3821
|
+
current: page === state.current,
|
|
3822
|
+
top,
|
|
3823
|
+
height: bottom - top,
|
|
3824
|
+
attrs
|
|
3825
|
+
};
|
|
3826
|
+
});
|
|
3827
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3828
|
+
...rest,
|
|
3829
|
+
ref: scroller,
|
|
3830
|
+
className: cx("ink-thumbnails", className),
|
|
3831
|
+
"data-ink-thumbnails": "",
|
|
3832
|
+
role: "list",
|
|
3833
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3834
|
+
className: "ink-thumbnails-spacer",
|
|
3835
|
+
"aria-hidden": "true",
|
|
3836
|
+
style: { height: `${offsets.at(-1) ?? 0}px` }
|
|
3837
|
+
}), items.map((item) => /* @__PURE__ */ jsx("div", {
|
|
3838
|
+
className: "ink-thumbnails-entry",
|
|
3839
|
+
...domAttrs(item.attrs),
|
|
3840
|
+
style: {
|
|
3841
|
+
top: `${item.top}px`,
|
|
3842
|
+
height: `${item.height}px`
|
|
3843
|
+
},
|
|
3844
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
3845
|
+
type: "button",
|
|
3846
|
+
className: "ink-thumbnails-item",
|
|
3847
|
+
"data-ink-thumbnail-item": item.page,
|
|
3848
|
+
"aria-current": item.current ? "page" : void 0,
|
|
3849
|
+
children: [
|
|
3850
|
+
/* @__PURE__ */ jsx(InkThumbnail, {
|
|
3851
|
+
page: item.page,
|
|
3852
|
+
width: state.width,
|
|
3853
|
+
annotations,
|
|
3854
|
+
marks
|
|
3855
|
+
}),
|
|
3856
|
+
/* @__PURE__ */ jsx("span", {
|
|
3857
|
+
className: "ink-thumbnails-label",
|
|
3858
|
+
"aria-hidden": "true",
|
|
3859
|
+
children: item.page + 1
|
|
3860
|
+
}),
|
|
3861
|
+
renderItem?.({
|
|
3862
|
+
page: item.page,
|
|
3863
|
+
current: item.current
|
|
3864
|
+
})
|
|
3865
|
+
]
|
|
3866
|
+
})
|
|
3867
|
+
}, item.page))]
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
//#endregion
|
|
3871
|
+
//#region src/components/RecordCards.tsx
|
|
3872
|
+
const NO_ROWS = Object.freeze([]);
|
|
3873
|
+
const actionButtons = (row) => [...row.querySelectorAll(".ink-card-actions > button")];
|
|
3874
|
+
/**
|
|
3875
|
+
* A record list's markup on core's record list controller, shared by `InkRecordList` and
|
|
3876
|
+
* `InkSearchResults`: `createRecordList`'s `div.ink-records` holding `ul.ink-record-list` with
|
|
3877
|
+
* one `li.ink-record` card per row, the empty text and a polite live region. The controller
|
|
3878
|
+
* holds the rows, the count and the announcement, and handles the clicks, keys, Tab stop,
|
|
3879
|
+
* focus and scrolling. Empty, with no empty text, until there is a source.
|
|
3880
|
+
*/
|
|
3881
|
+
function RecordCards({ source, emptyShown = true, empty, onCountChange, className, ...rest }) {
|
|
3882
|
+
const root = useRef(null);
|
|
3883
|
+
const [controller, setController] = useState(null);
|
|
3884
|
+
const latest = useLatest(onCountChange);
|
|
3885
|
+
/** The count last reported, across sources and StrictMode's replayed effects. */
|
|
3886
|
+
const reported = useRef(null);
|
|
3887
|
+
const held = useRef(null);
|
|
3888
|
+
useLayoutEffect(() => {
|
|
3889
|
+
const el = root.current;
|
|
3890
|
+
if (!source || !el) return;
|
|
3891
|
+
const c = createRecordListController(source);
|
|
3892
|
+
const report = (count) => {
|
|
3893
|
+
if (count === reported.current) return;
|
|
3894
|
+
reported.current = count;
|
|
3895
|
+
latest.current?.(count);
|
|
3896
|
+
};
|
|
3897
|
+
/**
|
|
3898
|
+
* Before the rows re-render under a focused action whose button goes (its card lost that
|
|
3899
|
+
* action), note where it was; the layout effect below focuses the same place in the card,
|
|
3900
|
+
* else the row. A row that goes is the controller's to re-home.
|
|
3901
|
+
*/
|
|
3902
|
+
const holdActionFocus = () => {
|
|
3903
|
+
const active = el.ownerDocument.activeElement;
|
|
3904
|
+
if (!(active instanceof HTMLButtonElement) || !el.contains(active)) return;
|
|
3905
|
+
const row = active.closest("[data-ink-record]");
|
|
3906
|
+
const id = row?.dataset.inkRecord;
|
|
3907
|
+
if (!row || id === void 0) return;
|
|
3908
|
+
held.current = {
|
|
3909
|
+
button: active,
|
|
3910
|
+
id,
|
|
3911
|
+
index: actionButtons(row).indexOf(active)
|
|
3912
|
+
};
|
|
3913
|
+
};
|
|
3914
|
+
setController(c);
|
|
3915
|
+
report(c.count.get());
|
|
3916
|
+
const stops = [c.rows.subscribe(holdActionFocus), c.count.subscribe(report)];
|
|
3917
|
+
const detach = c.attach(el);
|
|
3918
|
+
return () => {
|
|
3919
|
+
for (const stop of stops) stop();
|
|
3920
|
+
detach();
|
|
3921
|
+
c.destroy();
|
|
3922
|
+
setController(null);
|
|
3923
|
+
};
|
|
3924
|
+
}, [source, latest]);
|
|
3925
|
+
const rows = useReadable(controller?.rows, NO_ROWS);
|
|
3926
|
+
const announcement = useReadable(controller?.announcement, "");
|
|
3927
|
+
const showEmpty = controller !== null && rows.length === 0 && emptyShown;
|
|
3928
|
+
useLayoutEffect(() => {
|
|
3929
|
+
const focus = held.current;
|
|
3930
|
+
const el = root.current;
|
|
3931
|
+
held.current = null;
|
|
3932
|
+
if (!focus || !el || focus.button.isConnected) return;
|
|
3933
|
+
const row = [...el.querySelectorAll("[data-ink-record]")].find((r) => r.dataset.inkRecord === focus.id);
|
|
3934
|
+
if (row) (actionButtons(row)[focus.index] ?? row).focus({ preventScroll: true });
|
|
3935
|
+
}, [rows]);
|
|
3936
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3937
|
+
...rest,
|
|
3938
|
+
ref: root,
|
|
3939
|
+
className: cx("ink-records", className),
|
|
3940
|
+
"data-ink-records": "",
|
|
3941
|
+
tabIndex: -1,
|
|
3942
|
+
children: [
|
|
3943
|
+
/* @__PURE__ */ jsx("ul", {
|
|
3944
|
+
className: "ink-record-list",
|
|
3945
|
+
role: "list",
|
|
3946
|
+
hidden: rows.length === 0,
|
|
3947
|
+
children: rows.map(({ item, card, selected }) => /* @__PURE__ */ jsx("li", {
|
|
3948
|
+
className: "ink-record",
|
|
3949
|
+
"data-ink-record": item.id,
|
|
3950
|
+
"aria-current": selected ? "true" : void 0,
|
|
3951
|
+
"data-selected": selected ? "" : void 0,
|
|
3952
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3953
|
+
className: cx("ink-card", card?.className),
|
|
3954
|
+
children: card ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
3955
|
+
/* @__PURE__ */ jsx("p", {
|
|
3956
|
+
className: "ink-card-title",
|
|
3957
|
+
children: card.title
|
|
3958
|
+
}),
|
|
3959
|
+
card.meta !== void 0 ? /* @__PURE__ */ jsxs("p", {
|
|
3960
|
+
className: "ink-card-meta",
|
|
3961
|
+
children: [card.swatch !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
3962
|
+
className: "ink-card-swatch",
|
|
3963
|
+
style: { background: card.swatch }
|
|
3964
|
+
}) : null, card.meta]
|
|
3965
|
+
}) : null,
|
|
3966
|
+
card.body !== void 0 ? /* @__PURE__ */ jsx("p", {
|
|
3967
|
+
className: "ink-card-body",
|
|
3968
|
+
children: card.body
|
|
3969
|
+
}) : null,
|
|
3970
|
+
card.excerpt !== void 0 ? /* @__PURE__ */ jsxs("p", {
|
|
3971
|
+
className: "ink-card-excerpt",
|
|
3972
|
+
children: [
|
|
3973
|
+
card.excerpt.before,
|
|
3974
|
+
/* @__PURE__ */ jsx("mark", { children: card.excerpt.match }),
|
|
3975
|
+
card.excerpt.after
|
|
3976
|
+
]
|
|
3977
|
+
}) : null,
|
|
3978
|
+
/* @__PURE__ */ jsx("div", {
|
|
3979
|
+
className: "ink-card-actions",
|
|
3980
|
+
children: Object.keys(card.actions).map((label) => /* @__PURE__ */ jsx("button", {
|
|
3981
|
+
type: "button",
|
|
3982
|
+
onClick: () => controller?.act(item.id, label),
|
|
3983
|
+
children: label
|
|
3984
|
+
}, label))
|
|
3985
|
+
})
|
|
3986
|
+
] }) : null
|
|
3987
|
+
})
|
|
3988
|
+
}, item.id))
|
|
3989
|
+
}),
|
|
3990
|
+
/* @__PURE__ */ jsx("p", {
|
|
3991
|
+
className: "ink-record-list-empty",
|
|
3992
|
+
hidden: !showEmpty,
|
|
3993
|
+
children: empty === void 0 ? "Nothing here yet." : empty
|
|
3994
|
+
}),
|
|
3995
|
+
/* @__PURE__ */ jsx("div", {
|
|
3996
|
+
className: "ink-visually-hidden",
|
|
3997
|
+
"aria-live": "polite",
|
|
3998
|
+
"data-ink-record-status": "",
|
|
3999
|
+
children: announcement
|
|
4000
|
+
})
|
|
4001
|
+
]
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
//#endregion
|
|
4005
|
+
//#region src/components/InkRecordList.tsx
|
|
4006
|
+
function isDevelopment() {
|
|
4007
|
+
try {
|
|
4008
|
+
return process.env.NODE_ENV !== "production";
|
|
4009
|
+
} catch {
|
|
4010
|
+
return false;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
/**
|
|
4014
|
+
* The records a plugin keeps under `records` (`<plugin>.<name>`) as a list of cards, the same
|
|
4015
|
+
* cards their marks open, with the same actions: `createRecordList` on core's record list
|
|
4016
|
+
* controller. The list, the marks and `useRecordSelection` share one selection. `card`, `where`
|
|
4017
|
+
* and `sort` replace the marks' card, filter and reading order; a new function rebuilds the
|
|
4018
|
+
* list, so keep them stable (`useCallback`, or defined outside the component).
|
|
4019
|
+
* `onCountChange` gets the number listed whenever it changes; `empty` replaces "Nothing here
|
|
4020
|
+
* yet.". It fills its parent's height. Inside `InkRoot`; it renders empty on the server.
|
|
4021
|
+
*
|
|
4022
|
+
* In place of `records`, `source` takes a host's own `RecordListSource` and lists it with the
|
|
4023
|
+
* same controller and cards. Give one of the two: with both, `source` wins and a development
|
|
4024
|
+
* build warns.
|
|
4025
|
+
*/
|
|
4026
|
+
function InkRecordList({ records, source, card, where, sort, onCountChange, empty, ...rest }) {
|
|
4027
|
+
const ctx = useInkContext("InkRecordList");
|
|
4028
|
+
const viewport = useToReadable(ctx.viewport);
|
|
4029
|
+
const { pluginHost } = ctx;
|
|
4030
|
+
const both = records !== void 0 && source !== void 0;
|
|
4031
|
+
useEffect(() => {
|
|
4032
|
+
if (both && isDevelopment()) console.warn("[inkbar] InkRecordList takes `records` or `source`, not both: it lists `source`.");
|
|
4033
|
+
}, [both]);
|
|
4034
|
+
const listed = useMemo(() => {
|
|
4035
|
+
if (source) return source;
|
|
4036
|
+
if (!pluginHost || records === void 0) return null;
|
|
4037
|
+
const opts = {};
|
|
4038
|
+
if (card) opts.card = card;
|
|
4039
|
+
if (where) opts.where = where;
|
|
4040
|
+
if (sort) opts.sort = sort;
|
|
4041
|
+
return recordsSource({
|
|
4042
|
+
pluginHost,
|
|
4043
|
+
viewport
|
|
4044
|
+
}, records, opts);
|
|
4045
|
+
}, [
|
|
4046
|
+
source,
|
|
4047
|
+
pluginHost,
|
|
4048
|
+
records,
|
|
4049
|
+
card,
|
|
4050
|
+
where,
|
|
4051
|
+
sort,
|
|
4052
|
+
viewport
|
|
4053
|
+
]);
|
|
4054
|
+
return /* @__PURE__ */ jsx(RecordCards, {
|
|
4055
|
+
...rest,
|
|
4056
|
+
source: listed,
|
|
4057
|
+
onCountChange,
|
|
4058
|
+
empty
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
//#endregion
|
|
4062
|
+
//#region src/components/InkSearchResults.tsx
|
|
4063
|
+
/**
|
|
4064
|
+
* The search hits as a list of cards, for a sidebar panel: `createSearchResults` on core's
|
|
4065
|
+
* record list controller over the root's search. One card per hit, with its page and the text
|
|
4066
|
+
* around it. Choosing a card makes its hit current, as the find bar's next and previous do, and
|
|
4067
|
+
* the current hit is the selected card. Above the cards, a query field (`field={false}`
|
|
4068
|
+
* leaves it out when a find bar is on the page); under them, the find bar's match count.
|
|
4069
|
+
* Field and find bar share the search state, so they never disagree. It fills its parent's
|
|
4070
|
+
* height. Inside `InkRoot`; it renders empty on the server.
|
|
4071
|
+
*/
|
|
4072
|
+
function InkSearchResults({ field = true, className, ...rest }) {
|
|
4073
|
+
const ctx = useInkContext("InkSearchResults");
|
|
4074
|
+
const search = useSearch();
|
|
4075
|
+
const { state } = search;
|
|
4076
|
+
const source = useMemo(() => ctx.search ? searchSource(ctx.search) : null, [ctx.search]);
|
|
4077
|
+
const input = useRef(null);
|
|
4078
|
+
useLayoutEffect(() => {
|
|
4079
|
+
const el = input.current;
|
|
4080
|
+
if (el && el.value !== state.query) el.value = state.query;
|
|
4081
|
+
}, [state.query, field]);
|
|
4082
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4083
|
+
...rest,
|
|
4084
|
+
className: cx("ink-search-results", className),
|
|
4085
|
+
"data-ink-search-results": "",
|
|
4086
|
+
children: [
|
|
4087
|
+
field ? /* @__PURE__ */ jsx("div", {
|
|
4088
|
+
className: "ink-search-results-bar",
|
|
4089
|
+
role: "search",
|
|
4090
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
4091
|
+
className: "ink-input-group",
|
|
4092
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4093
|
+
className: "ink-input-accessory",
|
|
4094
|
+
children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
|
|
4095
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
4096
|
+
ref: input,
|
|
4097
|
+
type: "text",
|
|
4098
|
+
"aria-label": "Find in document",
|
|
4099
|
+
placeholder: "Search for a word or phrase",
|
|
4100
|
+
autoComplete: "off",
|
|
4101
|
+
spellCheck: false,
|
|
4102
|
+
defaultValue: state.query,
|
|
4103
|
+
onInput: (e) => search.setQuery(e.currentTarget.value),
|
|
4104
|
+
onKeyDown: (e) => handleResultsFieldKey(e.nativeEvent, search)
|
|
4105
|
+
})]
|
|
4106
|
+
})
|
|
4107
|
+
}) : null,
|
|
4108
|
+
/* @__PURE__ */ jsx(RecordCards, {
|
|
4109
|
+
source,
|
|
4110
|
+
emptyShown: isBlankQuery(state.query),
|
|
4111
|
+
empty: "Search to list every match."
|
|
4112
|
+
}),
|
|
4113
|
+
/* @__PURE__ */ jsx("div", {
|
|
4114
|
+
className: "ink-search-results-count",
|
|
4115
|
+
"data-ink-search-results-count": "",
|
|
4116
|
+
children: findCountText(state)
|
|
4117
|
+
})
|
|
4118
|
+
]
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
//#endregion
|
|
4122
|
+
export { InkAnnotation, InkColorMenu, InkCommand, InkDownloadButton, InkFindBar, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPanel, InkPrevPage, InkPrintButton, InkRecordList, InkRedo, InkRoot, InkSearchButton, InkSearchResults, InkShapeMenu, InkSidebar, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkThumbnail, InkThumbnails, InkToolButton, InkToolbar, InkToolbarButton, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useForm, useHistory, useInkContext, usePage, useReadable, useRecordSelection, useRecords, useSearch, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
|
|
3206
4123
|
|
|
3207
4124
|
//# sourceMappingURL=index.mjs.map
|