@inkbar/core 0.5.0 → 0.7.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 +111 -1
- package/dist/dom.d.mts +597 -40
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2154 -436
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +16 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BwaXi7C8.d.mts → signatures-CalOktkf.d.mts} +148 -13
- package/dist/signatures-CalOktkf.d.mts.map +1 -0
- package/dist/{signatures-CMapTKrC.mjs → signatures-Do_l2IlM.mjs} +882 -56
- package/dist/signatures-Do_l2IlM.mjs.map +1 -0
- package/package.json +4 -3
- package/dist/signatures-BwaXi7C8.d.mts.map +0 -1
- package/dist/signatures-CMapTKrC.mjs.map +0 -1
package/dist/dom.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $t as
|
|
2
|
-
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Check, ChevronDown, ChevronLeft, ChevronRight, Circle, CircleDashed, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, RectangleHorizontal, Redo2, Search, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
1
|
+
import { $t as createRecordStore, B as isTextMarkup, Bt as isStandardCallout, E as withoutReplies, G as resizedRect, H as movedEndpoint, I as hasCallout, J as withBox, K as scaled, Kt as isKnown, L as hasCornerHandles, O as createToolStyle, Q as deriveRect, Qt as mergeLineRects, R as hasRectGeometry, Rt as calloutStroke, S as isThreadRoot, T as threadRoot, U as movedTip, Ut as LINE_ENDINGS, V as keepsAspect, Vt as withCalloutBounds, W as resizedBox, X as isBlankQuery, Y as openDocument, Yt as cssVariableDefaults, _ as defaultEnds, an as invertMatrix, b as isAbandonedNote, bn as InkError, d as recogniseStroke, en as derived, f as DEFAULT_CORNER_RADIUS, fn as rectFromPoints, g as constrain, h as SHAPE_KINDS, hn as rectsIntersect, i as createInkTemplate, l as placeSignature, m as DEFAULT_SHAPE_SIZE, nn as writable, nt as isProduction, o as createMemorySignatureStore, p as DEFAULT_SHAPE_OPTIONS, q as translated, r as createImageTemplate, rn as applyMatrix, s as createSignatureLibrary, tn as readonly, v as isPointerOnlyKind, w as threadOf, xn as isInkError, y as shapeInput, z as isBubble, zt as freeTextBox } from "./signatures-Do_l2IlM.mjs";
|
|
2
|
+
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleDashed, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, RectangleHorizontal, Redo2, Search, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
3
3
|
import { getStroke } from "perfect-freehand";
|
|
4
4
|
//#region src/dom/transform.ts
|
|
5
5
|
/** Folds any integer multiple of 90 onto 0, 90, 180 or 270. Throws `RangeError` otherwise. */
|
|
@@ -500,7 +500,7 @@ function createViewport(opts) {
|
|
|
500
500
|
});
|
|
501
501
|
sync();
|
|
502
502
|
},
|
|
503
|
-
scrollToRect(page, rect) {
|
|
503
|
+
scrollToRect(page, rect, o) {
|
|
504
504
|
if (mode.get() === "single" && currentPage.get() !== page) {
|
|
505
505
|
currentPage.set(page);
|
|
506
506
|
relayout();
|
|
@@ -508,11 +508,13 @@ function createViewport(opts) {
|
|
|
508
508
|
const box = boxFor(page);
|
|
509
509
|
if (!box) return;
|
|
510
510
|
const css = transformFor(page).toCssRect(rect);
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
511
|
+
const at = origin();
|
|
512
|
+
const to = { behavior: o?.behavior ?? "auto" };
|
|
513
|
+
const top = at.y + box.top + css.top;
|
|
514
|
+
if (top < container.scrollTop || top + css.height > container.scrollTop + container.clientHeight) to.top = Math.max(0, top - (container.clientHeight - css.height) / 2);
|
|
515
|
+
const left = at.x + box.left + css.left;
|
|
516
|
+
if (left < container.scrollLeft || left + css.width > container.scrollLeft + container.clientWidth) to.left = Math.max(0, left - (container.clientWidth - css.width) / 2);
|
|
517
|
+
if (to.top !== void 0 || to.left !== void 0) container.scrollTo(to);
|
|
516
518
|
sync();
|
|
517
519
|
},
|
|
518
520
|
transformFor,
|
|
@@ -629,7 +631,7 @@ function watchDevicePixelRatio(view, current, onChange) {
|
|
|
629
631
|
}
|
|
630
632
|
//#endregion
|
|
631
633
|
//#region src/dom/page-renderer.ts
|
|
632
|
-
const isCancellation$
|
|
634
|
+
const isCancellation$2 = (error) => error?.name === "RenderingCancelledException";
|
|
633
635
|
function createPageRenderer(opts) {
|
|
634
636
|
const { document: doc, page, host } = opts;
|
|
635
637
|
const view = host.ownerDocument.defaultView;
|
|
@@ -715,7 +717,7 @@ function createPageRenderer(opts) {
|
|
|
715
717
|
opts.onRendered?.();
|
|
716
718
|
} catch (error) {
|
|
717
719
|
if (canvas && canvas !== current) release(canvas);
|
|
718
|
-
if (!((destroyed || gen !== generation) && isCancellation$
|
|
720
|
+
if (!((destroyed || gen !== generation) && isCancellation$2(error)) && !destroyed) opts.onError?.(error);
|
|
719
721
|
} finally {
|
|
720
722
|
if (gen === generation) settle();
|
|
721
723
|
}
|
|
@@ -2018,7 +2020,7 @@ function insertText(field, text) {
|
|
|
2018
2020
|
}
|
|
2019
2021
|
//#endregion
|
|
2020
2022
|
//#region src/pdfjs/render-region.ts
|
|
2021
|
-
const isCancellation = (error) => error?.name === "RenderingCancelledException";
|
|
2023
|
+
const isCancellation$1 = (error) => error?.name === "RenderingCancelledException";
|
|
2022
2024
|
/**
|
|
2023
2025
|
* Draws `userRect` of `page` (0-based) into `canvas` at `scale` device pixels per user-space
|
|
2024
2026
|
* unit (times `/UserUnit`, as PDF.js applies it), turned by `rotation`: the page's /Rotate plus
|
|
@@ -2061,7 +2063,7 @@ async function renderRegion(doc, page, userRect, scale, canvas, signal, rotation
|
|
|
2061
2063
|
try {
|
|
2062
2064
|
await task.promise;
|
|
2063
2065
|
} catch (error) {
|
|
2064
|
-
if (signal?.aborted && isCancellation(error)) throw signal.reason;
|
|
2066
|
+
if (signal?.aborted && isCancellation$1(error)) throw signal.reason;
|
|
2065
2067
|
throw error;
|
|
2066
2068
|
} finally {
|
|
2067
2069
|
signal?.removeEventListener("abort", cancel);
|
|
@@ -2310,7 +2312,7 @@ function ariaLabel(a) {
|
|
|
2310
2312
|
return a.author ? `${named} by ${a.author}` : named;
|
|
2311
2313
|
}
|
|
2312
2314
|
/** Reading order in user space: top edge first (y grows upwards), then left edge. */
|
|
2313
|
-
function readingOrder(a, b) {
|
|
2315
|
+
function readingOrder$1(a, b) {
|
|
2314
2316
|
return b.rect.y + b.rect.height - (a.rect.y + a.rect.height) || a.rect.x - b.rect.x;
|
|
2315
2317
|
}
|
|
2316
2318
|
/** A decoration's data key: lower-case kebab, as written after `data-`. */
|
|
@@ -2727,7 +2729,7 @@ function createOverlay(opts) {
|
|
|
2727
2729
|
}
|
|
2728
2730
|
function render() {
|
|
2729
2731
|
if (destroyed) return;
|
|
2730
|
-
const current = withoutReplies(store.list.get()).filter((a) => a.page === page).sort(readingOrder);
|
|
2732
|
+
const current = withoutReplies(store.list.get()).filter((a) => a.page === page).sort(readingOrder$1);
|
|
2731
2733
|
const ids = new Set(current.map((a) => a.id));
|
|
2732
2734
|
const active = doc.activeElement;
|
|
2733
2735
|
let focusId = [...elements].find(([, n]) => n === active)?.[0];
|
|
@@ -2818,153 +2820,1387 @@ function createOverlay(opts) {
|
|
|
2818
2820
|
};
|
|
2819
2821
|
}
|
|
2820
2822
|
//#endregion
|
|
2821
|
-
//#region src/dom/
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
2835
|
-
...callbacks("canvas")
|
|
2823
|
+
//#region src/dom/plugins/api/command.ts
|
|
2824
|
+
const DEFAULT_ERROR_STATUS = "Something went wrong.";
|
|
2825
|
+
/** A failure that is the run being cancelled, not a bug: not logged, no error status. */
|
|
2826
|
+
function isCancellation(error) {
|
|
2827
|
+
if (isInkError(error) && error.code === "DocumentDestroyed") return true;
|
|
2828
|
+
return error instanceof Error && error.name === "AbortError";
|
|
2829
|
+
}
|
|
2830
|
+
function createCommand(ctx, id, label, run, opts = {}) {
|
|
2831
|
+
const status = writable("");
|
|
2832
|
+
let controller = null;
|
|
2833
|
+
ctx.onDocument(() => () => {
|
|
2834
|
+
controller?.abort();
|
|
2835
|
+
status.set("");
|
|
2836
2836
|
});
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2837
|
+
return ctx.commands.add({
|
|
2838
|
+
id,
|
|
2839
|
+
label,
|
|
2840
|
+
...opts.icon && { icon: opts.icon },
|
|
2841
|
+
...opts.enabled && { enabled: opts.enabled },
|
|
2842
|
+
status: readonly(status),
|
|
2843
|
+
async run() {
|
|
2844
|
+
const doc = ctx.document.get();
|
|
2845
|
+
if (!doc) return;
|
|
2846
|
+
const own = new AbortController();
|
|
2847
|
+
controller = own;
|
|
2848
|
+
status.set("");
|
|
2849
|
+
try {
|
|
2850
|
+
const result = await run({
|
|
2851
|
+
doc,
|
|
2852
|
+
signal: own.signal
|
|
2853
|
+
});
|
|
2854
|
+
if (!own.signal.aborted) status.set(result ?? "");
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
if (own.signal.aborted || isCancellation(error)) return;
|
|
2857
|
+
console.error(error);
|
|
2858
|
+
status.set(opts.errorStatus ?? DEFAULT_ERROR_STATUS);
|
|
2859
|
+
} finally {
|
|
2860
|
+
if (controller === own) controller = null;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2843
2863
|
});
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2864
|
+
}
|
|
2865
|
+
//#endregion
|
|
2866
|
+
//#region src/dom/annotation-element.ts
|
|
2867
|
+
/** The overlay element drawn for annotation `id` under `root`, if it is in the DOM. */
|
|
2868
|
+
function annotationElementFor(root, id) {
|
|
2869
|
+
return root.querySelector(`[data-annotation="${CSS.escape(id)}"]`);
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Resolves the overlay element for `id` and keeps resolving while `root`'s subtree changes:
|
|
2873
|
+
* the overlay creates and removes these elements outside any framework (a page mounts as it
|
|
2874
|
+
* scrolls into view), so a popover anchored to one has to follow. Calls `onChange` once now
|
|
2875
|
+
* and again whenever the answer changes. The returned function stops observing.
|
|
2876
|
+
*/
|
|
2877
|
+
function observeAnnotationElement(root, id, onChange) {
|
|
2878
|
+
let last;
|
|
2879
|
+
const resolve = () => {
|
|
2880
|
+
const found = annotationElementFor(root, id);
|
|
2881
|
+
if (found === last) return;
|
|
2882
|
+
last = found;
|
|
2883
|
+
onChange(found);
|
|
2884
|
+
};
|
|
2885
|
+
resolve();
|
|
2886
|
+
const observer = new MutationObserver(resolve);
|
|
2887
|
+
observer.observe(root, {
|
|
2888
|
+
childList: true,
|
|
2889
|
+
subtree: true
|
|
2854
2890
|
});
|
|
2891
|
+
return () => observer.disconnect();
|
|
2892
|
+
}
|
|
2893
|
+
//#endregion
|
|
2894
|
+
//#region src/dom/popover-focus.ts
|
|
2895
|
+
/**
|
|
2896
|
+
* A press that will activate an annotation is not a dismissal: the popover follows the
|
|
2897
|
+
* activation. Mirrors the overlay's rule: a press on the annotation element or on its drawn
|
|
2898
|
+
* shape activates; a press on a custom element's own content belongs to that content.
|
|
2899
|
+
*/
|
|
2900
|
+
function shouldKeepOpenOnAnnotation(target) {
|
|
2901
|
+
if (!(target instanceof Element)) return false;
|
|
2902
|
+
const annotation = target.closest("[data-annotation]");
|
|
2903
|
+
if (!annotation) return false;
|
|
2904
|
+
return target === annotation || !annotation.hasAttribute("data-custom");
|
|
2905
|
+
}
|
|
2906
|
+
function createFocusReturn(opts) {
|
|
2907
|
+
let fallbacks = [];
|
|
2908
|
+
/** Set by `beforeRemove`: the close comes from removing the anchor's annotation. */
|
|
2909
|
+
let removing = false;
|
|
2855
2910
|
return {
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2911
|
+
beforeRemove(anchor, prefer) {
|
|
2912
|
+
fallbacks = [];
|
|
2913
|
+
removing = true;
|
|
2914
|
+
const siblings = anchor?.parentElement ? [...anchor.parentElement.children].filter((el) => el instanceof HTMLElement && el.hasAttribute("data-annotation")) : [];
|
|
2915
|
+
const i = anchor ? siblings.indexOf(anchor) : -1;
|
|
2916
|
+
if (i >= 0) {
|
|
2917
|
+
const after = siblings.slice(i + 1);
|
|
2918
|
+
const preferred = prefer ? after.filter((el) => el.matches(prefer)) : [];
|
|
2919
|
+
fallbacks.push(...preferred, ...after, ...siblings.slice(0, i).reverse());
|
|
2920
|
+
}
|
|
2863
2921
|
},
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2922
|
+
onCloseAutoFocus(event, anchor) {
|
|
2923
|
+
event.preventDefault();
|
|
2924
|
+
const plan = fallbacks;
|
|
2925
|
+
const removed = removing;
|
|
2926
|
+
fallbacks = [];
|
|
2927
|
+
removing = false;
|
|
2928
|
+
const root = opts.root();
|
|
2929
|
+
if (!root) return;
|
|
2930
|
+
const activated = opts.activated();
|
|
2931
|
+
if (activated !== null && annotationElementFor(root, activated)) return;
|
|
2932
|
+
const active = root.ownerDocument.activeElement;
|
|
2933
|
+
if (active && active !== root && root.contains(active)) return;
|
|
2934
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
2935
|
+
(removed ? [...plan, scroll] : [
|
|
2936
|
+
anchor,
|
|
2937
|
+
...plan,
|
|
2938
|
+
scroll
|
|
2939
|
+
]).find((el) => el?.isConnected)?.focus({ preventScroll: true });
|
|
2868
2940
|
}
|
|
2869
2941
|
};
|
|
2870
2942
|
}
|
|
2871
2943
|
//#endregion
|
|
2872
|
-
//#region src/dom/
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
const
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
const w = writable({ ...t.defaultOptions ?? {} });
|
|
2885
|
-
byId.set(t.id, w);
|
|
2886
|
-
views.set(t.id, readonly(w));
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
register(tools);
|
|
2890
|
-
return {
|
|
2891
|
-
readable(id) {
|
|
2892
|
-
entry(id);
|
|
2893
|
-
return views.get(id);
|
|
2894
|
-
},
|
|
2895
|
-
get: (id) => entry(id).get(),
|
|
2896
|
-
merge(id, patch) {
|
|
2897
|
-
const w = entry(id);
|
|
2898
|
-
w.set({
|
|
2899
|
-
...w.get(),
|
|
2900
|
-
...patch
|
|
2901
|
-
});
|
|
2902
|
-
},
|
|
2903
|
-
register
|
|
2904
|
-
};
|
|
2944
|
+
//#region src/dom/viewer/dom.ts
|
|
2945
|
+
/** `document.createElement` with attributes and children. */
|
|
2946
|
+
function h(tag, attrs = {}, ...children) {
|
|
2947
|
+
const el = document.createElement(tag);
|
|
2948
|
+
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
2949
|
+
el.append(...children);
|
|
2950
|
+
return el;
|
|
2951
|
+
}
|
|
2952
|
+
function button$1(label, onClick) {
|
|
2953
|
+
const b = h("button", { type: "button" }, label);
|
|
2954
|
+
b.addEventListener("click", onClick);
|
|
2955
|
+
return b;
|
|
2905
2956
|
}
|
|
2906
2957
|
//#endregion
|
|
2907
|
-
//#region src/dom/
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
const preview = host.ownerDocument.createElement("div");
|
|
2942
|
-
preview.className = "ink-annotation ink-edit-preview";
|
|
2943
|
-
preview.dataset.inkPreview = "";
|
|
2944
|
-
preview.dataset.type = a.type;
|
|
2945
|
-
preview.style.cssText = "position:absolute;pointer-events:none;";
|
|
2946
|
-
return preview;
|
|
2947
|
-
};
|
|
2948
|
-
/** Shows `draft` in `preview` beside `el`, which is hidden meanwhile. */
|
|
2949
|
-
const showPreview = (preview, el, draft, start) => {
|
|
2950
|
-
drawPreview(preview, draft, start);
|
|
2951
|
-
if (el) el.style.visibility = "hidden";
|
|
2952
|
-
if (!preview.isConnected) (el?.parentElement ?? start.host).append(preview);
|
|
2958
|
+
//#region src/dom/viewer/popover.ts
|
|
2959
|
+
/** Space between the anchor and the panel, and between the panel and the window edge. */
|
|
2960
|
+
const GAP = 6;
|
|
2961
|
+
const MARGIN = 8;
|
|
2962
|
+
const TABBABLE$1 = "button, input, select, textarea, a[href], [tabindex]";
|
|
2963
|
+
/**
|
|
2964
|
+
* Positions `panel` (fixed) below the `anchor` rectangle, or above it when only that fits,
|
|
2965
|
+
* kept inside the window. For panels anchored to something other than an annotation.
|
|
2966
|
+
*/
|
|
2967
|
+
function placePanel(panel, anchor) {
|
|
2968
|
+
const { innerWidth: vw, innerHeight: vh } = window;
|
|
2969
|
+
panel.style.position = "fixed";
|
|
2970
|
+
panel.style.left = "0px";
|
|
2971
|
+
panel.style.top = "0px";
|
|
2972
|
+
const w = panel.offsetWidth;
|
|
2973
|
+
const ht = panel.offsetHeight;
|
|
2974
|
+
let top = anchor.bottom + GAP;
|
|
2975
|
+
if (top + ht > vh - MARGIN && anchor.top - GAP - ht >= MARGIN) top = anchor.top - GAP - ht;
|
|
2976
|
+
top = Math.max(MARGIN, Math.min(top, vh - ht - MARGIN));
|
|
2977
|
+
const left = Math.max(MARGIN, Math.min(anchor.left, vw - w - MARGIN));
|
|
2978
|
+
panel.style.left = `${left}px`;
|
|
2979
|
+
panel.style.top = `${top}px`;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Calls `onChange` with `el` while it is under `root` and null while it is not: once now, then
|
|
2983
|
+
* whenever that changes. Returns a function that stops observing.
|
|
2984
|
+
*/
|
|
2985
|
+
function observeElement(root, el, onChange) {
|
|
2986
|
+
let last;
|
|
2987
|
+
const resolve = () => {
|
|
2988
|
+
const found = root.contains(el) ? el : null;
|
|
2989
|
+
if (found === last) return;
|
|
2990
|
+
last = found;
|
|
2991
|
+
onChange(found);
|
|
2953
2992
|
};
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2993
|
+
resolve();
|
|
2994
|
+
const observer = new MutationObserver(resolve);
|
|
2995
|
+
observer.observe(root, {
|
|
2996
|
+
childList: true,
|
|
2997
|
+
subtree: true
|
|
2998
|
+
});
|
|
2999
|
+
return () => observer.disconnect();
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Creates a non-modal `role="dialog"` panel on `<body>`, marked `data-ink-popover` so the theme
|
|
3003
|
+
* gives it its tokens, that an app anchors to an annotation with `show(id)` (for instance on
|
|
3004
|
+
* `viewer.activated`), or to any element in the viewer with `showAt(el)`. The panel follows the annotation's element as pages mount and unmount:
|
|
3005
|
+
* it hides while the element is gone and comes back with it. Tab loops inside the panel, and
|
|
3006
|
+
* Escape or a press outside calls `onDismiss`. Call `destroy` when done.
|
|
3007
|
+
*/
|
|
3008
|
+
function createPopover(opts) {
|
|
3009
|
+
const { root, focusReturn } = opts;
|
|
3010
|
+
const panel = h("div", {
|
|
3011
|
+
...opts.attrs,
|
|
3012
|
+
role: "dialog",
|
|
3013
|
+
"data-ink-popover": "",
|
|
3014
|
+
hidden: ""
|
|
3015
|
+
});
|
|
3016
|
+
document.body.append(panel);
|
|
3017
|
+
/** What the panel is anchored to: an annotation id, or an element from `showAt`. */
|
|
3018
|
+
let anchored = null;
|
|
3019
|
+
let anchor = null;
|
|
3020
|
+
let stopObserving = () => void 0;
|
|
3021
|
+
/** The element that had focus in the panel when the anchor went away. */
|
|
3022
|
+
let stranded = null;
|
|
3023
|
+
const place = () => {
|
|
3024
|
+
if (!anchor || panel.hidden) return;
|
|
3025
|
+
placePanel(panel, anchor.getBoundingClientRect());
|
|
3026
|
+
};
|
|
3027
|
+
const resizes = new ResizeObserver(place);
|
|
3028
|
+
resizes.observe(panel);
|
|
3029
|
+
const onAnchor = (el, closing = false) => {
|
|
3030
|
+
if (anchor) resizes.unobserve(anchor);
|
|
3031
|
+
anchor = el;
|
|
3032
|
+
if (!el) {
|
|
3033
|
+
const active = document.activeElement;
|
|
3034
|
+
if (!closing && !panel.hidden && active instanceof HTMLElement && panel.contains(active)) {
|
|
3035
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
3036
|
+
if (scroll) {
|
|
3037
|
+
stranded = active;
|
|
3038
|
+
scroll.focus({ preventScroll: true });
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
panel.hidden = true;
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
resizes.observe(el);
|
|
3045
|
+
const wasHidden = panel.hidden;
|
|
3046
|
+
panel.hidden = false;
|
|
3047
|
+
place();
|
|
3048
|
+
if (wasHidden && stranded) {
|
|
3049
|
+
const back = stranded;
|
|
3050
|
+
stranded = null;
|
|
3051
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
3052
|
+
if (panel.contains(back) && document.activeElement === scroll) back.focus({ preventScroll: true });
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
const onKey = (e) => {
|
|
3056
|
+
if (e.key !== "Escape" || panel.hidden || e.defaultPrevented) return;
|
|
3057
|
+
const target = e.target;
|
|
3058
|
+
if (!(target instanceof Node) || !(panel.contains(target) || root.contains(target))) return;
|
|
3059
|
+
e.preventDefault();
|
|
3060
|
+
opts.onDismiss();
|
|
3061
|
+
};
|
|
3062
|
+
const onPress = (e) => {
|
|
3063
|
+
if (panel.hidden || panel.contains(e.target)) return;
|
|
3064
|
+
if (shouldKeepOpenOnAnnotation(e.target)) return;
|
|
3065
|
+
opts.onDismiss();
|
|
3066
|
+
};
|
|
3067
|
+
const onTab = (e) => {
|
|
3068
|
+
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3069
|
+
const tabbable = [...panel.querySelectorAll(TABBABLE$1)].filter((el) => !el.disabled && el.tabIndex >= 0);
|
|
3070
|
+
const first = tabbable[0];
|
|
3071
|
+
const last = tabbable.at(-1);
|
|
3072
|
+
if (!first || !last) return;
|
|
3073
|
+
const active = document.activeElement;
|
|
3074
|
+
if (e.shiftKey ? active !== first : active !== last) return;
|
|
3075
|
+
e.preventDefault();
|
|
3076
|
+
(e.shiftKey ? last : first).focus();
|
|
3077
|
+
};
|
|
3078
|
+
panel.addEventListener("keydown", onTab);
|
|
3079
|
+
document.addEventListener("keydown", onKey);
|
|
3080
|
+
document.addEventListener("pointerdown", onPress, true);
|
|
3081
|
+
document.addEventListener("scroll", place, true);
|
|
3082
|
+
window.addEventListener("resize", place);
|
|
3083
|
+
return {
|
|
3084
|
+
panel,
|
|
3085
|
+
get anchor() {
|
|
3086
|
+
return anchor;
|
|
3087
|
+
},
|
|
3088
|
+
show(id) {
|
|
3089
|
+
if (id === anchored) return;
|
|
3090
|
+
stopObserving();
|
|
3091
|
+
anchored = id;
|
|
3092
|
+
stopObserving = observeAnnotationElement(root, id, onAnchor);
|
|
3093
|
+
},
|
|
3094
|
+
showAt(el) {
|
|
3095
|
+
if (el === anchored) return;
|
|
3096
|
+
stopObserving();
|
|
3097
|
+
anchored = el;
|
|
3098
|
+
stopObserving = observeElement(root, el, onAnchor);
|
|
3099
|
+
},
|
|
3100
|
+
hide() {
|
|
3101
|
+
if (anchored === null) return;
|
|
3102
|
+
stopObserving();
|
|
3103
|
+
stopObserving = () => void 0;
|
|
3104
|
+
const element = typeof anchored === "string" ? null : anchored;
|
|
3105
|
+
anchored = null;
|
|
3106
|
+
const back = anchor;
|
|
3107
|
+
stranded = null;
|
|
3108
|
+
onAnchor(null, true);
|
|
3109
|
+
queueMicrotask(() => {
|
|
3110
|
+
if (anchored !== null || !panel.isConnected) return;
|
|
3111
|
+
if (element?.isConnected) {
|
|
3112
|
+
const active = document.activeElement;
|
|
3113
|
+
if (!(active && active !== root && root.contains(active))) element.focus({ preventScroll: true });
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
focusReturn.onCloseAutoFocus(new Event("closeautofocus", { cancelable: true }), back);
|
|
3117
|
+
});
|
|
3118
|
+
},
|
|
3119
|
+
destroy() {
|
|
3120
|
+
stopObserving();
|
|
3121
|
+
resizes.disconnect();
|
|
3122
|
+
document.removeEventListener("keydown", onKey);
|
|
3123
|
+
document.removeEventListener("pointerdown", onPress, true);
|
|
3124
|
+
panel.removeEventListener("keydown", onTab);
|
|
3125
|
+
document.removeEventListener("scroll", place, true);
|
|
3126
|
+
window.removeEventListener("resize", place);
|
|
3127
|
+
panel.remove();
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
//#endregion
|
|
3132
|
+
//#region src/dom/plugins/api/card.ts
|
|
3133
|
+
function el(tag, className, ...children) {
|
|
3134
|
+
const e = document.createElement(tag);
|
|
3135
|
+
e.className = className;
|
|
3136
|
+
e.append(...children);
|
|
3137
|
+
return e;
|
|
3138
|
+
}
|
|
3139
|
+
function createCard(ctx, records, opts) {
|
|
3140
|
+
/** Made on first open, in the viewer root the mark is in. */
|
|
3141
|
+
let popover = null;
|
|
3142
|
+
let root = null;
|
|
3143
|
+
/** The record on show. */
|
|
3144
|
+
let shown = null;
|
|
3145
|
+
/** Set while an action runs: the action decides what happens after it. */
|
|
3146
|
+
let acting = false;
|
|
3147
|
+
/** The mark whose card is open: its `aria-expanded` is true. */
|
|
3148
|
+
let expanded = null;
|
|
3149
|
+
const expand = (mark) => {
|
|
3150
|
+
expanded?.setAttribute("aria-expanded", "false");
|
|
3151
|
+
expanded = mark;
|
|
3152
|
+
mark?.setAttribute("aria-expanded", "true");
|
|
3153
|
+
};
|
|
3154
|
+
/** The shown record as it stands now, while it still has a mark. */
|
|
3155
|
+
const current = () => {
|
|
3156
|
+
const record = shown === null ? void 0 : records.find(shown);
|
|
3157
|
+
return record && opts.marked(record) ? record : void 0;
|
|
3158
|
+
};
|
|
3159
|
+
const close = () => {
|
|
3160
|
+
shown = null;
|
|
3161
|
+
expand(null);
|
|
3162
|
+
popover?.hide();
|
|
3163
|
+
};
|
|
3164
|
+
/** Runs `fn` as one undo step, then re-renders, or closes and moves focus on. */
|
|
3165
|
+
const act = (fn) => {
|
|
3166
|
+
if (!popover || !root || shown === null) return;
|
|
3167
|
+
const marks = opts.marks(root);
|
|
3168
|
+
const i = popover.anchor ? marks.indexOf(popover.anchor) : -1;
|
|
3169
|
+
const after = i < 0 ? [] : [...marks.slice(i + 1), ...marks.slice(0, i).reverse()];
|
|
3170
|
+
acting = true;
|
|
3171
|
+
try {
|
|
3172
|
+
ctx.history.batch(fn);
|
|
3173
|
+
} finally {
|
|
3174
|
+
acting = false;
|
|
3175
|
+
}
|
|
3176
|
+
const record = current();
|
|
3177
|
+
if (record) return render(record);
|
|
3178
|
+
after.find((m) => m.isConnected)?.focus({ preventScroll: true });
|
|
3179
|
+
close();
|
|
3180
|
+
};
|
|
3181
|
+
/** Fills the panel from `record`, keeping focus on the same action. */
|
|
3182
|
+
const render = (record) => {
|
|
3183
|
+
const spec = opts.spec(record);
|
|
3184
|
+
const panel = popover.panel;
|
|
3185
|
+
const buttons = () => [...panel.querySelectorAll(".ink-card-actions > button")];
|
|
3186
|
+
const focused = buttons().indexOf(document.activeElement);
|
|
3187
|
+
panel.className = spec.className ? `ink-card ${spec.className}` : "ink-card";
|
|
3188
|
+
panel.setAttribute("aria-label", spec.label ?? spec.title);
|
|
3189
|
+
const parts = [el("p", "ink-card-title", spec.title)];
|
|
3190
|
+
if (spec.meta !== void 0) {
|
|
3191
|
+
const meta = el("p", "ink-card-meta", spec.meta);
|
|
3192
|
+
if (spec.swatch !== void 0) {
|
|
3193
|
+
const swatch = el("span", "ink-card-swatch");
|
|
3194
|
+
swatch.style.background = spec.swatch;
|
|
3195
|
+
meta.prepend(swatch);
|
|
3196
|
+
}
|
|
3197
|
+
parts.push(meta);
|
|
3198
|
+
}
|
|
3199
|
+
if (spec.body !== void 0) parts.push(el("p", "ink-card-body", spec.body));
|
|
3200
|
+
const actions = el("div", "ink-card-actions");
|
|
3201
|
+
for (const [label, fn] of Object.entries(spec.actions)) {
|
|
3202
|
+
const button = el("button", "", label);
|
|
3203
|
+
button.type = "button";
|
|
3204
|
+
button.addEventListener("click", () => act(fn));
|
|
3205
|
+
actions.append(button);
|
|
3206
|
+
}
|
|
3207
|
+
parts.push(actions);
|
|
3208
|
+
panel.replaceChildren(...parts);
|
|
3209
|
+
if (focused >= 0) {
|
|
3210
|
+
const now = buttons();
|
|
3211
|
+
(now[focused] ?? now[0])?.focus({ preventScroll: true });
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
ctx.onDocument(() => {
|
|
3215
|
+
const stop = records.list.subscribe(() => {
|
|
3216
|
+
if (acting || shown === null) return;
|
|
3217
|
+
const record = current();
|
|
3218
|
+
if (record) render(record);
|
|
3219
|
+
else close();
|
|
3220
|
+
});
|
|
3221
|
+
return () => {
|
|
3222
|
+
stop();
|
|
3223
|
+
shown = null;
|
|
3224
|
+
expand(null);
|
|
3225
|
+
popover?.destroy();
|
|
3226
|
+
popover = null;
|
|
3227
|
+
root = null;
|
|
3228
|
+
};
|
|
3229
|
+
});
|
|
3230
|
+
return {
|
|
3231
|
+
open(record, mark) {
|
|
3232
|
+
const viewer = mark.closest("[data-ink-root]");
|
|
3233
|
+
if (!viewer) return;
|
|
3234
|
+
if (viewer !== root) {
|
|
3235
|
+
popover?.destroy();
|
|
3236
|
+
root = viewer;
|
|
3237
|
+
popover = createPopover({
|
|
3238
|
+
root: viewer,
|
|
3239
|
+
attrs: { class: "ink-card" },
|
|
3240
|
+
focusReturn: createFocusReturn({
|
|
3241
|
+
root: () => viewer,
|
|
3242
|
+
activated: () => null
|
|
3243
|
+
}),
|
|
3244
|
+
onDismiss: close
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
shown = record.id;
|
|
3248
|
+
render(record);
|
|
3249
|
+
popover.showAt(mark);
|
|
3250
|
+
expand(mark);
|
|
3251
|
+
popover.panel.querySelector(".ink-card-actions > button")?.focus();
|
|
3252
|
+
},
|
|
3253
|
+
expanded: (mark) => mark === expanded
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
//#endregion
|
|
3257
|
+
//#region src/dom/plugins/api/marks.ts
|
|
3258
|
+
/** Events stopped on a mark, so the viewer's root never sees a press on it. */
|
|
3259
|
+
const STOPPED = [
|
|
3260
|
+
"pointerdown",
|
|
3261
|
+
"pointerup",
|
|
3262
|
+
"mousedown",
|
|
3263
|
+
"mouseup",
|
|
3264
|
+
"click"
|
|
3265
|
+
];
|
|
3266
|
+
/** Attributes the plugin's `attrs` cannot set: core sets them, or they have options of their own. */
|
|
3267
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
3268
|
+
"style",
|
|
3269
|
+
"class",
|
|
3270
|
+
"type"
|
|
3271
|
+
]);
|
|
3272
|
+
function createMarks(ctx, records, opts) {
|
|
3273
|
+
const applied = /* @__PURE__ */ new WeakMap();
|
|
3274
|
+
const marked = (record) => opts.where?.(record) ?? true;
|
|
3275
|
+
const spec = opts.card?.bind(opts);
|
|
3276
|
+
const card = spec && createCard(ctx, records, {
|
|
3277
|
+
spec,
|
|
3278
|
+
marked,
|
|
3279
|
+
marks: (root) => [...root.querySelectorAll(`.ink-plugin-layer[data-plugin="${ctx.id}"] > button`)].filter((mark) => applied.has(mark))
|
|
3280
|
+
});
|
|
3281
|
+
/** What activating a mark does. */
|
|
3282
|
+
const activate = (record, mark) => {
|
|
3283
|
+
card?.open(record, mark);
|
|
3284
|
+
opts.onActivate?.(record, mark);
|
|
3285
|
+
};
|
|
3286
|
+
const create = (doc) => {
|
|
3287
|
+
const mark = doc.createElement("button");
|
|
3288
|
+
if (opts.className) mark.className = opts.className;
|
|
3289
|
+
for (const type of STOPPED) mark.addEventListener(type, (e) => e.stopPropagation());
|
|
3290
|
+
mark.addEventListener("click", () => {
|
|
3291
|
+
const state = applied.get(mark);
|
|
3292
|
+
if (state) activate(state.record, mark);
|
|
3293
|
+
});
|
|
3294
|
+
return mark;
|
|
3295
|
+
};
|
|
3296
|
+
/** Brings `mark` up to date with `record`, placed through `transform`. */
|
|
3297
|
+
const apply = (mark, record, transform) => {
|
|
3298
|
+
const prev = applied.get(mark);
|
|
3299
|
+
const style = opts.style?.(record) ?? {};
|
|
3300
|
+
const styleNames = Object.keys(style).map(toCssName);
|
|
3301
|
+
const attrs = Object.fromEntries(Object.entries(opts.attrs?.(record) ?? {}).filter(([name]) => !RESERVED.has(name.toLowerCase())));
|
|
3302
|
+
for (const name of prev?.style ?? []) if (!styleNames.includes(name)) mark.style.removeProperty(name);
|
|
3303
|
+
for (const key of prev?.attrs ?? []) if (!(key in attrs)) mark.removeAttribute(key);
|
|
3304
|
+
for (const [key, value] of Object.entries(style)) mark.style.setProperty(toCssName(key), value);
|
|
3305
|
+
for (const [name, value] of Object.entries(attrs)) mark.setAttribute(name, value);
|
|
3306
|
+
mark.type = "button";
|
|
3307
|
+
mark.style.position = "absolute";
|
|
3308
|
+
mark.dataset.record = record.id;
|
|
3309
|
+
mark.dataset.plugin = ctx.id;
|
|
3310
|
+
mark.setAttribute("aria-label", opts.label(record));
|
|
3311
|
+
if (card) {
|
|
3312
|
+
mark.setAttribute("aria-haspopup", "dialog");
|
|
3313
|
+
mark.setAttribute("aria-expanded", String(card.expanded(mark)));
|
|
3314
|
+
}
|
|
3315
|
+
const r = transform.toCssRect(opts.rect(record));
|
|
3316
|
+
mark.style.left = `${r.left}px`;
|
|
3317
|
+
mark.style.top = `${r.top}px`;
|
|
3318
|
+
mark.style.width = `${r.width}px`;
|
|
3319
|
+
mark.style.height = `${r.height}px`;
|
|
3320
|
+
applied.set(mark, {
|
|
3321
|
+
record,
|
|
3322
|
+
style: styleNames,
|
|
3323
|
+
attrs: Object.keys(attrs)
|
|
3324
|
+
});
|
|
3325
|
+
};
|
|
3326
|
+
/**
|
|
3327
|
+
* Makes `el`'s marks match the page's records, in list order: marks that stay are moved,
|
|
3328
|
+
* new ones inserted, the rest removed.
|
|
3329
|
+
*/
|
|
3330
|
+
const draw = (el, page, transform) => {
|
|
3331
|
+
const list = records.list.get().filter((r) => r.page === page && marked(r));
|
|
3332
|
+
const ids = new Set(list.map((r) => r.id));
|
|
3333
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3334
|
+
for (const mark of [...el.children]) {
|
|
3335
|
+
const id = mark.dataset.record;
|
|
3336
|
+
if (id !== void 0 && ids.has(id) && !kept.has(id)) kept.set(id, mark);
|
|
3337
|
+
else mark.remove();
|
|
3338
|
+
}
|
|
3339
|
+
let next = el.firstElementChild;
|
|
3340
|
+
for (const record of list) {
|
|
3341
|
+
const mark = kept.get(record.id) ?? create(el.ownerDocument);
|
|
3342
|
+
apply(mark, record, transform);
|
|
3343
|
+
if (mark === next) next = mark.nextElementSibling;
|
|
3344
|
+
else el.insertBefore(mark, next);
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
let scrolling = false;
|
|
3348
|
+
/** Ids that started to get a mark in the change under way, read once it settles. */
|
|
3349
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
3350
|
+
/** Scrolls to the first fresh record still marked, so a batch of adds scrolls once. */
|
|
3351
|
+
const reveal = () => {
|
|
3352
|
+
const [first] = [...fresh].map((id) => records.find(id)).filter((r) => r !== void 0 && marked(r)).sort(readingOrder((r) => opts.rect(r)));
|
|
3353
|
+
fresh.clear();
|
|
3354
|
+
if (first) ctx.scrollTo(first.page, opts.rect(first), { behavior: reducedMotion() ? "instant" : "smooth" });
|
|
3355
|
+
};
|
|
3356
|
+
/** Notes a record that starts to get a mark; replaces (undo, redo, reset) never count. */
|
|
3357
|
+
const watch = (change) => {
|
|
3358
|
+
const record = change.kind === "add" ? change.record : change.kind === "update" && !marked(change.before) ? change.after : void 0;
|
|
3359
|
+
if (!record || !marked(record)) return;
|
|
3360
|
+
if (fresh.size === 0) queueMicrotask(reveal);
|
|
3361
|
+
fresh.add(record.id);
|
|
3362
|
+
};
|
|
3363
|
+
const layer = ctx.layer({ render: draw });
|
|
3364
|
+
ctx.onDocument(() => {
|
|
3365
|
+
const stopDraw = records.list.subscribe(() => layer.invalidate());
|
|
3366
|
+
const stopWatch = records.store()?.on("change", (change) => {
|
|
3367
|
+
if (scrolling) watch(change);
|
|
3368
|
+
});
|
|
3369
|
+
return () => {
|
|
3370
|
+
stopDraw();
|
|
3371
|
+
stopWatch?.();
|
|
3372
|
+
fresh.clear();
|
|
3373
|
+
};
|
|
3374
|
+
});
|
|
3375
|
+
const handle = {
|
|
3376
|
+
refresh: () => layer.invalidate(),
|
|
3377
|
+
scrollToFirst() {
|
|
3378
|
+
scrolling = true;
|
|
3379
|
+
return handle;
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
return handle;
|
|
3383
|
+
}
|
|
3384
|
+
/** Page first, then top to bottom and left to right, by each record's rect in user space. */
|
|
3385
|
+
function readingOrder(rect) {
|
|
3386
|
+
return (a, b) => {
|
|
3387
|
+
const ra = rect(a);
|
|
3388
|
+
const rb = rect(b);
|
|
3389
|
+
return a.page - b.page || rb.y + rb.height - (ra.y + ra.height) || ra.x - rb.x;
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
function reducedMotion() {
|
|
3393
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
3394
|
+
}
|
|
3395
|
+
/** `borderColor` → `border-color`, as `setProperty` takes it; `--custom` names stay as given. */
|
|
3396
|
+
function toCssName(key) {
|
|
3397
|
+
return key.startsWith("--") ? key : key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
3398
|
+
}
|
|
3399
|
+
//#endregion
|
|
3400
|
+
//#region src/dom/plugins/api/links.ts
|
|
3401
|
+
function isObject(value) {
|
|
3402
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3403
|
+
}
|
|
3404
|
+
/** Links `type` annotations to records; registers one `onAnnotationEdit` listener. */
|
|
3405
|
+
function createLink(deps, type, spec) {
|
|
3406
|
+
const linkOf = (a) => {
|
|
3407
|
+
if (a.type !== type) return void 0;
|
|
3408
|
+
const own = a.custom?.[deps.plugin];
|
|
3409
|
+
const id = isObject(own) ? own[deps.name] : void 0;
|
|
3410
|
+
return typeof id === "string" ? id : void 0;
|
|
3411
|
+
};
|
|
3412
|
+
deps.onAnnotationEdit((edit) => {
|
|
3413
|
+
if (edit.kind === "add") return;
|
|
3414
|
+
const annotation = edit.kind === "remove" ? edit.annotation : edit.after;
|
|
3415
|
+
const id = linkOf(annotation);
|
|
3416
|
+
const record = id === void 0 ? void 0 : deps.find(id);
|
|
3417
|
+
if (!record) return;
|
|
3418
|
+
if (edit.kind === "remove") spec.onRemoved?.(record, annotation);
|
|
3419
|
+
else spec.onChanged?.(record, annotation);
|
|
3420
|
+
});
|
|
3421
|
+
return {
|
|
3422
|
+
add(record) {
|
|
3423
|
+
const doc = deps.document();
|
|
3424
|
+
if (!doc) return void 0;
|
|
3425
|
+
const { color, custom, ...fields } = spec.from(record);
|
|
3426
|
+
const own = custom?.[deps.plugin];
|
|
3427
|
+
return doc.annotations.add({
|
|
3428
|
+
...fields,
|
|
3429
|
+
type,
|
|
3430
|
+
color: color ?? doc.annotations.defaults.color(type),
|
|
3431
|
+
custom: {
|
|
3432
|
+
...custom,
|
|
3433
|
+
[deps.plugin]: {
|
|
3434
|
+
...isObject(own) ? own : {},
|
|
3435
|
+
[deps.name]: record.id
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
},
|
|
3440
|
+
of(record) {
|
|
3441
|
+
const doc = deps.document();
|
|
3442
|
+
return doc ? doc.annotations.list.get().filter((a) => linkOf(a) === record.id) : [];
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3447
|
+
//#region src/dom/plugins/api/records.ts
|
|
3448
|
+
const EMPTY$1 = Object.freeze([]);
|
|
3449
|
+
/** The list of whichever store `source` holds, `[]` for none; emits on swaps and changes. */
|
|
3450
|
+
function listOf(source) {
|
|
3451
|
+
const read = (store) => store?.list.get() ?? EMPTY$1;
|
|
3452
|
+
return {
|
|
3453
|
+
get: () => read(source.get()),
|
|
3454
|
+
subscribe(fn) {
|
|
3455
|
+
let last = read(source.get());
|
|
3456
|
+
let stopList;
|
|
3457
|
+
const emit = (next) => {
|
|
3458
|
+
if (Object.is(next, last) || next.length === 0 && last.length === 0) return;
|
|
3459
|
+
last = next;
|
|
3460
|
+
fn(next);
|
|
3461
|
+
};
|
|
3462
|
+
const follow = (store) => {
|
|
3463
|
+
stopList?.();
|
|
3464
|
+
stopList = store?.list.subscribe(emit);
|
|
3465
|
+
emit(read(store));
|
|
3466
|
+
};
|
|
3467
|
+
follow(source.get());
|
|
3468
|
+
const stopSource = source.subscribe(follow);
|
|
3469
|
+
return () => {
|
|
3470
|
+
stopSource();
|
|
3471
|
+
stopList?.();
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
function matches(record, match) {
|
|
3477
|
+
return Object.entries(match).every(([k, v]) => record[k] === v);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Builds a handle over `source`, a plugin store from `ctx.store`. Every call reads the store
|
|
3481
|
+
* `source` holds then.
|
|
3482
|
+
*/
|
|
3483
|
+
function createRecords(deps, source, base = source) {
|
|
3484
|
+
const current = () => source.get();
|
|
3485
|
+
const all = () => current()?.list.get() ?? EMPTY$1;
|
|
3486
|
+
return {
|
|
3487
|
+
list: listOf(source),
|
|
3488
|
+
all,
|
|
3489
|
+
find: (id) => current()?.get(id),
|
|
3490
|
+
has: (id) => current()?.get(id) !== void 0,
|
|
3491
|
+
where: (match) => all().filter(typeof match === "function" ? match : (r) => matches(r, match)),
|
|
3492
|
+
add: (record) => current()?.add(record),
|
|
3493
|
+
addMissing(records) {
|
|
3494
|
+
const store = current();
|
|
3495
|
+
if (!store) return [];
|
|
3496
|
+
const added = [];
|
|
3497
|
+
deps.batch(() => {
|
|
3498
|
+
for (const record of records) if (!store.get(record.id)) added.push(store.add(record));
|
|
3499
|
+
});
|
|
3500
|
+
return added;
|
|
3501
|
+
},
|
|
3502
|
+
update: (id, patch) => current()?.update(id, patch),
|
|
3503
|
+
remove: (id) => current()?.remove(id),
|
|
3504
|
+
in(doc) {
|
|
3505
|
+
const captured = deps.storesDocument() === doc ? base.get() : null;
|
|
3506
|
+
return createRecords(deps, derived(base, (store) => store && store === captured ? store : null), base);
|
|
3507
|
+
},
|
|
3508
|
+
store: current,
|
|
3509
|
+
annotates: (type, spec) => createLink({
|
|
3510
|
+
plugin: deps.plugin,
|
|
3511
|
+
name: deps.name,
|
|
3512
|
+
onAnnotationEdit: (fn) => deps.onAnnotationEdit(fn),
|
|
3513
|
+
document: () => current() ? deps.storesDocument() : null,
|
|
3514
|
+
find: (id) => current()?.get(id)
|
|
3515
|
+
}, type, spec)
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
//#endregion
|
|
3519
|
+
//#region src/dom/plugins/api/api.ts
|
|
3520
|
+
function createPluginApi(ctx, options) {
|
|
3521
|
+
let storesDocument = null;
|
|
3522
|
+
ctx.onDocument((doc) => {
|
|
3523
|
+
storesDocument = doc;
|
|
3524
|
+
return () => {
|
|
3525
|
+
storesDocument = null;
|
|
3526
|
+
};
|
|
3527
|
+
});
|
|
3528
|
+
const batch = (fn) => ctx.history.batch(fn);
|
|
3529
|
+
return {
|
|
3530
|
+
...ctx,
|
|
3531
|
+
records(name, opts) {
|
|
3532
|
+
const source = ctx.store(name, opts ?? options.records?.[name]);
|
|
3533
|
+
return createRecords({
|
|
3534
|
+
batch,
|
|
3535
|
+
storesDocument: () => storesDocument,
|
|
3536
|
+
plugin: ctx.id,
|
|
3537
|
+
name,
|
|
3538
|
+
onAnnotationEdit: (fn) => ctx.onAnnotationEdit(fn)
|
|
3539
|
+
}, source);
|
|
3540
|
+
},
|
|
3541
|
+
batch,
|
|
3542
|
+
command: (id, label, run, opts) => createCommand(ctx, id, label, run, opts),
|
|
3543
|
+
marks: (records, opts) => createMarks(ctx, records, opts)
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
//#endregion
|
|
3547
|
+
//#region src/dom/plugins/define.ts
|
|
3548
|
+
const PLUGIN_ID = /^[a-z][a-z0-9-]*$/;
|
|
3549
|
+
function checkId(id) {
|
|
3550
|
+
if (!PLUGIN_ID.test(id)) throw new InkError("InvalidPluginId", `Plugin id ${JSON.stringify(id)} must be lower-case kebab`, { id });
|
|
3551
|
+
}
|
|
3552
|
+
function definePlugin(plugin, setup) {
|
|
3553
|
+
if (typeof plugin !== "string") {
|
|
3554
|
+
checkId(plugin.id);
|
|
3555
|
+
return plugin;
|
|
3556
|
+
}
|
|
3557
|
+
const id = plugin;
|
|
3558
|
+
checkId(id);
|
|
3559
|
+
return (options = {}) => ({
|
|
3560
|
+
id,
|
|
3561
|
+
setup: (ctx) => setup?.(createPluginApi(ctx, options), options)
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
//#endregion
|
|
3565
|
+
//#region src/dom/plugins/commands.ts
|
|
3566
|
+
/**
|
|
3567
|
+
* The error for a command id no plugin registered, naming the ids that were. Bindings log it
|
|
3568
|
+
* rather than throw, and show the control as it is before the plugins are set up.
|
|
3569
|
+
*/
|
|
3570
|
+
function unknownCommand(id, registered) {
|
|
3571
|
+
const known = registered.length > 0 ? registered.join(", ") : "none";
|
|
3572
|
+
return new InkError("UnknownCommand", `No plugin registered a command ${id} (registered: ${known})`, {
|
|
3573
|
+
id,
|
|
3574
|
+
registered
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
const EMPTY = {
|
|
3578
|
+
get: () => "",
|
|
3579
|
+
subscribe: () => () => {}
|
|
3580
|
+
};
|
|
3581
|
+
/**
|
|
3582
|
+
* A boolean readable computed from `sources`. Like `derived`, it subscribes to the sources only
|
|
3583
|
+
* while it has subscribers of its own; `watch` re-reads them, so a source can be swapped while
|
|
3584
|
+
* subscribed (a document's status when the document changes).
|
|
3585
|
+
*/
|
|
3586
|
+
function computed(compute, watch) {
|
|
3587
|
+
const subs = /* @__PURE__ */ new Set();
|
|
3588
|
+
let cached = false;
|
|
3589
|
+
let stop;
|
|
3590
|
+
const changed = () => {
|
|
3591
|
+
const next = compute();
|
|
3592
|
+
if (next === cached) return;
|
|
3593
|
+
cached = next;
|
|
3594
|
+
for (const sub of [...subs]) sub(next);
|
|
3595
|
+
};
|
|
3596
|
+
return {
|
|
3597
|
+
get: () => stop ? cached : compute(),
|
|
3598
|
+
subscribe(sub) {
|
|
3599
|
+
if (!stop) {
|
|
3600
|
+
cached = compute();
|
|
3601
|
+
stop = watch(changed);
|
|
3602
|
+
}
|
|
3603
|
+
subs.add(sub);
|
|
3604
|
+
return () => {
|
|
3605
|
+
if (!subs.delete(sub) || subs.size > 0) return;
|
|
3606
|
+
stop?.();
|
|
3607
|
+
stop = void 0;
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
/** True while `document` holds a document whose status is ready. */
|
|
3613
|
+
function documentReady(document) {
|
|
3614
|
+
return computed(() => document.get()?.status.get().state === "ready", (changed) => {
|
|
3615
|
+
let stopStatus;
|
|
3616
|
+
const follow = (doc) => {
|
|
3617
|
+
stopStatus?.();
|
|
3618
|
+
stopStatus = doc?.status.subscribe(changed);
|
|
3619
|
+
changed();
|
|
3620
|
+
};
|
|
3621
|
+
const stopDocument = document.subscribe(follow);
|
|
3622
|
+
follow(document.get());
|
|
3623
|
+
return () => {
|
|
3624
|
+
stopDocument();
|
|
3625
|
+
stopStatus?.();
|
|
3626
|
+
};
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Plugin commands by namespaced id. A command is busy from `run()` until the promise its spec
|
|
3631
|
+
* returned settles, and is disabled meanwhile, so a double click runs it once.
|
|
3632
|
+
*/
|
|
3633
|
+
function createCommandRegistry(opts) {
|
|
3634
|
+
const commands = /* @__PURE__ */ new Map();
|
|
3635
|
+
const ready = documentReady(opts.document);
|
|
3636
|
+
return {
|
|
3637
|
+
commands,
|
|
3638
|
+
add(plugin, spec) {
|
|
3639
|
+
const id = `${plugin}.${spec.id}`;
|
|
3640
|
+
if (commands.has(id)) throw new InkError("DuplicateId", `A command with id ${id} already exists`, { id });
|
|
3641
|
+
if (!spec.label.trim()) throw new InkError("InvalidCommandLabel", `Command ${id} needs a label`, { id });
|
|
3642
|
+
const busy = writable(false);
|
|
3643
|
+
const allowed = spec.enabled ?? ready;
|
|
3644
|
+
const enabled = computed(() => allowed.get() && !busy.get(), (changed) => {
|
|
3645
|
+
const stops = [allowed.subscribe(changed), busy.subscribe(changed)];
|
|
3646
|
+
return () => {
|
|
3647
|
+
for (const stop of stops) stop();
|
|
3648
|
+
};
|
|
3649
|
+
});
|
|
3650
|
+
const command = {
|
|
3651
|
+
id,
|
|
3652
|
+
label: spec.label,
|
|
3653
|
+
icon: spec.icon,
|
|
3654
|
+
enabled,
|
|
3655
|
+
busy: readonly(busy),
|
|
3656
|
+
status: spec.status ?? EMPTY,
|
|
3657
|
+
async run() {
|
|
3658
|
+
if (!enabled.get()) return;
|
|
3659
|
+
busy.set(true);
|
|
3660
|
+
try {
|
|
3661
|
+
await spec.run();
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
if (!(isInkError(error) && error.code === "DocumentDestroyed")) console.error(error);
|
|
3664
|
+
} finally {
|
|
3665
|
+
busy.set(false);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
commands.set(id, command);
|
|
3670
|
+
return command;
|
|
3671
|
+
}
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
//#endregion
|
|
3675
|
+
//#region src/dom/plugins/layer.ts
|
|
3676
|
+
function createLayerRegistry() {
|
|
3677
|
+
const layers = [];
|
|
3678
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3679
|
+
const emit = (event) => {
|
|
3680
|
+
for (const listener of [...listeners]) listener(event);
|
|
3681
|
+
};
|
|
3682
|
+
return {
|
|
3683
|
+
layers,
|
|
3684
|
+
watchLayers(listener) {
|
|
3685
|
+
listeners.add(listener);
|
|
3686
|
+
return () => listeners.delete(listener);
|
|
3687
|
+
},
|
|
3688
|
+
add(layer) {
|
|
3689
|
+
layers.push(layer);
|
|
3690
|
+
emit({
|
|
3691
|
+
type: "add",
|
|
3692
|
+
layer
|
|
3693
|
+
});
|
|
3694
|
+
return { invalidate: (page) => emit({
|
|
3695
|
+
type: "invalidate",
|
|
3696
|
+
layer,
|
|
3697
|
+
page
|
|
3698
|
+
}) };
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
/** Runs a plugin's drawing; a throw is logged so one plugin cannot break the page. */
|
|
3703
|
+
function guard$1(fn) {
|
|
3704
|
+
try {
|
|
3705
|
+
fn();
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
console.error(error);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
const CHILD_RULE = ":where(.ink-plugin-layer > *) { pointer-events: auto; }";
|
|
3711
|
+
/** One sheet per window: a constructed sheet can only be adopted in its own window. */
|
|
3712
|
+
const childSheets = /* @__PURE__ */ new WeakMap();
|
|
3713
|
+
/**
|
|
3714
|
+
* Puts the children's `pointer-events: auto` rule in the document or shadow root `el` lives
|
|
3715
|
+
* in: an adopted sheet, again if the host has since replaced its adopted sheets, or one
|
|
3716
|
+
* `<style>` element where adopted sheets are missing. A layer not yet connected gets it on a
|
|
3717
|
+
* later draw, which calls this again.
|
|
3718
|
+
*/
|
|
3719
|
+
function adoptChildRule(el) {
|
|
3720
|
+
if (!el.isConnected) return;
|
|
3721
|
+
const root = el.getRootNode();
|
|
3722
|
+
const doc = el.ownerDocument;
|
|
3723
|
+
const win = doc.defaultView;
|
|
3724
|
+
if (!root.adoptedStyleSheets || !win) {
|
|
3725
|
+
const parent = root === doc ? doc.head ?? doc.documentElement : root;
|
|
3726
|
+
if ([...parent.children].some((c) => c.matches("style[data-ink-plugin-layer]"))) return;
|
|
3727
|
+
const style = doc.createElement("style");
|
|
3728
|
+
style.dataset.inkPluginLayer = "";
|
|
3729
|
+
style.textContent = CHILD_RULE;
|
|
3730
|
+
parent.append(style);
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
let sheet = childSheets.get(win);
|
|
3734
|
+
if (!sheet) {
|
|
3735
|
+
sheet = new win.CSSStyleSheet();
|
|
3736
|
+
sheet.replaceSync(CHILD_RULE);
|
|
3737
|
+
childSheets.set(win, sheet);
|
|
3738
|
+
}
|
|
3739
|
+
if (!root.adoptedStyleSheets.includes(sheet)) root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Mounts `source`'s layers on the pages the engine mounts, and follows it: a layer added later
|
|
3743
|
+
* is drawn on every mounted page, and an invalidate redraws its page, or all of them.
|
|
3744
|
+
*
|
|
3745
|
+
* @internal For bindings, which mount plugin layers; plugin authors add a layer with
|
|
3746
|
+
* `ctx.layer`.
|
|
3747
|
+
*/
|
|
3748
|
+
function createPluginLayers(source) {
|
|
3749
|
+
const pages = /* @__PURE__ */ new Map();
|
|
3750
|
+
const draw = (layer, el, page, t) => {
|
|
3751
|
+
adoptChildRule(el);
|
|
3752
|
+
guard$1(() => layer.render(el, page, t));
|
|
3753
|
+
};
|
|
3754
|
+
const add = (layer, page, state) => {
|
|
3755
|
+
const el = state.host.ownerDocument.createElement("div");
|
|
3756
|
+
el.className = "ink-plugin-layer";
|
|
3757
|
+
el.dataset.plugin = layer.plugin;
|
|
3758
|
+
el.style.cssText = "position:absolute;inset:0;pointer-events:none;";
|
|
3759
|
+
state.host.append(el);
|
|
3760
|
+
state.elements.set(layer, el);
|
|
3761
|
+
draw(layer, el, page, state.transform);
|
|
3762
|
+
};
|
|
3763
|
+
return {
|
|
3764
|
+
mount(page, host, transform) {
|
|
3765
|
+
const state = {
|
|
3766
|
+
host,
|
|
3767
|
+
transform,
|
|
3768
|
+
elements: /* @__PURE__ */ new Map()
|
|
3769
|
+
};
|
|
3770
|
+
pages.set(page, state);
|
|
3771
|
+
for (const layer of source.layers) add(layer, page, state);
|
|
3772
|
+
return {
|
|
3773
|
+
update(next) {
|
|
3774
|
+
state.transform = next;
|
|
3775
|
+
for (const [layer, el] of state.elements) draw(layer, el, page, next);
|
|
3776
|
+
},
|
|
3777
|
+
destroy() {
|
|
3778
|
+
if (pages.get(page) === state) pages.delete(page);
|
|
3779
|
+
for (const [layer, el] of state.elements) {
|
|
3780
|
+
if (layer.destroy) guard$1(() => layer.destroy?.(el, page));
|
|
3781
|
+
el.remove();
|
|
3782
|
+
}
|
|
3783
|
+
state.elements.clear();
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
},
|
|
3787
|
+
destroy: source.watchLayers((event) => {
|
|
3788
|
+
if (event.type === "add") {
|
|
3789
|
+
for (const [page, state] of pages) add(event.layer, page, state);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const redraw = (page, state) => {
|
|
3793
|
+
const el = state.elements.get(event.layer);
|
|
3794
|
+
if (el) draw(event.layer, el, page, state.transform);
|
|
3795
|
+
};
|
|
3796
|
+
if (event.page === void 0) for (const [page, state] of pages) redraw(page, state);
|
|
3797
|
+
else {
|
|
3798
|
+
const state = pages.get(event.page);
|
|
3799
|
+
if (state) redraw(event.page, state);
|
|
3800
|
+
}
|
|
3801
|
+
})
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
//#endregion
|
|
3805
|
+
//#region src/dom/plugins/host.ts
|
|
3806
|
+
/** What `ctx.search` is when the host has no search: closed, and every action does nothing. */
|
|
3807
|
+
const inertSearch = {
|
|
3808
|
+
state: readonly(writable({
|
|
3809
|
+
open: false,
|
|
3810
|
+
query: "",
|
|
3811
|
+
matchCase: false,
|
|
3812
|
+
hits: [],
|
|
3813
|
+
current: -1,
|
|
3814
|
+
scanning: false
|
|
3815
|
+
})),
|
|
3816
|
+
open() {},
|
|
3817
|
+
setQuery() {},
|
|
3818
|
+
setMatchCase() {},
|
|
3819
|
+
next() {},
|
|
3820
|
+
previous() {},
|
|
3821
|
+
close() {}
|
|
3822
|
+
};
|
|
3823
|
+
/**
|
|
3824
|
+
* `value` seen with `extra` laid over it, read-only. Everything else is read from and written to
|
|
3825
|
+
* `value` itself, and its methods are bound to it, so a method always runs on the original
|
|
3826
|
+
* (a class instance keeps its `#private` fields, and `this.id` is the plugin's own id).
|
|
3827
|
+
*/
|
|
3828
|
+
function withProps(value, extra) {
|
|
3829
|
+
const bound = /* @__PURE__ */ new Map();
|
|
3830
|
+
const own = (key) => Object.hasOwn(extra, key);
|
|
3831
|
+
return new Proxy(Object.create(null), {
|
|
3832
|
+
get(_, key) {
|
|
3833
|
+
if (own(key)) return extra[key];
|
|
3834
|
+
const v = Reflect.get(value, key, value);
|
|
3835
|
+
if (typeof v !== "function") return v;
|
|
3836
|
+
let entry = bound.get(key);
|
|
3837
|
+
if (entry?.fn !== v) {
|
|
3838
|
+
entry = {
|
|
3839
|
+
fn: v,
|
|
3840
|
+
bound: v.bind(value)
|
|
3841
|
+
};
|
|
3842
|
+
bound.set(key, entry);
|
|
3843
|
+
}
|
|
3844
|
+
return entry.bound;
|
|
3845
|
+
},
|
|
3846
|
+
set: (_, key, v) => !own(key) && Reflect.set(value, key, v, value),
|
|
3847
|
+
defineProperty(target, key, d) {
|
|
3848
|
+
if (own(key) || !Reflect.defineProperty(value, key, d)) return false;
|
|
3849
|
+
const defined = Reflect.getOwnPropertyDescriptor(value, key);
|
|
3850
|
+
if (defined && !defined.configurable) Reflect.defineProperty(target, key, defined);
|
|
3851
|
+
return true;
|
|
3852
|
+
},
|
|
3853
|
+
deleteProperty: (_, key) => !own(key) && Reflect.deleteProperty(value, key),
|
|
3854
|
+
has: (_, key) => own(key) || Reflect.has(value, key),
|
|
3855
|
+
ownKeys: () => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(value), ...Reflect.ownKeys(extra)])],
|
|
3856
|
+
getOwnPropertyDescriptor(target, key) {
|
|
3857
|
+
const d = Reflect.getOwnPropertyDescriptor(own(key) ? extra : value, key);
|
|
3858
|
+
const fixed = Reflect.getOwnPropertyDescriptor(target, key)?.configurable === false;
|
|
3859
|
+
return d && {
|
|
3860
|
+
...d,
|
|
3861
|
+
configurable: !fixed
|
|
3862
|
+
};
|
|
3863
|
+
},
|
|
3864
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(value)
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
/** Runs a plugin callback; a throw is logged so one plugin cannot break the others. */
|
|
3868
|
+
function guard(fn) {
|
|
3869
|
+
try {
|
|
3870
|
+
return fn();
|
|
3871
|
+
} catch (error) {
|
|
3872
|
+
console.error(error);
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
/**
|
|
3877
|
+
* @internal For bindings and `createViewer`, which place a viewer's plugins; plugin authors use
|
|
3878
|
+
* `definePlugin` and never call it.
|
|
3879
|
+
*/
|
|
3880
|
+
function createPluginHost(opts) {
|
|
3881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3882
|
+
for (const plugin of opts.plugins) {
|
|
3883
|
+
if (seen.has(plugin.id)) throw new InkError("DuplicatePlugin", `A plugin with id ${plugin.id} is registered twice`, { id: plugin.id });
|
|
3884
|
+
seen.add(plugin.id);
|
|
3885
|
+
}
|
|
3886
|
+
const registry = createCommandRegistry({ document: opts.document });
|
|
3887
|
+
const tools = [];
|
|
3888
|
+
const decorators = [];
|
|
3889
|
+
const layers = createLayerRegistry();
|
|
3890
|
+
const requests = [];
|
|
3891
|
+
const documentFns = [];
|
|
3892
|
+
const editFns = [];
|
|
3893
|
+
const setupCleanups = [];
|
|
3894
|
+
const redecorators = /* @__PURE__ */ new Set();
|
|
3895
|
+
let opened = null;
|
|
3896
|
+
let destroyed = false;
|
|
3897
|
+
/** Creates a requested store for the open document and has its history track it. */
|
|
3898
|
+
const openStore = (request, state, history) => {
|
|
3899
|
+
const store = createRecordStore(request.key, {
|
|
3900
|
+
initial: request.opts.initial?.(state.doc) ?? [],
|
|
3901
|
+
...request.opts.onChange && { onChange: request.opts.onChange }
|
|
3902
|
+
});
|
|
3903
|
+
state.stores.push(store);
|
|
3904
|
+
state.detach.push(history.track(store));
|
|
3905
|
+
request.current.set(store);
|
|
3906
|
+
};
|
|
3907
|
+
const runOnDocument = (fn, state) => {
|
|
3908
|
+
const cleanup = guard(() => fn(state.doc));
|
|
3909
|
+
if (typeof cleanup === "function") state.cleanups.push(cleanup);
|
|
3910
|
+
};
|
|
3911
|
+
const open = (doc) => {
|
|
3912
|
+
const history = opts.history();
|
|
3913
|
+
const state = {
|
|
3914
|
+
doc,
|
|
3915
|
+
stores: [],
|
|
3916
|
+
detach: [],
|
|
3917
|
+
cleanups: []
|
|
3918
|
+
};
|
|
3919
|
+
opened = state;
|
|
3920
|
+
for (const request of requests) openStore(request, state, history);
|
|
3921
|
+
for (const fn of editFns) state.detach.push(history.react(fn));
|
|
3922
|
+
for (const fn of documentFns) runOnDocument(fn, state);
|
|
3923
|
+
};
|
|
3924
|
+
const close = () => {
|
|
3925
|
+
const state = opened;
|
|
3926
|
+
if (!state) return;
|
|
3927
|
+
opened = null;
|
|
3928
|
+
for (const detach of state.detach) detach();
|
|
3929
|
+
for (const store of state.stores) store.destroy();
|
|
3930
|
+
for (const request of requests) request.current.set(null);
|
|
3931
|
+
for (const cleanup of state.cleanups.reverse()) guard(cleanup);
|
|
3932
|
+
};
|
|
3933
|
+
const contextFor = (plugin) => {
|
|
3934
|
+
const storeNames = /* @__PURE__ */ new Set();
|
|
3935
|
+
return {
|
|
3936
|
+
id: plugin.id,
|
|
3937
|
+
document: opts.document,
|
|
3938
|
+
history: { batch: (fn) => opts.history().batch(fn) },
|
|
3939
|
+
activated: opts.activated,
|
|
3940
|
+
setActivated: (id) => opts.setActivated(id),
|
|
3941
|
+
root: opts.root,
|
|
3942
|
+
commands: { add: (spec) => registry.add(plugin.id, spec) },
|
|
3943
|
+
tools: { add(tool) {
|
|
3944
|
+
const id = `${plugin.id}.${tool.id}`;
|
|
3945
|
+
if (tools.some((t) => t.id === id)) throw new InkError("DuplicateId", `A tool with id ${id} already exists`, { id });
|
|
3946
|
+
tools.push(withProps(tool, { id }));
|
|
3947
|
+
} },
|
|
3948
|
+
decorate(fn) {
|
|
3949
|
+
decorators.push(fn);
|
|
3950
|
+
},
|
|
3951
|
+
redecorate() {
|
|
3952
|
+
for (const listener of [...redecorators]) listener();
|
|
3953
|
+
},
|
|
3954
|
+
store(name, storeOpts = {}) {
|
|
3955
|
+
const key = `${plugin.id}.${name}`;
|
|
3956
|
+
if (storeNames.has(name)) throw new InkError("DuplicateId", `A store with key ${key} already exists`, { key });
|
|
3957
|
+
storeNames.add(name);
|
|
3958
|
+
const request = {
|
|
3959
|
+
key,
|
|
3960
|
+
opts: storeOpts,
|
|
3961
|
+
current: writable(null)
|
|
3962
|
+
};
|
|
3963
|
+
requests.push(request);
|
|
3964
|
+
if (opened) openStore(request, opened, opts.history());
|
|
3965
|
+
return readonly(request.current);
|
|
3966
|
+
},
|
|
3967
|
+
onDocument(fn) {
|
|
3968
|
+
documentFns.push(fn);
|
|
3969
|
+
if (opened) runOnDocument(fn, opened);
|
|
3970
|
+
},
|
|
3971
|
+
onAnnotationEdit(fn) {
|
|
3972
|
+
editFns.push(fn);
|
|
3973
|
+
if (opened) opened.detach.push(opts.history().react(fn));
|
|
3974
|
+
},
|
|
3975
|
+
layer(spec) {
|
|
3976
|
+
return layers.add(withProps(spec, { plugin: plugin.id }));
|
|
3977
|
+
},
|
|
3978
|
+
scrollTo(page, rect, scrollOpts) {
|
|
3979
|
+
opts.viewport?.()?.scrollToRect(page, rect, scrollOpts);
|
|
3980
|
+
},
|
|
3981
|
+
search: opts.search ?? inertSearch
|
|
3982
|
+
};
|
|
3983
|
+
};
|
|
3984
|
+
for (const plugin of opts.plugins) {
|
|
3985
|
+
let cleanup;
|
|
3986
|
+
try {
|
|
3987
|
+
cleanup = plugin.setup(contextFor(plugin));
|
|
3988
|
+
} catch (error) {
|
|
3989
|
+
for (const done of setupCleanups.reverse()) guard(done);
|
|
3990
|
+
throw error;
|
|
3991
|
+
}
|
|
3992
|
+
if (typeof cleanup === "function") setupCleanups.push(cleanup);
|
|
3993
|
+
}
|
|
3994
|
+
const current = opts.document.get();
|
|
3995
|
+
if (current) open(current);
|
|
3996
|
+
const stopDocument = opts.document.subscribe((doc) => {
|
|
3997
|
+
close();
|
|
3998
|
+
if (doc) open(doc);
|
|
3999
|
+
});
|
|
4000
|
+
return {
|
|
4001
|
+
commands: registry.commands,
|
|
4002
|
+
command: (id) => registry.commands.get(id),
|
|
4003
|
+
tools,
|
|
4004
|
+
decorate(annotation) {
|
|
4005
|
+
const all = [opts.decorate?.(), ...decorators];
|
|
4006
|
+
let merged;
|
|
4007
|
+
for (const fn of all) {
|
|
4008
|
+
const d = fn && guard(() => fn(annotation));
|
|
4009
|
+
if (!d) continue;
|
|
4010
|
+
merged ??= {};
|
|
4011
|
+
if (d.data) merged = {
|
|
4012
|
+
...merged,
|
|
4013
|
+
data: {
|
|
4014
|
+
...d.data,
|
|
4015
|
+
...merged.data
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
if (d.classes) merged = {
|
|
4019
|
+
...merged,
|
|
4020
|
+
classes: [...merged.classes ?? [], ...d.classes]
|
|
4021
|
+
};
|
|
4022
|
+
if (merged.description === void 0 && d.description !== void 0) merged = {
|
|
4023
|
+
...merged,
|
|
4024
|
+
description: d.description
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
return merged;
|
|
4028
|
+
},
|
|
4029
|
+
layers: layers.layers,
|
|
4030
|
+
watchLayers: (listener) => layers.watchLayers(listener),
|
|
4031
|
+
watchRedecorate(listener) {
|
|
4032
|
+
const run = () => listener();
|
|
4033
|
+
redecorators.add(run);
|
|
4034
|
+
return () => {
|
|
4035
|
+
redecorators.delete(run);
|
|
4036
|
+
};
|
|
4037
|
+
},
|
|
4038
|
+
destroy() {
|
|
4039
|
+
if (destroyed) return;
|
|
4040
|
+
destroyed = true;
|
|
4041
|
+
stopDocument();
|
|
4042
|
+
close();
|
|
4043
|
+
for (const cleanup of setupCleanups.reverse()) guard(cleanup);
|
|
4044
|
+
}
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
//#endregion
|
|
4048
|
+
//#region src/dom/page-mount.ts
|
|
4049
|
+
/**
|
|
4050
|
+
* Canvas, text layer, annotation overlay and any plugin layers for one page, stacked in that
|
|
4051
|
+
* order.
|
|
4052
|
+
*/
|
|
4053
|
+
function createPageMount(opts) {
|
|
4054
|
+
const { document, page, host, transform } = opts;
|
|
4055
|
+
const callbacks = (layer) => ({
|
|
4056
|
+
...opts.onError ? { onError: (error) => opts.onError?.(error, layer) } : {},
|
|
4057
|
+
...opts.onRendered ? { onRendered: () => opts.onRendered?.(layer) } : {}
|
|
4058
|
+
});
|
|
4059
|
+
const renderer = createPageRenderer({
|
|
4060
|
+
document,
|
|
4061
|
+
page,
|
|
4062
|
+
host,
|
|
4063
|
+
transform,
|
|
4064
|
+
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
4065
|
+
...callbacks("canvas")
|
|
4066
|
+
});
|
|
4067
|
+
const textLayer = createTextLayer({
|
|
4068
|
+
document,
|
|
4069
|
+
page,
|
|
4070
|
+
host,
|
|
4071
|
+
transform,
|
|
4072
|
+
...callbacks("text")
|
|
4073
|
+
});
|
|
4074
|
+
const marks = opts.highlights?.mount(page, host, transform);
|
|
4075
|
+
const overlay = createOverlay({
|
|
4076
|
+
host,
|
|
4077
|
+
page,
|
|
4078
|
+
store: opts.store,
|
|
4079
|
+
transform,
|
|
4080
|
+
document,
|
|
4081
|
+
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
4082
|
+
...opts.onActivate ? { onActivate: (id) => opts.onActivate?.(id) } : {},
|
|
4083
|
+
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {},
|
|
4084
|
+
...opts.decorate ? { decorate: opts.decorate } : {}
|
|
4085
|
+
});
|
|
4086
|
+
const layers = opts.layers?.mount(page, host, transform);
|
|
4087
|
+
return {
|
|
4088
|
+
renderer,
|
|
4089
|
+
textLayer,
|
|
4090
|
+
overlay,
|
|
4091
|
+
update(next) {
|
|
4092
|
+
renderer.update(next);
|
|
4093
|
+
textLayer.update(next);
|
|
4094
|
+
overlay.update(next);
|
|
4095
|
+
layers?.update(next);
|
|
4096
|
+
marks?.update(next);
|
|
4097
|
+
},
|
|
4098
|
+
destroy() {
|
|
4099
|
+
layers?.destroy();
|
|
4100
|
+
overlay.destroy();
|
|
4101
|
+
marks?.destroy();
|
|
4102
|
+
textLayer.destroy();
|
|
4103
|
+
renderer.destroy();
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
//#endregion
|
|
4108
|
+
//#region src/dom/tools/options.ts
|
|
4109
|
+
function createToolOptions(tools = []) {
|
|
4110
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4111
|
+
const views = /* @__PURE__ */ new Map();
|
|
4112
|
+
const entry = (id) => {
|
|
4113
|
+
const w = byId.get(id);
|
|
4114
|
+
if (!w) throw new RangeError(`Unknown tool ${id}`);
|
|
4115
|
+
return w;
|
|
4116
|
+
};
|
|
4117
|
+
function register(list) {
|
|
4118
|
+
for (const t of list) {
|
|
4119
|
+
if (byId.has(t.id)) continue;
|
|
4120
|
+
const w = writable({ ...t.defaultOptions ?? {} });
|
|
4121
|
+
byId.set(t.id, w);
|
|
4122
|
+
views.set(t.id, readonly(w));
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
register(tools);
|
|
4126
|
+
return {
|
|
4127
|
+
readable(id) {
|
|
4128
|
+
entry(id);
|
|
4129
|
+
return views.get(id);
|
|
4130
|
+
},
|
|
4131
|
+
get: (id) => entry(id).get(),
|
|
4132
|
+
merge(id, patch) {
|
|
4133
|
+
const w = entry(id);
|
|
4134
|
+
w.set({
|
|
4135
|
+
...w.get(),
|
|
4136
|
+
...patch
|
|
4137
|
+
});
|
|
4138
|
+
},
|
|
4139
|
+
register
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
//#endregion
|
|
4143
|
+
//#region src/dom/tools/selection-edit.ts
|
|
4144
|
+
const CORNERS = [
|
|
4145
|
+
"nw",
|
|
4146
|
+
"ne",
|
|
4147
|
+
"sw",
|
|
4148
|
+
"se"
|
|
4149
|
+
];
|
|
4150
|
+
const ENDS = ["start", "end"];
|
|
4151
|
+
const CALLOUT = "callout";
|
|
4152
|
+
/** Pointer travel, CSS px, before a press on an annotation becomes a move. */
|
|
4153
|
+
const DRAG_THRESHOLD = 3;
|
|
4154
|
+
function createSelectionEdit(ctx, opts = {}) {
|
|
4155
|
+
let mode = { kind: "idle" };
|
|
4156
|
+
const batch = (fn) => ctx.history ? ctx.history.batch(fn) : fn();
|
|
4157
|
+
const reset = () => {
|
|
4158
|
+
if (mode.kind === "marquee") mode.el.remove();
|
|
4159
|
+
if (mode.kind === "move") {
|
|
4160
|
+
for (const el of mode.els) el.style.transform = "";
|
|
4161
|
+
ctx.root.removeAttribute("data-ink-dragging");
|
|
4162
|
+
}
|
|
4163
|
+
if (mode.kind === "resize" && mode.el) {
|
|
4164
|
+
mode.el.style.transform = "";
|
|
4165
|
+
mode.el.style.transformOrigin = "";
|
|
4166
|
+
mode.el.style.visibility = "";
|
|
4167
|
+
}
|
|
4168
|
+
if (mode.kind === "resize") mode.preview?.remove();
|
|
4169
|
+
if (mode.kind === "endpoint") {
|
|
4170
|
+
mode.preview.remove();
|
|
4171
|
+
if (mode.el) mode.el.style.visibility = "";
|
|
4172
|
+
}
|
|
4173
|
+
mode = { kind: "idle" };
|
|
4174
|
+
};
|
|
4175
|
+
const elementIn = (root, id) => root.querySelector(`[data-annotation="${CSS.escape(id)}"]`) ?? void 0;
|
|
4176
|
+
const previewFor = (host, a) => {
|
|
4177
|
+
const preview = host.ownerDocument.createElement("div");
|
|
4178
|
+
preview.className = "ink-annotation ink-edit-preview";
|
|
4179
|
+
preview.dataset.inkPreview = "";
|
|
4180
|
+
preview.dataset.type = a.type;
|
|
4181
|
+
preview.style.cssText = "position:absolute;pointer-events:none;";
|
|
4182
|
+
return preview;
|
|
4183
|
+
};
|
|
4184
|
+
/** Shows `draft` in `preview` beside `el`, which is hidden meanwhile. */
|
|
4185
|
+
const showPreview = (preview, el, draft, start) => {
|
|
4186
|
+
drawPreview(preview, draft, start);
|
|
4187
|
+
if (el) el.style.visibility = "hidden";
|
|
4188
|
+
if (!preview.isConnected) (el?.parentElement ?? start.host).append(preview);
|
|
4189
|
+
};
|
|
4190
|
+
function startHandle(e, hit, a, handle) {
|
|
4191
|
+
const el = elementIn(hit.host, a.id);
|
|
4192
|
+
if (CORNERS.includes(handle) && hasCornerHandles(a)) {
|
|
4193
|
+
const text = a.type === "freetext";
|
|
4194
|
+
mode = {
|
|
4195
|
+
kind: "resize",
|
|
4196
|
+
id: a.id,
|
|
4197
|
+
start: hit,
|
|
4198
|
+
corner: handle,
|
|
4199
|
+
box: text ? hit.transform.toCssRect(freeTextBox(a)) : handleBox(a, hit.transform),
|
|
4200
|
+
el,
|
|
4201
|
+
locked: keepsAspect(a),
|
|
4202
|
+
preview: text && (a.fringe !== void 0 || hasCallout(a)) ? previewFor(hit.host, a) : void 0
|
|
4203
|
+
};
|
|
2968
4204
|
} else if (ENDS.includes(handle) && a.type === "line" || handle === CALLOUT && hasCallout(a)) {
|
|
2969
4205
|
const preview = previewFor(hit.host, a);
|
|
2970
4206
|
mode = {
|
|
@@ -3611,6 +4847,8 @@ function onScrollBar(e, root) {
|
|
|
3611
4847
|
function inCustomContent(target) {
|
|
3612
4848
|
if (!(target instanceof Element)) return false;
|
|
3613
4849
|
if (target.closest("[data-ink-editor]")) return true;
|
|
4850
|
+
const layer = target.closest(".ink-plugin-layer");
|
|
4851
|
+
if (layer && layer !== target) return true;
|
|
3614
4852
|
const el = target.closest("[data-annotation]");
|
|
3615
4853
|
return el !== null && el !== target && el.hasAttribute("data-custom") && target.closest("[data-handle]") === null;
|
|
3616
4854
|
}
|
|
@@ -5364,284 +6602,154 @@ function handleRadioKeys(e, group) {
|
|
|
5364
6602
|
const next = radios[(at + (forward ? 1 : -1) + radios.length) % radios.length];
|
|
5365
6603
|
next.focus({ preventScroll: true });
|
|
5366
6604
|
next.click();
|
|
5367
|
-
}
|
|
5368
|
-
/**
|
|
5369
|
-
* Arrow keys, Home and End inside a `role="tablist"`: the next (or previous) enabled
|
|
5370
|
-
* `role="tab"` takes focus and is chosen, wrapping at the ends.
|
|
5371
|
-
*/
|
|
5372
|
-
function handleTabKeys(e, tablist) {
|
|
5373
|
-
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
5374
|
-
const tabs = [...tablist.querySelectorAll("[role=\"tab\"]:not(:disabled):not([aria-disabled=\"true\"])")];
|
|
5375
|
-
const at = tabs.indexOf(document.activeElement);
|
|
5376
|
-
if (at < 0) return;
|
|
5377
|
-
const last = tabs.length - 1;
|
|
5378
|
-
const next = e.key === "ArrowRight" || e.key === "ArrowDown" ? at === last ? 0 : at + 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? at === 0 ? last : at - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
|
|
5379
|
-
if (next < 0) return;
|
|
5380
|
-
e.preventDefault();
|
|
5381
|
-
tabs[next].focus({ preventScroll: true });
|
|
5382
|
-
tabs[next].click();
|
|
5383
|
-
}
|
|
5384
|
-
const TABBABLE
|
|
5385
|
-
"a[href]",
|
|
5386
|
-
"button:not(:disabled)",
|
|
5387
|
-
"input:not(:disabled):not([type=\"hidden\"])",
|
|
5388
|
-
"select:not(:disabled)",
|
|
5389
|
-
"textarea:not(:disabled)",
|
|
5390
|
-
"[tabindex]"
|
|
5391
|
-
].map((s) => `${s}:not([tabindex="-1"])`).join(", ");
|
|
5392
|
-
/**
|
|
5393
|
-
* Keeps Tab inside `container`, a modal: Tab from its last stop goes to its first, and
|
|
5394
|
-
* Shift+Tab from the first to the last.
|
|
5395
|
-
*/
|
|
5396
|
-
function trapTab(e, container) {
|
|
5397
|
-
if (e.key !== "Tab" || e.defaultPrevented) return;
|
|
5398
|
-
const stops = [...container.querySelectorAll(TABBABLE
|
|
5399
|
-
if (stops.length === 0) {
|
|
5400
|
-
e.preventDefault();
|
|
5401
|
-
return;
|
|
5402
|
-
}
|
|
5403
|
-
const first = stops[0];
|
|
5404
|
-
const last = stops[stops.length - 1];
|
|
5405
|
-
const active = document.activeElement;
|
|
5406
|
-
if (e.shiftKey && (active === first || !container.contains(active))) {
|
|
5407
|
-
e.preventDefault();
|
|
5408
|
-
last.focus();
|
|
5409
|
-
} else if (!e.shiftKey && (active === last || !container.contains(active))) {
|
|
5410
|
-
e.preventDefault();
|
|
5411
|
-
first.focus();
|
|
5412
|
-
}
|
|
5413
|
-
}
|
|
5414
|
-
//#endregion
|
|
5415
|
-
//#region src/dom/annotation-element.ts
|
|
5416
|
-
/** The overlay element drawn for annotation `id` under `root`, if it is in the DOM. */
|
|
5417
|
-
function annotationElementFor(root, id) {
|
|
5418
|
-
return root.querySelector(`[data-annotation="${CSS.escape(id)}"]`);
|
|
5419
|
-
}
|
|
5420
|
-
/**
|
|
5421
|
-
* Resolves the overlay element for `id` and keeps resolving while `root`'s subtree changes:
|
|
5422
|
-
* the overlay creates and removes these elements outside any framework (a page mounts as it
|
|
5423
|
-
* scrolls into view), so a popover anchored to one has to follow. Calls `onChange` once now
|
|
5424
|
-
* and again whenever the answer changes. The returned function stops observing.
|
|
5425
|
-
*/
|
|
5426
|
-
function observeAnnotationElement(root, id, onChange) {
|
|
5427
|
-
let last;
|
|
5428
|
-
const resolve = () => {
|
|
5429
|
-
const found = annotationElementFor(root, id);
|
|
5430
|
-
if (found === last) return;
|
|
5431
|
-
last = found;
|
|
5432
|
-
onChange(found);
|
|
5433
|
-
};
|
|
5434
|
-
resolve();
|
|
5435
|
-
const observer = new MutationObserver(resolve);
|
|
5436
|
-
observer.observe(root, {
|
|
5437
|
-
childList: true,
|
|
5438
|
-
subtree: true
|
|
5439
|
-
});
|
|
5440
|
-
return () => observer.disconnect();
|
|
5441
|
-
}
|
|
5442
|
-
//#endregion
|
|
5443
|
-
//#region src/dom/popover-focus.ts
|
|
5444
|
-
/**
|
|
5445
|
-
* A press that will activate an annotation is not a dismissal: the popover follows the
|
|
5446
|
-
* activation. Mirrors the overlay's rule: a press on the annotation element or on its drawn
|
|
5447
|
-
* shape activates; a press on a custom element's own content belongs to that content.
|
|
5448
|
-
*/
|
|
5449
|
-
function shouldKeepOpenOnAnnotation(target) {
|
|
5450
|
-
if (!(target instanceof Element)) return false;
|
|
5451
|
-
const annotation = target.closest("[data-annotation]");
|
|
5452
|
-
if (!annotation) return false;
|
|
5453
|
-
return target === annotation || !annotation.hasAttribute("data-custom");
|
|
5454
|
-
}
|
|
5455
|
-
function createFocusReturn(opts) {
|
|
5456
|
-
let fallbacks = [];
|
|
5457
|
-
/** Set by `beforeRemove`: the close comes from removing the anchor's annotation. */
|
|
5458
|
-
let removing = false;
|
|
5459
|
-
return {
|
|
5460
|
-
beforeRemove(anchor, prefer) {
|
|
5461
|
-
fallbacks = [];
|
|
5462
|
-
removing = true;
|
|
5463
|
-
const siblings = anchor?.parentElement ? [...anchor.parentElement.children].filter((el) => el instanceof HTMLElement && el.hasAttribute("data-annotation")) : [];
|
|
5464
|
-
const i = anchor ? siblings.indexOf(anchor) : -1;
|
|
5465
|
-
if (i >= 0) {
|
|
5466
|
-
const after = siblings.slice(i + 1);
|
|
5467
|
-
const preferred = prefer ? after.filter((el) => el.matches(prefer)) : [];
|
|
5468
|
-
fallbacks.push(...preferred, ...after, ...siblings.slice(0, i).reverse());
|
|
5469
|
-
}
|
|
5470
|
-
},
|
|
5471
|
-
onCloseAutoFocus(event, anchor) {
|
|
5472
|
-
event.preventDefault();
|
|
5473
|
-
const plan = fallbacks;
|
|
5474
|
-
const removed = removing;
|
|
5475
|
-
fallbacks = [];
|
|
5476
|
-
removing = false;
|
|
5477
|
-
const root = opts.root();
|
|
5478
|
-
if (!root) return;
|
|
5479
|
-
const activated = opts.activated();
|
|
5480
|
-
if (activated !== null && annotationElementFor(root, activated)) return;
|
|
5481
|
-
const active = root.ownerDocument.activeElement;
|
|
5482
|
-
if (active && active !== root && root.contains(active)) return;
|
|
5483
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5484
|
-
(removed ? [...plan, scroll] : [
|
|
5485
|
-
anchor,
|
|
5486
|
-
...plan,
|
|
5487
|
-
scroll
|
|
5488
|
-
]).find((el) => el?.isConnected)?.focus({ preventScroll: true });
|
|
5489
|
-
}
|
|
5490
|
-
};
|
|
5491
|
-
}
|
|
5492
|
-
//#endregion
|
|
5493
|
-
//#region src/dom/viewer/dom.ts
|
|
5494
|
-
/** `document.createElement` with attributes and children. */
|
|
5495
|
-
function h(tag, attrs = {}, ...children) {
|
|
5496
|
-
const el = document.createElement(tag);
|
|
5497
|
-
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
5498
|
-
el.append(...children);
|
|
5499
|
-
return el;
|
|
5500
|
-
}
|
|
5501
|
-
function button$1(label, onClick) {
|
|
5502
|
-
const b = h("button", { type: "button" }, label);
|
|
5503
|
-
b.addEventListener("click", onClick);
|
|
5504
|
-
return b;
|
|
5505
|
-
}
|
|
5506
|
-
//#endregion
|
|
5507
|
-
//#region src/dom/viewer/popover.ts
|
|
5508
|
-
/** Space between the anchor and the panel, and between the panel and the window edge. */
|
|
5509
|
-
const GAP = 6;
|
|
5510
|
-
const MARGIN = 8;
|
|
5511
|
-
const TABBABLE = "button, input, select, textarea, a[href], [tabindex]";
|
|
5512
|
-
/**
|
|
5513
|
-
* Positions `panel` (fixed) below the `anchor` rectangle, or above it when only that fits,
|
|
5514
|
-
* kept inside the window. For panels anchored to something other than an annotation.
|
|
5515
|
-
*/
|
|
5516
|
-
function placePanel(panel, anchor) {
|
|
5517
|
-
const { innerWidth: vw, innerHeight: vh } = window;
|
|
5518
|
-
panel.style.position = "fixed";
|
|
5519
|
-
panel.style.left = "0px";
|
|
5520
|
-
panel.style.top = "0px";
|
|
5521
|
-
const w = panel.offsetWidth;
|
|
5522
|
-
const ht = panel.offsetHeight;
|
|
5523
|
-
let top = anchor.bottom + GAP;
|
|
5524
|
-
if (top + ht > vh - MARGIN && anchor.top - GAP - ht >= MARGIN) top = anchor.top - GAP - ht;
|
|
5525
|
-
top = Math.max(MARGIN, Math.min(top, vh - ht - MARGIN));
|
|
5526
|
-
const left = Math.max(MARGIN, Math.min(anchor.left, vw - w - MARGIN));
|
|
5527
|
-
panel.style.left = `${left}px`;
|
|
5528
|
-
panel.style.top = `${top}px`;
|
|
5529
|
-
}
|
|
5530
|
-
/**
|
|
5531
|
-
* Creates a non-modal `role="dialog"` panel on `<body>`, marked `data-ink-popover` so the theme
|
|
5532
|
-
* gives it its tokens, that an app anchors to an annotation with `show(id)` (for instance on
|
|
5533
|
-
* `viewer.activated`). The panel follows the annotation's element as pages mount and unmount:
|
|
5534
|
-
* it hides while the element is gone and comes back with it. Tab loops inside the panel, and
|
|
5535
|
-
* Escape or a press outside calls `onDismiss`. Call `destroy` when done.
|
|
5536
|
-
*/
|
|
5537
|
-
function createPopover(opts) {
|
|
5538
|
-
const { root, focusReturn } = opts;
|
|
5539
|
-
const panel = h("div", {
|
|
5540
|
-
...opts.attrs,
|
|
5541
|
-
role: "dialog",
|
|
5542
|
-
"data-ink-popover": "",
|
|
5543
|
-
hidden: ""
|
|
5544
|
-
});
|
|
5545
|
-
document.body.append(panel);
|
|
5546
|
-
let anchoredId = null;
|
|
5547
|
-
let anchor = null;
|
|
5548
|
-
let stopObserving = () => void 0;
|
|
5549
|
-
/** The element that had focus in the panel when the anchor went away. */
|
|
5550
|
-
let stranded = null;
|
|
5551
|
-
const place = () => {
|
|
5552
|
-
if (!anchor || panel.hidden) return;
|
|
5553
|
-
placePanel(panel, anchor.getBoundingClientRect());
|
|
5554
|
-
};
|
|
5555
|
-
const resizes = new ResizeObserver(place);
|
|
5556
|
-
resizes.observe(panel);
|
|
5557
|
-
const onAnchor = (el, closing = false) => {
|
|
5558
|
-
if (anchor) resizes.unobserve(anchor);
|
|
5559
|
-
anchor = el;
|
|
5560
|
-
if (!el) {
|
|
5561
|
-
const active = document.activeElement;
|
|
5562
|
-
if (!closing && !panel.hidden && active instanceof HTMLElement && panel.contains(active)) {
|
|
5563
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5564
|
-
if (scroll) {
|
|
5565
|
-
stranded = active;
|
|
5566
|
-
scroll.focus({ preventScroll: true });
|
|
5567
|
-
}
|
|
5568
|
-
}
|
|
5569
|
-
panel.hidden = true;
|
|
5570
|
-
return;
|
|
5571
|
-
}
|
|
5572
|
-
resizes.observe(el);
|
|
5573
|
-
const wasHidden = panel.hidden;
|
|
5574
|
-
panel.hidden = false;
|
|
5575
|
-
place();
|
|
5576
|
-
if (wasHidden && stranded) {
|
|
5577
|
-
const back = stranded;
|
|
5578
|
-
stranded = null;
|
|
5579
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5580
|
-
if (panel.contains(back) && document.activeElement === scroll) back.focus({ preventScroll: true });
|
|
5581
|
-
}
|
|
5582
|
-
};
|
|
5583
|
-
const onKey = (e) => {
|
|
5584
|
-
if (e.key !== "Escape" || panel.hidden || e.defaultPrevented) return;
|
|
5585
|
-
const target = e.target;
|
|
5586
|
-
if (!(target instanceof Node) || !(panel.contains(target) || root.contains(target))) return;
|
|
6605
|
+
}
|
|
6606
|
+
/**
|
|
6607
|
+
* Arrow keys, Home and End inside a `role="tablist"`: the next (or previous) enabled
|
|
6608
|
+
* `role="tab"` takes focus and is chosen, wrapping at the ends.
|
|
6609
|
+
*/
|
|
6610
|
+
function handleTabKeys(e, tablist) {
|
|
6611
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
6612
|
+
const tabs = [...tablist.querySelectorAll("[role=\"tab\"]:not(:disabled):not([aria-disabled=\"true\"])")];
|
|
6613
|
+
const at = tabs.indexOf(document.activeElement);
|
|
6614
|
+
if (at < 0) return;
|
|
6615
|
+
const last = tabs.length - 1;
|
|
6616
|
+
const next = e.key === "ArrowRight" || e.key === "ArrowDown" ? at === last ? 0 : at + 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? at === 0 ? last : at - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
|
|
6617
|
+
if (next < 0) return;
|
|
6618
|
+
e.preventDefault();
|
|
6619
|
+
tabs[next].focus({ preventScroll: true });
|
|
6620
|
+
tabs[next].click();
|
|
6621
|
+
}
|
|
6622
|
+
const TABBABLE = [
|
|
6623
|
+
"a[href]",
|
|
6624
|
+
"button:not(:disabled)",
|
|
6625
|
+
"input:not(:disabled):not([type=\"hidden\"])",
|
|
6626
|
+
"select:not(:disabled)",
|
|
6627
|
+
"textarea:not(:disabled)",
|
|
6628
|
+
"[tabindex]"
|
|
6629
|
+
].map((s) => `${s}:not([tabindex="-1"])`).join(", ");
|
|
6630
|
+
/**
|
|
6631
|
+
* Keeps Tab inside `container`, a modal: Tab from its last stop goes to its first, and
|
|
6632
|
+
* Shift+Tab from the first to the last.
|
|
6633
|
+
*/
|
|
6634
|
+
function trapTab(e, container) {
|
|
6635
|
+
if (e.key !== "Tab" || e.defaultPrevented) return;
|
|
6636
|
+
const stops = [...container.querySelectorAll(TABBABLE)].filter((el) => !el.matches(":disabled") && el.getClientRects().length > 0);
|
|
6637
|
+
if (stops.length === 0) {
|
|
5587
6638
|
e.preventDefault();
|
|
5588
|
-
|
|
5589
|
-
}
|
|
5590
|
-
const
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
};
|
|
5595
|
-
const onTab = (e) => {
|
|
5596
|
-
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
5597
|
-
const tabbable = [...panel.querySelectorAll(TABBABLE)].filter((el) => !el.disabled && el.tabIndex >= 0);
|
|
5598
|
-
const first = tabbable[0];
|
|
5599
|
-
const last = tabbable.at(-1);
|
|
5600
|
-
if (!first || !last) return;
|
|
5601
|
-
const active = document.activeElement;
|
|
5602
|
-
if (e.shiftKey ? active !== first : active !== last) return;
|
|
6639
|
+
return;
|
|
6640
|
+
}
|
|
6641
|
+
const first = stops[0];
|
|
6642
|
+
const last = stops[stops.length - 1];
|
|
6643
|
+
const active = document.activeElement;
|
|
6644
|
+
if (e.shiftKey && (active === first || !container.contains(active))) {
|
|
5603
6645
|
e.preventDefault();
|
|
5604
|
-
|
|
6646
|
+
last.focus();
|
|
6647
|
+
} else if (!e.shiftKey && (active === last || !container.contains(active))) {
|
|
6648
|
+
e.preventDefault();
|
|
6649
|
+
first.focus();
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
//#endregion
|
|
6653
|
+
//#region src/dom/search/highlights.ts
|
|
6654
|
+
const NO_HITS = [];
|
|
6655
|
+
/** One box over the quad: its CSS bounding box, clipped to the quad when it is not upright. */
|
|
6656
|
+
function hitBox(doc, t, quad) {
|
|
6657
|
+
const { p1, p2, p3, p4 } = t.toCssQuad(quad);
|
|
6658
|
+
const xs = [
|
|
6659
|
+
p1.x,
|
|
6660
|
+
p2.x,
|
|
6661
|
+
p3.x,
|
|
6662
|
+
p4.x
|
|
6663
|
+
];
|
|
6664
|
+
const ys = [
|
|
6665
|
+
p1.y,
|
|
6666
|
+
p2.y,
|
|
6667
|
+
p3.y,
|
|
6668
|
+
p4.y
|
|
6669
|
+
];
|
|
6670
|
+
const left = Math.min(...xs);
|
|
6671
|
+
const top = Math.min(...ys);
|
|
6672
|
+
const width = Math.max(...xs) - left;
|
|
6673
|
+
const height = Math.max(...ys) - top;
|
|
6674
|
+
const el = doc.createElement("div");
|
|
6675
|
+
el.dataset.searchHit = "";
|
|
6676
|
+
let css = `position:absolute;left:${left}px;top:${top}px;width:${width}px;height:${height}px;`;
|
|
6677
|
+
if (!(Math.abs(p1.y - p2.y) < .01 && Math.abs(p3.y - p4.y) < .01 || Math.abs(p1.x - p2.x) < .01 && Math.abs(p3.x - p4.x) < .01) && width > 0 && height > 0) {
|
|
6678
|
+
const pt = (p) => `${((p.x - left) / width * 100).toFixed(3)}% ${((p.y - top) / height * 100).toFixed(3)}%`;
|
|
6679
|
+
css += `clip-path:polygon(${[
|
|
6680
|
+
p1,
|
|
6681
|
+
p2,
|
|
6682
|
+
p4,
|
|
6683
|
+
p3
|
|
6684
|
+
].map(pt).join(",")});`;
|
|
6685
|
+
}
|
|
6686
|
+
el.style.cssText = css;
|
|
6687
|
+
return el;
|
|
6688
|
+
}
|
|
6689
|
+
function createSearchHighlights(search) {
|
|
6690
|
+
const pages = /* @__PURE__ */ new Map();
|
|
6691
|
+
const sameHits = (a, b) => a.length === b.length && a.every((hit, i) => hit === b[i]);
|
|
6692
|
+
const draw = (page, force, hits) => {
|
|
6693
|
+
const { current: index, hits: all } = search.state.get();
|
|
6694
|
+
const current = all[index];
|
|
6695
|
+
const shown = current && hits.includes(current) ? current : void 0;
|
|
6696
|
+
if (!force && sameHits(page.hits, hits) && page.current === shown) return;
|
|
6697
|
+
page.hits = hits;
|
|
6698
|
+
page.current = shown;
|
|
6699
|
+
const doc = page.el.ownerDocument;
|
|
6700
|
+
page.el.replaceChildren(...hits.flatMap((hit) => hit.quads.map((quad) => {
|
|
6701
|
+
const box = hitBox(doc, page.transform, quad);
|
|
6702
|
+
if (hit === shown) box.dataset.current = "";
|
|
6703
|
+
return box;
|
|
6704
|
+
})));
|
|
6705
|
+
};
|
|
6706
|
+
/** The hits on `page`: a run of the state's hits, which are in `(page, start)` order. */
|
|
6707
|
+
const hitsOn = (all, page) => {
|
|
6708
|
+
const first = (p) => {
|
|
6709
|
+
let low = 0;
|
|
6710
|
+
let high = all.length;
|
|
6711
|
+
while (low < high) {
|
|
6712
|
+
const mid = low + high >>> 1;
|
|
6713
|
+
if (all[mid].page < p) low = mid + 1;
|
|
6714
|
+
else high = mid;
|
|
6715
|
+
}
|
|
6716
|
+
return low;
|
|
6717
|
+
};
|
|
6718
|
+
const from = first(page);
|
|
6719
|
+
const to = first(page + 1);
|
|
6720
|
+
return to > from ? all.slice(from, to) : NO_HITS;
|
|
5605
6721
|
};
|
|
5606
|
-
panel.addEventListener("keydown", onTab);
|
|
5607
|
-
document.addEventListener("keydown", onKey);
|
|
5608
|
-
document.addEventListener("pointerdown", onPress, true);
|
|
5609
|
-
document.addEventListener("scroll", place, true);
|
|
5610
|
-
window.addEventListener("resize", place);
|
|
5611
6722
|
return {
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
6723
|
+
mount(index, host, transform) {
|
|
6724
|
+
const el = host.ownerDocument.createElement("div");
|
|
6725
|
+
el.className = "ink-search-layer";
|
|
6726
|
+
el.dataset.inkSearchLayer = "";
|
|
6727
|
+
el.setAttribute("aria-hidden", "true");
|
|
6728
|
+
el.style.cssText = "position:absolute;inset:0;pointer-events:none;";
|
|
6729
|
+
const overlay = host.querySelector(":scope > .ink-overlay");
|
|
6730
|
+
host.insertBefore(el, overlay);
|
|
6731
|
+
const page = {
|
|
6732
|
+
el,
|
|
6733
|
+
transform,
|
|
6734
|
+
hits: NO_HITS,
|
|
6735
|
+
current: void 0
|
|
6736
|
+
};
|
|
6737
|
+
pages.set(index, page);
|
|
6738
|
+
draw(page, true, hitsOn(search.state.get().hits, index));
|
|
6739
|
+
return {
|
|
6740
|
+
update(next) {
|
|
6741
|
+
page.transform = next;
|
|
6742
|
+
draw(page, true, page.hits);
|
|
6743
|
+
},
|
|
6744
|
+
destroy() {
|
|
6745
|
+
if (pages.get(index) === page) pages.delete(index);
|
|
6746
|
+
el.remove();
|
|
6747
|
+
}
|
|
6748
|
+
};
|
|
5634
6749
|
},
|
|
5635
|
-
destroy() {
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
document.removeEventListener("keydown", onKey);
|
|
5639
|
-
document.removeEventListener("pointerdown", onPress, true);
|
|
5640
|
-
panel.removeEventListener("keydown", onTab);
|
|
5641
|
-
document.removeEventListener("scroll", place, true);
|
|
5642
|
-
window.removeEventListener("resize", place);
|
|
5643
|
-
panel.remove();
|
|
5644
|
-
}
|
|
6750
|
+
destroy: search.state.subscribe(({ hits }) => {
|
|
6751
|
+
for (const [index, page] of pages) draw(page, false, hitsOn(hits, index));
|
|
6752
|
+
})
|
|
5645
6753
|
};
|
|
5646
6754
|
}
|
|
5647
6755
|
//#endregion
|
|
@@ -5657,7 +6765,17 @@ function mountViewer(opts) {
|
|
|
5657
6765
|
let stopKeys = () => void 0;
|
|
5658
6766
|
let stopActive = () => void 0;
|
|
5659
6767
|
let destroyed = false;
|
|
5660
|
-
const
|
|
6768
|
+
const plugins = opts.plugins;
|
|
6769
|
+
const hostTools = [...opts.tools ?? [], ...plugins?.tools ?? []];
|
|
6770
|
+
/** Plugin layers follow the host for the engine's life, across rebuilds. */
|
|
6771
|
+
const layers = plugins && createPluginLayers(plugins);
|
|
6772
|
+
/** Match highlights follow the controller for the engine's life, across rebuilds. */
|
|
6773
|
+
const highlights = opts.search && createSearchHighlights(opts.search);
|
|
6774
|
+
const decorate = plugins ? (a) => plugins.decorate(a) : (a) => opts.decorateAnnotation?.(a);
|
|
6775
|
+
/** A plugin's `ctx.redecorate()` reaches every page this engine has mounted. */
|
|
6776
|
+
const stopRedecorate = plugins?.watchRedecorate(() => {
|
|
6777
|
+
for (const mount of pages.values()) mount.overlay.redecorate();
|
|
6778
|
+
});
|
|
5661
6779
|
/** Option values outlive rebuilds: every controller shares this registry. */
|
|
5662
6780
|
const toolOptions = opts.toolOptions ?? createToolOptions();
|
|
5663
6781
|
/** Without a host style, the engine's own follows the active tool across rebuilds. */
|
|
@@ -5686,7 +6804,9 @@ function mountViewer(opts) {
|
|
|
5686
6804
|
store: doc.annotations,
|
|
5687
6805
|
onActivate: (id) => opts.onActivate?.(id),
|
|
5688
6806
|
isCustom: () => isCustom,
|
|
5689
|
-
decorate
|
|
6807
|
+
decorate,
|
|
6808
|
+
...layers ? { layers } : {},
|
|
6809
|
+
...highlights ? { highlights } : {},
|
|
5690
6810
|
onError: (cause, layer) => {
|
|
5691
6811
|
failed.add(layer);
|
|
5692
6812
|
if (error) return;
|
|
@@ -5871,9 +6991,292 @@ function mountViewer(opts) {
|
|
|
5871
6991
|
if (destroyed) return;
|
|
5872
6992
|
destroyed = true;
|
|
5873
6993
|
teardown();
|
|
6994
|
+
layers?.destroy();
|
|
6995
|
+
highlights?.destroy();
|
|
6996
|
+
stopRedecorate?.();
|
|
6997
|
+
}
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
7000
|
+
//#endregion
|
|
7001
|
+
//#region src/dom/search/controller.ts
|
|
7002
|
+
function compare(a, b) {
|
|
7003
|
+
return a.page - b.page || a.start - b.start;
|
|
7004
|
+
}
|
|
7005
|
+
/** Where `hit` goes in `hits`, which is sorted by `(page, start)`. */
|
|
7006
|
+
function insertionIndex(hits, hit) {
|
|
7007
|
+
let low = 0;
|
|
7008
|
+
let high = hits.length;
|
|
7009
|
+
while (low < high) {
|
|
7010
|
+
const mid = low + high >>> 1;
|
|
7011
|
+
if (compare(hits[mid], hit) <= 0) low = mid + 1;
|
|
7012
|
+
else high = mid;
|
|
7013
|
+
}
|
|
7014
|
+
return low;
|
|
7015
|
+
}
|
|
7016
|
+
function createSearchController(opts) {
|
|
7017
|
+
const debounceMs = opts.debounceMs ?? 150;
|
|
7018
|
+
const state = writable({
|
|
7019
|
+
open: false,
|
|
7020
|
+
query: "",
|
|
7021
|
+
matchCase: false,
|
|
7022
|
+
hits: [],
|
|
7023
|
+
current: -1,
|
|
7024
|
+
scanning: false
|
|
7025
|
+
});
|
|
7026
|
+
const bars = writable(0);
|
|
7027
|
+
const patch = (changes) => state.update((s) => ({
|
|
7028
|
+
...s,
|
|
7029
|
+
...changes
|
|
7030
|
+
}));
|
|
7031
|
+
let timer;
|
|
7032
|
+
let scan;
|
|
7033
|
+
/** The current hit and its index in `hits`, kept in step as pages of hits arrive. */
|
|
7034
|
+
let currentHit;
|
|
7035
|
+
let currentIndex = -1;
|
|
7036
|
+
/** Hits of one page not yet in the state, in page order, and the task that adds them. */
|
|
7037
|
+
let pending = [];
|
|
7038
|
+
let flushTimer;
|
|
7039
|
+
let destroyed = false;
|
|
7040
|
+
function scrollTo(hit) {
|
|
7041
|
+
opts.viewport()?.scrollToRect(hit.page, hit.rect);
|
|
7042
|
+
}
|
|
7043
|
+
/** Stops the pending debounce and the running scan, and empties the hits. */
|
|
7044
|
+
function reset() {
|
|
7045
|
+
clearTimeout(timer);
|
|
7046
|
+
timer = void 0;
|
|
7047
|
+
scan?.abort();
|
|
7048
|
+
scan = void 0;
|
|
7049
|
+
currentHit = void 0;
|
|
7050
|
+
currentIndex = -1;
|
|
7051
|
+
pending = [];
|
|
7052
|
+
clearTimeout(flushTimer);
|
|
7053
|
+
flushTimer = void 0;
|
|
7054
|
+
patch({
|
|
7055
|
+
hits: [],
|
|
7056
|
+
current: -1,
|
|
7057
|
+
scanning: false
|
|
7058
|
+
});
|
|
7059
|
+
}
|
|
7060
|
+
/**
|
|
7061
|
+
* Adds the pending page of hits to the state in one change. A page's hits sit together in
|
|
7062
|
+
* `(page, start)` order, so they go in with one splice, and `current` moves by their count
|
|
7063
|
+
* when they land before it. The first hit becomes current and is scrolled to, unless
|
|
7064
|
+
* `scroll` is false: a step flushes first and scrolls to where it lands instead.
|
|
7065
|
+
*/
|
|
7066
|
+
function flush(scroll = true) {
|
|
7067
|
+
clearTimeout(flushTimer);
|
|
7068
|
+
flushTimer = void 0;
|
|
7069
|
+
if (pending.length === 0) return;
|
|
7070
|
+
const batch = pending;
|
|
7071
|
+
pending = [];
|
|
7072
|
+
const first = batch[0];
|
|
7073
|
+
const { hits } = state.get();
|
|
7074
|
+
const at = insertionIndex(hits, first);
|
|
7075
|
+
const next = hits.slice(0, at).concat(batch, hits.slice(at));
|
|
7076
|
+
if (!currentHit) {
|
|
7077
|
+
currentHit = first;
|
|
7078
|
+
currentIndex = at;
|
|
7079
|
+
if (scroll) scrollTo(first);
|
|
7080
|
+
} else if (at <= currentIndex) currentIndex += batch.length;
|
|
7081
|
+
patch({
|
|
7082
|
+
hits: next,
|
|
7083
|
+
current: currentIndex
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
7086
|
+
/**
|
|
7087
|
+
* Holds `hit` until its page is done: the next page's first hit, the end of the scan, or the
|
|
7088
|
+
* next task, since a page's hits arrive together and the scan yields between pages.
|
|
7089
|
+
*/
|
|
7090
|
+
function accept(hit) {
|
|
7091
|
+
if (pending.length > 0 && pending[0].page !== hit.page) flush();
|
|
7092
|
+
pending.push(hit);
|
|
7093
|
+
flushTimer ??= setTimeout(() => flush());
|
|
7094
|
+
}
|
|
7095
|
+
async function run(doc, controller) {
|
|
7096
|
+
const { query, matchCase } = state.get();
|
|
7097
|
+
const count = doc.pages.length;
|
|
7098
|
+
const from = Math.min(Math.max(opts.viewport()?.currentPage.get() ?? 0, 0), Math.max(count - 1, 0));
|
|
7099
|
+
const pages = [...Array.from({ length: count - from }, (_, i) => from + i), ...Array.from({ length: from }, (_, i) => i)];
|
|
7100
|
+
try {
|
|
7101
|
+
for await (const hit of doc.search(query, {
|
|
7102
|
+
matchCase,
|
|
7103
|
+
pages,
|
|
7104
|
+
signal: controller.signal
|
|
7105
|
+
})) {
|
|
7106
|
+
if (controller.signal.aborted) return;
|
|
7107
|
+
accept(hit);
|
|
7108
|
+
}
|
|
7109
|
+
} catch (error) {
|
|
7110
|
+
if (!controller.signal.aborted) {
|
|
7111
|
+
console.error(error);
|
|
7112
|
+
if (scan === controller) scan = void 0;
|
|
7113
|
+
flush();
|
|
7114
|
+
patch({ scanning: false });
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
if (scan === controller) {
|
|
7119
|
+
scan = void 0;
|
|
7120
|
+
flush();
|
|
7121
|
+
patch({ scanning: false });
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
function start() {
|
|
7125
|
+
timer = void 0;
|
|
7126
|
+
scan?.abort();
|
|
7127
|
+
scan = void 0;
|
|
7128
|
+
const doc = opts.document.get();
|
|
7129
|
+
if (!doc || isBlankQuery(state.get().query)) {
|
|
7130
|
+
patch({ scanning: false });
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
const controller = new AbortController();
|
|
7134
|
+
scan = controller;
|
|
7135
|
+
patch({ scanning: true });
|
|
7136
|
+
run(doc, controller);
|
|
7137
|
+
}
|
|
7138
|
+
/** Empties the hits now and scans again, after the debounce when `delay` is set. */
|
|
7139
|
+
function restart(delay) {
|
|
7140
|
+
reset();
|
|
7141
|
+
if (isBlankQuery(state.get().query)) return;
|
|
7142
|
+
patch({ scanning: true });
|
|
7143
|
+
if (delay) timer = setTimeout(start, debounceMs);
|
|
7144
|
+
else start();
|
|
7145
|
+
}
|
|
7146
|
+
function step(by) {
|
|
7147
|
+
const shown = currentHit !== void 0;
|
|
7148
|
+
flush(false);
|
|
7149
|
+
const { hits, current } = state.get();
|
|
7150
|
+
if (hits.length === 0) return;
|
|
7151
|
+
const index = shown ? (current + by + hits.length) % hits.length : current;
|
|
7152
|
+
const hit = hits[index];
|
|
7153
|
+
currentHit = hit;
|
|
7154
|
+
currentIndex = index;
|
|
7155
|
+
patch({ current: index });
|
|
7156
|
+
scrollTo(hit);
|
|
7157
|
+
}
|
|
7158
|
+
const stopDocument = opts.document.subscribe(() => {
|
|
7159
|
+
if (destroyed) return;
|
|
7160
|
+
reset();
|
|
7161
|
+
if (state.get().open) restart(false);
|
|
7162
|
+
});
|
|
7163
|
+
function setQuery(query) {
|
|
7164
|
+
patch({ query });
|
|
7165
|
+
restart(true);
|
|
7166
|
+
}
|
|
7167
|
+
function open(query) {
|
|
7168
|
+
const wasOpen = state.get().open;
|
|
7169
|
+
patch({ open: true });
|
|
7170
|
+
if (query !== void 0) setQuery(query);
|
|
7171
|
+
else if (!wasOpen && !isBlankQuery(state.get().query)) restart(false);
|
|
7172
|
+
}
|
|
7173
|
+
return {
|
|
7174
|
+
state: readonly(state),
|
|
7175
|
+
open,
|
|
7176
|
+
setQuery,
|
|
7177
|
+
setMatchCase(on) {
|
|
7178
|
+
patch({ matchCase: on });
|
|
7179
|
+
restart(false);
|
|
7180
|
+
},
|
|
7181
|
+
next: () => step(1),
|
|
7182
|
+
previous: () => step(-1),
|
|
7183
|
+
close() {
|
|
7184
|
+
reset();
|
|
7185
|
+
patch({ open: false });
|
|
7186
|
+
},
|
|
7187
|
+
attachBar() {
|
|
7188
|
+
let attached = true;
|
|
7189
|
+
bars.update((n) => n + 1);
|
|
7190
|
+
return () => {
|
|
7191
|
+
if (!attached) return;
|
|
7192
|
+
attached = false;
|
|
7193
|
+
bars.update((n) => n - 1);
|
|
7194
|
+
};
|
|
7195
|
+
},
|
|
7196
|
+
barAttached: {
|
|
7197
|
+
get: () => bars.get() > 0,
|
|
7198
|
+
subscribe(fn) {
|
|
7199
|
+
let last = bars.get() > 0;
|
|
7200
|
+
return bars.subscribe((n) => {
|
|
7201
|
+
if (n > 0 === last) return;
|
|
7202
|
+
last = n > 0;
|
|
7203
|
+
fn(last);
|
|
7204
|
+
});
|
|
7205
|
+
}
|
|
7206
|
+
},
|
|
7207
|
+
destroy() {
|
|
7208
|
+
destroyed = true;
|
|
7209
|
+
stopDocument();
|
|
7210
|
+
reset();
|
|
5874
7211
|
}
|
|
5875
7212
|
};
|
|
5876
7213
|
}
|
|
7214
|
+
/**
|
|
7215
|
+
* The actions and state of `controller` without the bar registration: what callers of a viewer
|
|
7216
|
+
* or a plugin get.
|
|
7217
|
+
*
|
|
7218
|
+
* @internal Shared with the framework bindings.
|
|
7219
|
+
*/
|
|
7220
|
+
function viewerSearchOf(controller) {
|
|
7221
|
+
return {
|
|
7222
|
+
state: controller.state,
|
|
7223
|
+
open: (query) => controller.open(query),
|
|
7224
|
+
setQuery: (query) => controller.setQuery(query),
|
|
7225
|
+
setMatchCase: (on) => controller.setMatchCase(on),
|
|
7226
|
+
next: () => controller.next(),
|
|
7227
|
+
previous: () => controller.previous(),
|
|
7228
|
+
close: () => controller.close()
|
|
7229
|
+
};
|
|
7230
|
+
}
|
|
7231
|
+
//#endregion
|
|
7232
|
+
//#region src/dom/search/find-key.ts
|
|
7233
|
+
/** Longest page selection that becomes the query. */
|
|
7234
|
+
const MAX_SELECTION = 200;
|
|
7235
|
+
const FIELD = "[data-ink-find-bar] input";
|
|
7236
|
+
/**
|
|
7237
|
+
* The text selected on the page inside `root`, on one line and trimmed; '' when there is none,
|
|
7238
|
+
* when it is not in `root`, when it is in the find bar, or when it is too long to be a query.
|
|
7239
|
+
*/
|
|
7240
|
+
function pageSelectionText(root) {
|
|
7241
|
+
const sel = root.ownerDocument.defaultView?.getSelection();
|
|
7242
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return "";
|
|
7243
|
+
const { anchorNode, focusNode } = sel;
|
|
7244
|
+
if (!anchorNode || !focusNode || !root.contains(anchorNode) || !root.contains(focusNode)) return "";
|
|
7245
|
+
const bar = (node) => (node instanceof Element ? node : node.parentElement)?.closest("[data-ink-find-bar]") != null;
|
|
7246
|
+
if (bar(anchorNode) || bar(focusNode)) return "";
|
|
7247
|
+
const text = sel.toString().replace(/\s+/g, " ").trim();
|
|
7248
|
+
return text.length > MAX_SELECTION ? "" : text;
|
|
7249
|
+
}
|
|
7250
|
+
/**
|
|
7251
|
+
* F by its letter, or by its place on a layout without Latin letters (Cyrillic, Greek), where
|
|
7252
|
+
* `key` is that layout's letter. A Latin layout with another letter there keeps that letter.
|
|
7253
|
+
*/
|
|
7254
|
+
function isFindKey(e) {
|
|
7255
|
+
if (e.key.toLowerCase() === "f") return true;
|
|
7256
|
+
return e.code === "KeyF" && !/^[a-z]$/i.test(e.key);
|
|
7257
|
+
}
|
|
7258
|
+
/**
|
|
7259
|
+
* Ctrl/Cmd+F on a viewer's root. Call it from a `keydown` listener on that root (it reads
|
|
7260
|
+
* `currentTarget`). Takes the key only when a find bar is mounted for this viewer and the event
|
|
7261
|
+
* comes from inside the root; otherwise the browser keeps it. Opens the search with the page selection as
|
|
7262
|
+
* the query, if there is one, and focuses and selects the field. Returns whether it took the key.
|
|
7263
|
+
*
|
|
7264
|
+
* @internal Shared with the framework bindings.
|
|
7265
|
+
*/
|
|
7266
|
+
function handleFindKey(e, controller, selection) {
|
|
7267
|
+
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey || !isFindKey(e)) return false;
|
|
7268
|
+
if (!controller.barAttached.get()) return false;
|
|
7269
|
+
const root = e.currentTarget;
|
|
7270
|
+
if (!(root instanceof HTMLElement)) return false;
|
|
7271
|
+
if (!(e.target instanceof Node) || !root.contains(e.target)) return false;
|
|
7272
|
+
e.preventDefault();
|
|
7273
|
+
const text = selection();
|
|
7274
|
+
controller.open(text === "" ? void 0 : text);
|
|
7275
|
+
const field = root.querySelector(FIELD);
|
|
7276
|
+
field?.focus();
|
|
7277
|
+
field?.select();
|
|
7278
|
+
return true;
|
|
7279
|
+
}
|
|
5877
7280
|
//#endregion
|
|
5878
7281
|
//#region src/dom/viewer/thread-popover.ts
|
|
5879
7282
|
function createThreadPopover(opts) {
|
|
@@ -6043,8 +7446,14 @@ function createThreadForm(n, actions) {
|
|
|
6043
7446
|
}
|
|
6044
7447
|
//#endregion
|
|
6045
7448
|
//#region src/dom/viewer/create-viewer.ts
|
|
7449
|
+
const noop = () => void 0;
|
|
7450
|
+
const passThrough = {
|
|
7451
|
+
batch: (fn) => fn(),
|
|
7452
|
+
track: () => noop,
|
|
7453
|
+
react: () => noop
|
|
7454
|
+
};
|
|
6046
7455
|
function createViewer(el, opts) {
|
|
6047
|
-
const { source: initial, assets, password, defaults, history, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, signatures: signatureStore, ...view } = opts;
|
|
7456
|
+
const { source: initial, assets, password, defaults, history, plugins, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, signatures: signatureStore, search: _search, ...view } = opts;
|
|
6048
7457
|
/** One library for the viewer's life, so templates and the menu outlive each document. */
|
|
6049
7458
|
const signatures = createSignatureLibrary(signatureStore ?? createMemorySignatureStore());
|
|
6050
7459
|
/** Tool options persist for the viewer's life; the tool style is per document. */
|
|
@@ -6056,6 +7465,7 @@ function createViewer(el, opts) {
|
|
|
6056
7465
|
const tools = writable(null);
|
|
6057
7466
|
/** The activated annotation: the note thread opens from it. */
|
|
6058
7467
|
const activated = writable(null);
|
|
7468
|
+
const rootEl = writable(null);
|
|
6059
7469
|
const root = h("div", {
|
|
6060
7470
|
"data-ink-root": "",
|
|
6061
7471
|
"data-state": "idle"
|
|
@@ -6074,6 +7484,35 @@ function createViewer(el, opts) {
|
|
|
6074
7484
|
}, scroll);
|
|
6075
7485
|
root.append(viewportRoot);
|
|
6076
7486
|
el.append(root);
|
|
7487
|
+
rootEl.set(scroll);
|
|
7488
|
+
/** One controller for the viewer's life: the host, the engine and the find bar share it. */
|
|
7489
|
+
const search = createSearchController({
|
|
7490
|
+
document: readonly(current),
|
|
7491
|
+
viewport: () => viewport.get()
|
|
7492
|
+
});
|
|
7493
|
+
/** What callers get: the actions, not the bar registration. */
|
|
7494
|
+
const viewerSearch = viewerSearchOf(search);
|
|
7495
|
+
root.addEventListener("keydown", (e) => {
|
|
7496
|
+
handleFindKey(e, search, () => pageSelectionText(root));
|
|
7497
|
+
});
|
|
7498
|
+
/**
|
|
7499
|
+
* One host for the viewer's life. It reads the history when a document opens, and `current`
|
|
7500
|
+
* is set before it notifies, so that is the new document's. With no document open, a
|
|
7501
|
+
* command's `batch` just runs its function.
|
|
7502
|
+
*/
|
|
7503
|
+
const host = createPluginHost({
|
|
7504
|
+
plugins: plugins ?? [],
|
|
7505
|
+
document: readonly(current),
|
|
7506
|
+
history: () => current.get()?.history ?? passThrough,
|
|
7507
|
+
activated: readonly(activated),
|
|
7508
|
+
setActivated: (id) => {
|
|
7509
|
+
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
7510
|
+
},
|
|
7511
|
+
root: readonly(rootEl),
|
|
7512
|
+
decorate: () => view.decorateAnnotation,
|
|
7513
|
+
viewport: () => viewport.get(),
|
|
7514
|
+
search: viewerSearch
|
|
7515
|
+
});
|
|
6077
7516
|
const focusReturn = createFocusReturn({
|
|
6078
7517
|
root: () => root,
|
|
6079
7518
|
activated: () => activated.get()
|
|
@@ -6123,6 +7562,8 @@ function createViewer(el, opts) {
|
|
|
6123
7562
|
};
|
|
6124
7563
|
const mounted = mountViewer({
|
|
6125
7564
|
...view,
|
|
7565
|
+
plugins: host,
|
|
7566
|
+
search,
|
|
6126
7567
|
container: scroll,
|
|
6127
7568
|
document: doc,
|
|
6128
7569
|
toolOptions,
|
|
@@ -6173,6 +7614,7 @@ function createViewer(el, opts) {
|
|
|
6173
7614
|
}
|
|
6174
7615
|
let loads = 0;
|
|
6175
7616
|
let destroyed = false;
|
|
7617
|
+
const destroyers = [];
|
|
6176
7618
|
let stop = () => void 0;
|
|
6177
7619
|
/** Tears down the open document, if any, and destroys it. */
|
|
6178
7620
|
function close() {
|
|
@@ -6219,7 +7661,7 @@ function createViewer(el, opts) {
|
|
|
6219
7661
|
});
|
|
6220
7662
|
}
|
|
6221
7663
|
open(initial);
|
|
6222
|
-
|
|
7664
|
+
const viewer = {
|
|
6223
7665
|
element: root,
|
|
6224
7666
|
status: readonly(status),
|
|
6225
7667
|
document: readonly(current),
|
|
@@ -6227,24 +7669,65 @@ function createViewer(el, opts) {
|
|
|
6227
7669
|
viewport: readonly(viewport),
|
|
6228
7670
|
tools: readonly(tools),
|
|
6229
7671
|
activated: readonly(activated),
|
|
6230
|
-
setActivated(id) {
|
|
7672
|
+
setActivated: (id) => {
|
|
6231
7673
|
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
6232
7674
|
},
|
|
7675
|
+
commands: { get: (id) => host.command(id) },
|
|
7676
|
+
search: viewerSearch,
|
|
6233
7677
|
open,
|
|
6234
7678
|
destroy() {
|
|
6235
7679
|
if (destroyed) return;
|
|
6236
7680
|
destroyed = true;
|
|
6237
7681
|
loads++;
|
|
6238
7682
|
close();
|
|
7683
|
+
for (const fn of destroyers) fn();
|
|
7684
|
+
search.destroy();
|
|
7685
|
+
host.destroy();
|
|
6239
7686
|
root.remove();
|
|
6240
7687
|
}
|
|
6241
7688
|
};
|
|
7689
|
+
toolsByViewer.set(viewer, [...view.tools ?? [], ...host.tools]);
|
|
7690
|
+
commandIdsByViewer.set(viewer, () => [...host.commands.keys()]);
|
|
7691
|
+
internals.set(viewer, {
|
|
7692
|
+
search,
|
|
7693
|
+
destroyers,
|
|
7694
|
+
destroyed: () => destroyed
|
|
7695
|
+
});
|
|
7696
|
+
return viewer;
|
|
7697
|
+
}
|
|
7698
|
+
const toolsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7699
|
+
/** The host tools a viewer was given (its own and its plugins'), for the controls' labels and icons. */
|
|
7700
|
+
function hostToolsOf(viewer) {
|
|
7701
|
+
return toolsByViewer.get(viewer) ?? [];
|
|
7702
|
+
}
|
|
7703
|
+
const commandIdsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7704
|
+
/** The ids of the commands a viewer's plugins registered, for the error on an unknown one. */
|
|
7705
|
+
function hostCommandIdsOf(viewer) {
|
|
7706
|
+
return commandIdsByViewer.get(viewer)?.() ?? [];
|
|
7707
|
+
}
|
|
7708
|
+
const internals = /* @__PURE__ */ new WeakMap();
|
|
7709
|
+
/** @internal The viewer's search controller, which the find bar registers with. */
|
|
7710
|
+
function searchControllerOf(viewer) {
|
|
7711
|
+
const found = internals.get(viewer);
|
|
7712
|
+
if (!found) throw new Error("Not a viewer made by createViewer");
|
|
7713
|
+
return found.search;
|
|
7714
|
+
}
|
|
7715
|
+
/**
|
|
7716
|
+
* @internal Runs `fn` when the viewer is destroyed, for what a control set up on it; at once
|
|
7717
|
+
* when it already is.
|
|
7718
|
+
*/
|
|
7719
|
+
function onViewerDestroy(viewer, fn) {
|
|
7720
|
+
const found = internals.get(viewer);
|
|
7721
|
+
if (!found) return;
|
|
7722
|
+
if (found.destroyed()) fn();
|
|
7723
|
+
else found.destroyers.push(fn);
|
|
6242
7724
|
}
|
|
6243
7725
|
//#endregion
|
|
6244
7726
|
//#region src/dom/viewer/controls.ts
|
|
6245
7727
|
/**
|
|
6246
7728
|
* Runs `connect` for every value `source` takes while it is set, undoing the previous run's
|
|
6247
|
-
* subscriptions first, and disables `control` while it is `null`.
|
|
7729
|
+
* subscriptions first, and disables `control` while it is `null`. Returns the function that
|
|
7730
|
+
* stops following.
|
|
6248
7731
|
*/
|
|
6249
7732
|
function follow(source, control, connect, reset) {
|
|
6250
7733
|
let stops = [];
|
|
@@ -6260,7 +7743,12 @@ function follow(source, control, connect, reset) {
|
|
|
6260
7743
|
else connect(value, watch);
|
|
6261
7744
|
};
|
|
6262
7745
|
run(source.get());
|
|
6263
|
-
source.subscribe(run);
|
|
7746
|
+
const stopSource = source.subscribe(run);
|
|
7747
|
+
return () => {
|
|
7748
|
+
stopSource();
|
|
7749
|
+
for (const s of stops) s();
|
|
7750
|
+
stops = [];
|
|
7751
|
+
};
|
|
6264
7752
|
}
|
|
6265
7753
|
const SVG = "http://www.w3.org/2000/svg";
|
|
6266
7754
|
const TOOL_ICONS = {
|
|
@@ -6295,6 +7783,7 @@ function createIcon(icon) {
|
|
|
6295
7783
|
}
|
|
6296
7784
|
return svg;
|
|
6297
7785
|
}
|
|
7786
|
+
/** @internal A labelled icon button, as the toolbar controls make them. */
|
|
6298
7787
|
function button(label, attrs, icon) {
|
|
6299
7788
|
const b = h("button", {
|
|
6300
7789
|
type: "button",
|
|
@@ -6306,6 +7795,31 @@ function button(label, attrs, icon) {
|
|
|
6306
7795
|
return b;
|
|
6307
7796
|
}
|
|
6308
7797
|
/**
|
|
7798
|
+
* Opens the viewer's find bar; pressed while it is open. Pressing it again focuses the field.
|
|
7799
|
+
* The bar itself is `createFindBar`.
|
|
7800
|
+
*/
|
|
7801
|
+
function createSearchButton(viewer) {
|
|
7802
|
+
const b = button("Find", {
|
|
7803
|
+
class: "ink-search-button",
|
|
7804
|
+
"data-ink-search-button": "",
|
|
7805
|
+
"aria-pressed": "false"
|
|
7806
|
+
}, Search);
|
|
7807
|
+
b.addEventListener("click", () => {
|
|
7808
|
+
viewer.search.open();
|
|
7809
|
+
const field = viewer.element.querySelector("[data-ink-find-bar] input");
|
|
7810
|
+
field?.focus();
|
|
7811
|
+
field?.select();
|
|
7812
|
+
});
|
|
7813
|
+
const show = (s) => {
|
|
7814
|
+
b.dataset.state = s.open ? "on" : "off";
|
|
7815
|
+
b.setAttribute("aria-pressed", String(s.open));
|
|
7816
|
+
};
|
|
7817
|
+
show(viewer.search.state.get());
|
|
7818
|
+
onViewerDestroy(viewer, viewer.search.state.subscribe(show));
|
|
7819
|
+
onViewerDestroy(viewer, follow(viewer.document, b, () => void 0));
|
|
7820
|
+
return b;
|
|
7821
|
+
}
|
|
7822
|
+
/**
|
|
6309
7823
|
* A toolbar following the WAI-ARIA toolbar pattern: its buttons share one Tab stop, with
|
|
6310
7824
|
* arrow keys, Home and End to move between them. Fields and selects are Tab stops of their
|
|
6311
7825
|
* own and keep their keys. Buttons added later join in.
|
|
@@ -6350,15 +7864,16 @@ function historyButton(viewer, action) {
|
|
|
6350
7864
|
return b;
|
|
6351
7865
|
}
|
|
6352
7866
|
/**
|
|
6353
|
-
* Sets the tool; pressed while it is the active one. A host tool's button
|
|
6354
|
-
*
|
|
7867
|
+
* Sets the tool; pressed while it is the active one. A host or plugin tool's button takes the
|
|
7868
|
+
* tool's own `label` and `icon`; `opts.label` wins, and without either it is named by the tool id.
|
|
6355
7869
|
*/
|
|
6356
7870
|
function createToolButton(viewer, tool, opts = {}) {
|
|
6357
|
-
const
|
|
7871
|
+
const own = hostToolsOf(viewer).find((t) => t.id === tool);
|
|
7872
|
+
const b = button(opts.label ?? TOOL_LABELS[tool] ?? own?.label ?? tool, {
|
|
6358
7873
|
class: "ink-tool-button",
|
|
6359
7874
|
"data-ink-tool-button": "",
|
|
6360
7875
|
"data-tool": tool
|
|
6361
|
-
}, TOOL_ICONS[tool]);
|
|
7876
|
+
}, TOOL_ICONS[tool] ?? own?.icon);
|
|
6362
7877
|
const show = (active) => {
|
|
6363
7878
|
const on = active === tool;
|
|
6364
7879
|
b.dataset.state = on ? "on" : "off";
|
|
@@ -6368,13 +7883,55 @@ function createToolButton(viewer, tool, opts = {}) {
|
|
|
6368
7883
|
follow(viewer.tools, b, (tools, watch) => watch(tools.active, show), () => show(null));
|
|
6369
7884
|
return b;
|
|
6370
7885
|
}
|
|
7886
|
+
/**
|
|
7887
|
+
* A plugin command as a button with its status text beside it. The button is named by the
|
|
7888
|
+
* command's label and disabled while the command is, except while it runs: then it keeps focus,
|
|
7889
|
+
* with `aria-busy` and `aria-disabled`, and a press does nothing. The status is a live region. A command without an icon shows its label as the button's text. For an id no
|
|
7890
|
+
* plugin registered it logs `InkError('UnknownCommand')` to the console and returns the same
|
|
7891
|
+
* markup disabled, `aria-hidden` and unnamed.
|
|
7892
|
+
*/
|
|
7893
|
+
function createCommandButton(viewer, id) {
|
|
7894
|
+
const command = viewer.commands.get(id);
|
|
7895
|
+
const status = h("span", {
|
|
7896
|
+
class: "ink-command-status",
|
|
7897
|
+
role: "status"
|
|
7898
|
+
});
|
|
7899
|
+
if (!command) {
|
|
7900
|
+
console.error(unknownCommand(id, hostCommandIdsOf(viewer)));
|
|
7901
|
+
const pending = h("button", {
|
|
7902
|
+
type: "button",
|
|
7903
|
+
"data-command": id,
|
|
7904
|
+
"aria-hidden": "true"
|
|
7905
|
+
});
|
|
7906
|
+
pending.disabled = true;
|
|
7907
|
+
return h("div", { class: "ink-command" }, pending, status);
|
|
7908
|
+
}
|
|
7909
|
+
const b = button(command.label, { "data-command": id }, command.icon);
|
|
7910
|
+
if (!command.icon) b.textContent = command.label;
|
|
7911
|
+
b.addEventListener("click", () => void command.run());
|
|
7912
|
+
const sync = () => {
|
|
7913
|
+
const busy = command.busy.get();
|
|
7914
|
+
b.disabled = !command.enabled.get() && !busy;
|
|
7915
|
+
b.setAttribute("aria-busy", String(busy));
|
|
7916
|
+
if (busy) b.setAttribute("aria-disabled", "true");
|
|
7917
|
+
else b.removeAttribute("aria-disabled");
|
|
7918
|
+
};
|
|
7919
|
+
command.enabled.subscribe(sync);
|
|
7920
|
+
command.busy.subscribe(sync);
|
|
7921
|
+
sync();
|
|
7922
|
+
command.status.subscribe((text) => {
|
|
7923
|
+
status.textContent = text;
|
|
7924
|
+
});
|
|
7925
|
+
status.textContent = command.status.get();
|
|
7926
|
+
return h("div", { class: "ink-command" }, b, status);
|
|
7927
|
+
}
|
|
6371
7928
|
function createZoomIn(viewer) {
|
|
6372
7929
|
const b = button("Zoom in", {
|
|
6373
7930
|
class: "ink-zoom-in",
|
|
6374
7931
|
"data-ink-zoom-in": ""
|
|
6375
7932
|
}, ZoomIn);
|
|
6376
7933
|
b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(1.25));
|
|
6377
|
-
follow(viewer.viewport, b, () => void 0);
|
|
7934
|
+
onViewerDestroy(viewer, follow(viewer.viewport, b, () => void 0));
|
|
6378
7935
|
return b;
|
|
6379
7936
|
}
|
|
6380
7937
|
function createZoomOut(viewer) {
|
|
@@ -6383,7 +7940,7 @@ function createZoomOut(viewer) {
|
|
|
6383
7940
|
"data-ink-zoom-out": ""
|
|
6384
7941
|
}, ZoomOut);
|
|
6385
7942
|
b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(.8));
|
|
6386
|
-
follow(viewer.viewport, b, () => void 0);
|
|
7943
|
+
onViewerDestroy(viewer, follow(viewer.viewport, b, () => void 0));
|
|
6387
7944
|
return b;
|
|
6388
7945
|
}
|
|
6389
7946
|
/** Fit width, fit page and the zoom levels; a zoom between levels gets an option of its own. */
|
|
@@ -6548,6 +8105,167 @@ function rovingTabindex(bar) {
|
|
|
6548
8105
|
};
|
|
6549
8106
|
}
|
|
6550
8107
|
//#endregion
|
|
8108
|
+
//#region src/dom/search/find-text.ts
|
|
8109
|
+
/**
|
|
8110
|
+
* The count beside the field: "3 of 17", "3 of 12…" while scanning, "No results".
|
|
8111
|
+
*
|
|
8112
|
+
* @internal Shared with the framework bindings.
|
|
8113
|
+
*/
|
|
8114
|
+
function findCountText(s) {
|
|
8115
|
+
if (isBlankQuery(s.query)) return "";
|
|
8116
|
+
if (s.hits.length === 0) return s.scanning ? "" : "No results";
|
|
8117
|
+
return `${s.current + 1} of ${s.hits.length}${s.scanning ? "…" : ""}`;
|
|
8118
|
+
}
|
|
8119
|
+
/**
|
|
8120
|
+
* Returns the function a bar calls with each search state for its live region's text.
|
|
8121
|
+
*
|
|
8122
|
+
* A settled scan reads "Match 3 of 17, page 2" or "No results". While a scan runs it says
|
|
8123
|
+
* nothing until a step moves the current hit, then "Match 3 of 12 so far, page 2", and keeps
|
|
8124
|
+
* that text as more hits arrive, so the region speaks once per step and once on settle rather
|
|
8125
|
+
* than once per hit.
|
|
8126
|
+
*
|
|
8127
|
+
* @internal Shared with the framework bindings; each bar keeps its own.
|
|
8128
|
+
*/
|
|
8129
|
+
function findAnnouncer() {
|
|
8130
|
+
let shown;
|
|
8131
|
+
let said = "";
|
|
8132
|
+
return (s) => {
|
|
8133
|
+
const hit = s.hits[s.current];
|
|
8134
|
+
if (!s.open || isBlankQuery(s.query)) {
|
|
8135
|
+
shown = void 0;
|
|
8136
|
+
said = "";
|
|
8137
|
+
} else if (!s.scanning) {
|
|
8138
|
+
shown = hit;
|
|
8139
|
+
said = hit ? `Match ${s.current + 1} of ${s.hits.length}, page ${hit.page + 1}` : "No results";
|
|
8140
|
+
} else {
|
|
8141
|
+
if (!hit) said = "";
|
|
8142
|
+
else if (shown && hit !== shown) said = `Match ${s.current + 1} of ${s.hits.length} so far, page ${hit.page + 1}`;
|
|
8143
|
+
shown = hit;
|
|
8144
|
+
}
|
|
8145
|
+
return said;
|
|
8146
|
+
};
|
|
8147
|
+
}
|
|
8148
|
+
//#endregion
|
|
8149
|
+
//#region src/dom/search/find-bar.ts
|
|
8150
|
+
/**
|
|
8151
|
+
* The find bar for `viewer`: a field, the match count, previous and next, a match-case toggle
|
|
8152
|
+
* and close. Its buttons keep focus while used; Enter in the field steps to the next match,
|
|
8153
|
+
* Shift+Enter to the previous, Escape closes. Closing returns focus to where it was before the
|
|
8154
|
+
* bar opened, else to the document.
|
|
8155
|
+
*/
|
|
8156
|
+
function createFindBar(viewer) {
|
|
8157
|
+
const controller = searchControllerOf(viewer);
|
|
8158
|
+
const { state } = controller;
|
|
8159
|
+
const field = h("input", {
|
|
8160
|
+
type: "text",
|
|
8161
|
+
"aria-label": "Find in document",
|
|
8162
|
+
autocomplete: "off",
|
|
8163
|
+
spellcheck: "false"
|
|
8164
|
+
});
|
|
8165
|
+
const count = h("span", {
|
|
8166
|
+
class: "ink-find-count",
|
|
8167
|
+
"data-ink-find-count": ""
|
|
8168
|
+
});
|
|
8169
|
+
const previous = button("Previous match", { "data-ink-find-previous": "" }, ChevronUp);
|
|
8170
|
+
const next = button("Next match", { "data-ink-find-next": "" }, ChevronDown);
|
|
8171
|
+
const matchCase = button("Match case", {
|
|
8172
|
+
"data-ink-find-match-case": "",
|
|
8173
|
+
"aria-pressed": "false"
|
|
8174
|
+
}, CaseSensitive);
|
|
8175
|
+
const close = button("Close find", { "data-ink-find-close": "" }, X);
|
|
8176
|
+
const live = h("div", {
|
|
8177
|
+
class: "ink-visually-hidden",
|
|
8178
|
+
"data-ink-find-status": "",
|
|
8179
|
+
"aria-live": "polite"
|
|
8180
|
+
});
|
|
8181
|
+
const bar = h("div", {
|
|
8182
|
+
class: "ink-find-bar",
|
|
8183
|
+
"data-ink-find-bar": "",
|
|
8184
|
+
role: "search",
|
|
8185
|
+
hidden: ""
|
|
8186
|
+
}, field, count, previous, next, matchCase, close, live);
|
|
8187
|
+
field.addEventListener("input", () => controller.setQuery(field.value));
|
|
8188
|
+
previous.addEventListener("click", () => controller.previous());
|
|
8189
|
+
next.addEventListener("click", () => controller.next());
|
|
8190
|
+
matchCase.addEventListener("click", () => controller.setMatchCase(!state.get().matchCase));
|
|
8191
|
+
close.addEventListener("click", () => controller.close());
|
|
8192
|
+
field.addEventListener("keydown", (e) => {
|
|
8193
|
+
if (e.key !== "Enter" || e.isComposing) return;
|
|
8194
|
+
e.preventDefault();
|
|
8195
|
+
if (e.shiftKey) controller.previous();
|
|
8196
|
+
else controller.next();
|
|
8197
|
+
});
|
|
8198
|
+
bar.addEventListener("keydown", (e) => {
|
|
8199
|
+
if (e.key !== "Escape" || e.defaultPrevented || e.isComposing) return;
|
|
8200
|
+
e.preventDefault();
|
|
8201
|
+
e.stopPropagation();
|
|
8202
|
+
controller.close();
|
|
8203
|
+
});
|
|
8204
|
+
/** Where focus was when the bar opened. */
|
|
8205
|
+
let before = null;
|
|
8206
|
+
let wasOpen = false;
|
|
8207
|
+
const announce = findAnnouncer();
|
|
8208
|
+
let announced = "";
|
|
8209
|
+
function restoreFocus() {
|
|
8210
|
+
const target = before?.isConnected && !bar.contains(before) ? before : viewer.element.querySelector("[data-ink-scroll]");
|
|
8211
|
+
before = null;
|
|
8212
|
+
target?.focus({ preventScroll: true });
|
|
8213
|
+
}
|
|
8214
|
+
function show(s) {
|
|
8215
|
+
if (s.open !== wasOpen) {
|
|
8216
|
+
wasOpen = s.open;
|
|
8217
|
+
if (s.open) {
|
|
8218
|
+
const active = bar.ownerDocument.activeElement;
|
|
8219
|
+
before = active instanceof HTMLElement && active !== bar.ownerDocument.body && !bar.contains(active) ? active : null;
|
|
8220
|
+
bar.hidden = false;
|
|
8221
|
+
field.focus();
|
|
8222
|
+
field.select();
|
|
8223
|
+
} else {
|
|
8224
|
+
const active = bar.ownerDocument.activeElement;
|
|
8225
|
+
const hadFocus = bar.contains(active) || active === bar.ownerDocument.body;
|
|
8226
|
+
bar.hidden = true;
|
|
8227
|
+
if (hadFocus) restoreFocus();
|
|
8228
|
+
else before = null;
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
if (field.value !== s.query) {
|
|
8232
|
+
field.value = s.query;
|
|
8233
|
+
if (bar.ownerDocument.activeElement === field) field.select();
|
|
8234
|
+
}
|
|
8235
|
+
count.textContent = findCountText(s);
|
|
8236
|
+
matchCase.setAttribute("aria-pressed", String(s.matchCase));
|
|
8237
|
+
const say = announce(s);
|
|
8238
|
+
if (say !== announced) {
|
|
8239
|
+
announced = say;
|
|
8240
|
+
live.textContent = say;
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
let detach = null;
|
|
8244
|
+
const sync = () => {
|
|
8245
|
+
const inside = viewer.element.contains(bar);
|
|
8246
|
+
if (inside && !detach) detach = controller.attachBar();
|
|
8247
|
+
else if (!inside && detach) {
|
|
8248
|
+
detach();
|
|
8249
|
+
detach = null;
|
|
8250
|
+
}
|
|
8251
|
+
};
|
|
8252
|
+
const observer = new MutationObserver(sync);
|
|
8253
|
+
observer.observe(viewer.element, {
|
|
8254
|
+
childList: true,
|
|
8255
|
+
subtree: true
|
|
8256
|
+
});
|
|
8257
|
+
const stopState = state.subscribe(show);
|
|
8258
|
+
show(state.get());
|
|
8259
|
+
sync();
|
|
8260
|
+
onViewerDestroy(viewer, () => {
|
|
8261
|
+
observer.disconnect();
|
|
8262
|
+
stopState();
|
|
8263
|
+
detach?.();
|
|
8264
|
+
detach = null;
|
|
8265
|
+
});
|
|
8266
|
+
return bar;
|
|
8267
|
+
}
|
|
8268
|
+
//#endregion
|
|
6551
8269
|
//#region src/dom/viewer/menu.ts
|
|
6552
8270
|
const FOCUSABLE = "select:not(:disabled), input:not(:disabled), button:not(:disabled):not([tabindex=\"-1\"])";
|
|
6553
8271
|
function createMenu(trigger, opts) {
|
|
@@ -8110,6 +9828,6 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
8110
9828
|
return root;
|
|
8111
9829
|
}
|
|
8112
9830
|
//#endregion
|
|
8113
|
-
export { ARROWHEAD_CHOICES, ARROWHEAD_ENDING, COLOR_COLUMNS, COLOR_LABELS, DASH_PATTERN, DEFAULT_CORNER_RADIUS, DEFAULT_FREETEXT_WIDTH, DEFAULT_HIGHLIGHT_OPTIONS, DEFAULT_SHAPE_SIZE, FONT_FAMILIES, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, arrowheadChoice, attachViewportKeys, builtinTools, clampFontSize, colorMenuModel, computeCssLayout, createColorMenu, createFocusReturn, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPopover, createPrevPage, createRedo, createShapeMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, editFreeText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, highlightTool, hitFor, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, placePanel, redactAreaTool, renderAnnotationContent, renderTypedSignature, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, signatureTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, trapTab, visibleRangeCss, withHostTools };
|
|
9831
|
+
export { ARROWHEAD_CHOICES, ARROWHEAD_ENDING, COLOR_COLUMNS, COLOR_LABELS, DASH_PATTERN, DEFAULT_CORNER_RADIUS, DEFAULT_FREETEXT_WIDTH, DEFAULT_HIGHLIGHT_OPTIONS, DEFAULT_SHAPE_SIZE, FONT_FAMILIES, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, arrowheadChoice, attachViewportKeys, builtinTools, clampFontSize, colorMenuModel, computeCssLayout, createColorMenu, createCommandButton, createFindBar, createFocusReturn, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPluginHost, createPluginLayers, createPopover, createPrevPage, createRedo, createSearchButton, createSearchController, createShapeMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, definePlugin, editFreeText, findAnnouncer, findCountText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleFindKey, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, highlightTool, hitFor, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, pageSelectionText, placePanel, redactAreaTool, renderAnnotationContent, renderTypedSignature, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, signatureTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, trapTab, unknownCommand, viewerSearchOf, visibleRangeCss, withHostTools };
|
|
8114
9832
|
|
|
8115
9833
|
//# sourceMappingURL=dom.mjs.map
|