@inkbar/core 0.5.0 → 0.6.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 +73 -1
- package/dist/dom.d.mts +477 -41
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +1338 -259
- 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-By4B40DP.d.mts} +86 -4
- package/dist/signatures-By4B40DP.d.mts.map +1 -0
- package/dist/{signatures-CMapTKrC.mjs → signatures-D68fuLlt.mjs} +268 -50
- package/dist/signatures-D68fuLlt.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/signatures-BwaXi7C8.d.mts.map +0 -1
- package/dist/signatures-CMapTKrC.mjs.map +0 -1
package/dist/dom.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $t as derived, B as isTextMarkup, Bt as withCalloutBounds, E as withoutReplies, G as resizedRect, Gt as isKnown, H as movedEndpoint, Ht as LINE_ENDINGS, I as hasCallout, J as withBox, Jt as cssVariableDefaults, K as scaled, L as hasCornerHandles, Lt as calloutStroke, O as createToolStyle, Qt as createRecordStore, R as hasRectGeometry, Rt as freeTextBox, S as isThreadRoot, T as threadRoot, U as movedTip, V as keepsAspect, W as resizedBox, Y as openDocument, Z as deriveRect, Zt as mergeLineRects, _ as defaultEnds, b as isAbandonedNote, bn as isInkError, d as recogniseStroke, dn as rectFromPoints, en as readonly, f as DEFAULT_CORNER_RADIUS, g as constrain, h as SHAPE_KINDS, i as createInkTemplate, in as invertMatrix, l as placeSignature, m as DEFAULT_SHAPE_SIZE, mn as rectsIntersect, nn as applyMatrix, o as createMemorySignatureStore, p as DEFAULT_SHAPE_OPTIONS, q as translated, r as createImageTemplate, s as createSignatureLibrary, tn as writable, tt as isProduction, v as isPointerOnlyKind, w as threadOf, y as shapeInput, yn as InkError, z as isBubble, zt as isStandardCallout } from "./signatures-D68fuLlt.mjs";
|
|
2
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";
|
|
3
3
|
import { getStroke } from "perfect-freehand";
|
|
4
4
|
//#region src/dom/transform.ts
|
|
@@ -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,8 +2820,1218 @@ function createOverlay(opts) {
|
|
|
2818
2820
|
};
|
|
2819
2821
|
}
|
|
2820
2822
|
//#endregion
|
|
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
|
+
});
|
|
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
|
+
}
|
|
2863
|
+
});
|
|
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
|
|
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;
|
|
2910
|
+
return {
|
|
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
|
+
}
|
|
2921
|
+
},
|
|
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 });
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
//#endregion
|
|
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;
|
|
2956
|
+
}
|
|
2957
|
+
//#endregion
|
|
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);
|
|
2992
|
+
};
|
|
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
|
+
/**
|
|
3807
|
+
* `value` seen with `extra` laid over it, read-only. Everything else is read from and written to
|
|
3808
|
+
* `value` itself, and its methods are bound to it, so a method always runs on the original
|
|
3809
|
+
* (a class instance keeps its `#private` fields, and `this.id` is the plugin's own id).
|
|
3810
|
+
*/
|
|
3811
|
+
function withProps(value, extra) {
|
|
3812
|
+
const bound = /* @__PURE__ */ new Map();
|
|
3813
|
+
const own = (key) => Object.hasOwn(extra, key);
|
|
3814
|
+
return new Proxy(Object.create(null), {
|
|
3815
|
+
get(_, key) {
|
|
3816
|
+
if (own(key)) return extra[key];
|
|
3817
|
+
const v = Reflect.get(value, key, value);
|
|
3818
|
+
if (typeof v !== "function") return v;
|
|
3819
|
+
let entry = bound.get(key);
|
|
3820
|
+
if (entry?.fn !== v) {
|
|
3821
|
+
entry = {
|
|
3822
|
+
fn: v,
|
|
3823
|
+
bound: v.bind(value)
|
|
3824
|
+
};
|
|
3825
|
+
bound.set(key, entry);
|
|
3826
|
+
}
|
|
3827
|
+
return entry.bound;
|
|
3828
|
+
},
|
|
3829
|
+
set: (_, key, v) => !own(key) && Reflect.set(value, key, v, value),
|
|
3830
|
+
defineProperty(target, key, d) {
|
|
3831
|
+
if (own(key) || !Reflect.defineProperty(value, key, d)) return false;
|
|
3832
|
+
const defined = Reflect.getOwnPropertyDescriptor(value, key);
|
|
3833
|
+
if (defined && !defined.configurable) Reflect.defineProperty(target, key, defined);
|
|
3834
|
+
return true;
|
|
3835
|
+
},
|
|
3836
|
+
deleteProperty: (_, key) => !own(key) && Reflect.deleteProperty(value, key),
|
|
3837
|
+
has: (_, key) => own(key) || Reflect.has(value, key),
|
|
3838
|
+
ownKeys: () => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(value), ...Reflect.ownKeys(extra)])],
|
|
3839
|
+
getOwnPropertyDescriptor(target, key) {
|
|
3840
|
+
const d = Reflect.getOwnPropertyDescriptor(own(key) ? extra : value, key);
|
|
3841
|
+
const fixed = Reflect.getOwnPropertyDescriptor(target, key)?.configurable === false;
|
|
3842
|
+
return d && {
|
|
3843
|
+
...d,
|
|
3844
|
+
configurable: !fixed
|
|
3845
|
+
};
|
|
3846
|
+
},
|
|
3847
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(value)
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
/** Runs a plugin callback; a throw is logged so one plugin cannot break the others. */
|
|
3851
|
+
function guard(fn) {
|
|
3852
|
+
try {
|
|
3853
|
+
return fn();
|
|
3854
|
+
} catch (error) {
|
|
3855
|
+
console.error(error);
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* @internal For bindings and `createViewer`, which place a viewer's plugins; plugin authors use
|
|
3861
|
+
* `definePlugin` and never call it.
|
|
3862
|
+
*/
|
|
3863
|
+
function createPluginHost(opts) {
|
|
3864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3865
|
+
for (const plugin of opts.plugins) {
|
|
3866
|
+
if (seen.has(plugin.id)) throw new InkError("DuplicatePlugin", `A plugin with id ${plugin.id} is registered twice`, { id: plugin.id });
|
|
3867
|
+
seen.add(plugin.id);
|
|
3868
|
+
}
|
|
3869
|
+
const registry = createCommandRegistry({ document: opts.document });
|
|
3870
|
+
const tools = [];
|
|
3871
|
+
const decorators = [];
|
|
3872
|
+
const layers = createLayerRegistry();
|
|
3873
|
+
const requests = [];
|
|
3874
|
+
const documentFns = [];
|
|
3875
|
+
const editFns = [];
|
|
3876
|
+
const setupCleanups = [];
|
|
3877
|
+
const redecorators = /* @__PURE__ */ new Set();
|
|
3878
|
+
let opened = null;
|
|
3879
|
+
let destroyed = false;
|
|
3880
|
+
/** Creates a requested store for the open document and has its history track it. */
|
|
3881
|
+
const openStore = (request, state, history) => {
|
|
3882
|
+
const store = createRecordStore(request.key, {
|
|
3883
|
+
initial: request.opts.initial?.(state.doc) ?? [],
|
|
3884
|
+
...request.opts.onChange && { onChange: request.opts.onChange }
|
|
3885
|
+
});
|
|
3886
|
+
state.stores.push(store);
|
|
3887
|
+
state.detach.push(history.track(store));
|
|
3888
|
+
request.current.set(store);
|
|
3889
|
+
};
|
|
3890
|
+
const runOnDocument = (fn, state) => {
|
|
3891
|
+
const cleanup = guard(() => fn(state.doc));
|
|
3892
|
+
if (typeof cleanup === "function") state.cleanups.push(cleanup);
|
|
3893
|
+
};
|
|
3894
|
+
const open = (doc) => {
|
|
3895
|
+
const history = opts.history();
|
|
3896
|
+
const state = {
|
|
3897
|
+
doc,
|
|
3898
|
+
stores: [],
|
|
3899
|
+
detach: [],
|
|
3900
|
+
cleanups: []
|
|
3901
|
+
};
|
|
3902
|
+
opened = state;
|
|
3903
|
+
for (const request of requests) openStore(request, state, history);
|
|
3904
|
+
for (const fn of editFns) state.detach.push(history.react(fn));
|
|
3905
|
+
for (const fn of documentFns) runOnDocument(fn, state);
|
|
3906
|
+
};
|
|
3907
|
+
const close = () => {
|
|
3908
|
+
const state = opened;
|
|
3909
|
+
if (!state) return;
|
|
3910
|
+
opened = null;
|
|
3911
|
+
for (const detach of state.detach) detach();
|
|
3912
|
+
for (const store of state.stores) store.destroy();
|
|
3913
|
+
for (const request of requests) request.current.set(null);
|
|
3914
|
+
for (const cleanup of state.cleanups.reverse()) guard(cleanup);
|
|
3915
|
+
};
|
|
3916
|
+
const contextFor = (plugin) => {
|
|
3917
|
+
const storeNames = /* @__PURE__ */ new Set();
|
|
3918
|
+
return {
|
|
3919
|
+
id: plugin.id,
|
|
3920
|
+
document: opts.document,
|
|
3921
|
+
history: { batch: (fn) => opts.history().batch(fn) },
|
|
3922
|
+
activated: opts.activated,
|
|
3923
|
+
setActivated: (id) => opts.setActivated(id),
|
|
3924
|
+
root: opts.root,
|
|
3925
|
+
commands: { add: (spec) => registry.add(plugin.id, spec) },
|
|
3926
|
+
tools: { add(tool) {
|
|
3927
|
+
const id = `${plugin.id}.${tool.id}`;
|
|
3928
|
+
if (tools.some((t) => t.id === id)) throw new InkError("DuplicateId", `A tool with id ${id} already exists`, { id });
|
|
3929
|
+
tools.push(withProps(tool, { id }));
|
|
3930
|
+
} },
|
|
3931
|
+
decorate(fn) {
|
|
3932
|
+
decorators.push(fn);
|
|
3933
|
+
},
|
|
3934
|
+
redecorate() {
|
|
3935
|
+
for (const listener of [...redecorators]) listener();
|
|
3936
|
+
},
|
|
3937
|
+
store(name, storeOpts = {}) {
|
|
3938
|
+
const key = `${plugin.id}.${name}`;
|
|
3939
|
+
if (storeNames.has(name)) throw new InkError("DuplicateId", `A store with key ${key} already exists`, { key });
|
|
3940
|
+
storeNames.add(name);
|
|
3941
|
+
const request = {
|
|
3942
|
+
key,
|
|
3943
|
+
opts: storeOpts,
|
|
3944
|
+
current: writable(null)
|
|
3945
|
+
};
|
|
3946
|
+
requests.push(request);
|
|
3947
|
+
if (opened) openStore(request, opened, opts.history());
|
|
3948
|
+
return readonly(request.current);
|
|
3949
|
+
},
|
|
3950
|
+
onDocument(fn) {
|
|
3951
|
+
documentFns.push(fn);
|
|
3952
|
+
if (opened) runOnDocument(fn, opened);
|
|
3953
|
+
},
|
|
3954
|
+
onAnnotationEdit(fn) {
|
|
3955
|
+
editFns.push(fn);
|
|
3956
|
+
if (opened) opened.detach.push(opts.history().react(fn));
|
|
3957
|
+
},
|
|
3958
|
+
layer(spec) {
|
|
3959
|
+
return layers.add(withProps(spec, { plugin: plugin.id }));
|
|
3960
|
+
},
|
|
3961
|
+
scrollTo(page, rect, scrollOpts) {
|
|
3962
|
+
opts.viewport?.()?.scrollToRect(page, rect, scrollOpts);
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
};
|
|
3966
|
+
for (const plugin of opts.plugins) {
|
|
3967
|
+
let cleanup;
|
|
3968
|
+
try {
|
|
3969
|
+
cleanup = plugin.setup(contextFor(plugin));
|
|
3970
|
+
} catch (error) {
|
|
3971
|
+
for (const done of setupCleanups.reverse()) guard(done);
|
|
3972
|
+
throw error;
|
|
3973
|
+
}
|
|
3974
|
+
if (typeof cleanup === "function") setupCleanups.push(cleanup);
|
|
3975
|
+
}
|
|
3976
|
+
const current = opts.document.get();
|
|
3977
|
+
if (current) open(current);
|
|
3978
|
+
const stopDocument = opts.document.subscribe((doc) => {
|
|
3979
|
+
close();
|
|
3980
|
+
if (doc) open(doc);
|
|
3981
|
+
});
|
|
3982
|
+
return {
|
|
3983
|
+
commands: registry.commands,
|
|
3984
|
+
command: (id) => registry.commands.get(id),
|
|
3985
|
+
tools,
|
|
3986
|
+
decorate(annotation) {
|
|
3987
|
+
const all = [opts.decorate?.(), ...decorators];
|
|
3988
|
+
let merged;
|
|
3989
|
+
for (const fn of all) {
|
|
3990
|
+
const d = fn && guard(() => fn(annotation));
|
|
3991
|
+
if (!d) continue;
|
|
3992
|
+
merged ??= {};
|
|
3993
|
+
if (d.data) merged = {
|
|
3994
|
+
...merged,
|
|
3995
|
+
data: {
|
|
3996
|
+
...d.data,
|
|
3997
|
+
...merged.data
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
if (d.classes) merged = {
|
|
4001
|
+
...merged,
|
|
4002
|
+
classes: [...merged.classes ?? [], ...d.classes]
|
|
4003
|
+
};
|
|
4004
|
+
if (merged.description === void 0 && d.description !== void 0) merged = {
|
|
4005
|
+
...merged,
|
|
4006
|
+
description: d.description
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
return merged;
|
|
4010
|
+
},
|
|
4011
|
+
layers: layers.layers,
|
|
4012
|
+
watchLayers: (listener) => layers.watchLayers(listener),
|
|
4013
|
+
watchRedecorate(listener) {
|
|
4014
|
+
const run = () => listener();
|
|
4015
|
+
redecorators.add(run);
|
|
4016
|
+
return () => {
|
|
4017
|
+
redecorators.delete(run);
|
|
4018
|
+
};
|
|
4019
|
+
},
|
|
4020
|
+
destroy() {
|
|
4021
|
+
if (destroyed) return;
|
|
4022
|
+
destroyed = true;
|
|
4023
|
+
stopDocument();
|
|
4024
|
+
close();
|
|
4025
|
+
for (const cleanup of setupCleanups.reverse()) guard(cleanup);
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
//#endregion
|
|
2821
4030
|
//#region src/dom/page-mount.ts
|
|
2822
|
-
/**
|
|
4031
|
+
/**
|
|
4032
|
+
* Canvas, text layer, annotation overlay and any plugin layers for one page, stacked in that
|
|
4033
|
+
* order.
|
|
4034
|
+
*/
|
|
2823
4035
|
function createPageMount(opts) {
|
|
2824
4036
|
const { document, page, host, transform } = opts;
|
|
2825
4037
|
const callbacks = (layer) => ({
|
|
@@ -2852,6 +4064,7 @@ function createPageMount(opts) {
|
|
|
2852
4064
|
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {},
|
|
2853
4065
|
...opts.decorate ? { decorate: opts.decorate } : {}
|
|
2854
4066
|
});
|
|
4067
|
+
const layers = opts.layers?.mount(page, host, transform);
|
|
2855
4068
|
return {
|
|
2856
4069
|
renderer,
|
|
2857
4070
|
textLayer,
|
|
@@ -2860,8 +4073,10 @@ function createPageMount(opts) {
|
|
|
2860
4073
|
renderer.update(next);
|
|
2861
4074
|
textLayer.update(next);
|
|
2862
4075
|
overlay.update(next);
|
|
4076
|
+
layers?.update(next);
|
|
2863
4077
|
},
|
|
2864
4078
|
destroy() {
|
|
4079
|
+
layers?.destroy();
|
|
2865
4080
|
overlay.destroy();
|
|
2866
4081
|
textLayer.destroy();
|
|
2867
4082
|
renderer.destroy();
|
|
@@ -3611,6 +4826,8 @@ function onScrollBar(e, root) {
|
|
|
3611
4826
|
function inCustomContent(target) {
|
|
3612
4827
|
if (!(target instanceof Element)) return false;
|
|
3613
4828
|
if (target.closest("[data-ink-editor]")) return true;
|
|
4829
|
+
const layer = target.closest(".ink-plugin-layer");
|
|
4830
|
+
if (layer && layer !== target) return true;
|
|
3614
4831
|
const el = target.closest("[data-annotation]");
|
|
3615
4832
|
return el !== null && el !== target && el.hasAttribute("data-custom") && target.closest("[data-handle]") === null;
|
|
3616
4833
|
}
|
|
@@ -5381,7 +6598,7 @@ function handleTabKeys(e, tablist) {
|
|
|
5381
6598
|
tabs[next].focus({ preventScroll: true });
|
|
5382
6599
|
tabs[next].click();
|
|
5383
6600
|
}
|
|
5384
|
-
const TABBABLE
|
|
6601
|
+
const TABBABLE = [
|
|
5385
6602
|
"a[href]",
|
|
5386
6603
|
"button:not(:disabled)",
|
|
5387
6604
|
"input:not(:disabled):not([type=\"hidden\"])",
|
|
@@ -5395,7 +6612,7 @@ const TABBABLE$1 = [
|
|
|
5395
6612
|
*/
|
|
5396
6613
|
function trapTab(e, container) {
|
|
5397
6614
|
if (e.key !== "Tab" || e.defaultPrevented) return;
|
|
5398
|
-
const stops = [...container.querySelectorAll(TABBABLE
|
|
6615
|
+
const stops = [...container.querySelectorAll(TABBABLE)].filter((el) => !el.matches(":disabled") && el.getClientRects().length > 0);
|
|
5399
6616
|
if (stops.length === 0) {
|
|
5400
6617
|
e.preventDefault();
|
|
5401
6618
|
return;
|
|
@@ -5412,239 +6629,6 @@ function trapTab(e, container) {
|
|
|
5412
6629
|
}
|
|
5413
6630
|
}
|
|
5414
6631
|
//#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;
|
|
5587
|
-
e.preventDefault();
|
|
5588
|
-
opts.onDismiss();
|
|
5589
|
-
};
|
|
5590
|
-
const onPress = (e) => {
|
|
5591
|
-
if (panel.hidden || panel.contains(e.target)) return;
|
|
5592
|
-
if (shouldKeepOpenOnAnnotation(e.target)) return;
|
|
5593
|
-
opts.onDismiss();
|
|
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;
|
|
5603
|
-
e.preventDefault();
|
|
5604
|
-
(e.shiftKey ? last : first).focus();
|
|
5605
|
-
};
|
|
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
|
-
return {
|
|
5612
|
-
panel,
|
|
5613
|
-
get anchor() {
|
|
5614
|
-
return anchor;
|
|
5615
|
-
},
|
|
5616
|
-
show(id) {
|
|
5617
|
-
if (id === anchoredId) return;
|
|
5618
|
-
stopObserving();
|
|
5619
|
-
anchoredId = id;
|
|
5620
|
-
stopObserving = observeAnnotationElement(root, id, onAnchor);
|
|
5621
|
-
},
|
|
5622
|
-
hide() {
|
|
5623
|
-
if (anchoredId === null) return;
|
|
5624
|
-
stopObserving();
|
|
5625
|
-
stopObserving = () => void 0;
|
|
5626
|
-
anchoredId = null;
|
|
5627
|
-
const back = anchor;
|
|
5628
|
-
stranded = null;
|
|
5629
|
-
onAnchor(null, true);
|
|
5630
|
-
queueMicrotask(() => {
|
|
5631
|
-
if (anchoredId !== null || !panel.isConnected) return;
|
|
5632
|
-
focusReturn.onCloseAutoFocus(new Event("closeautofocus", { cancelable: true }), back);
|
|
5633
|
-
});
|
|
5634
|
-
},
|
|
5635
|
-
destroy() {
|
|
5636
|
-
stopObserving();
|
|
5637
|
-
resizes.disconnect();
|
|
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
|
-
}
|
|
5645
|
-
};
|
|
5646
|
-
}
|
|
5647
|
-
//#endregion
|
|
5648
6632
|
//#region src/dom/viewer-engine.ts
|
|
5649
6633
|
function mountViewer(opts) {
|
|
5650
6634
|
const { container: el, document: doc } = opts;
|
|
@@ -5657,7 +6641,15 @@ function mountViewer(opts) {
|
|
|
5657
6641
|
let stopKeys = () => void 0;
|
|
5658
6642
|
let stopActive = () => void 0;
|
|
5659
6643
|
let destroyed = false;
|
|
5660
|
-
const
|
|
6644
|
+
const plugins = opts.plugins;
|
|
6645
|
+
const hostTools = [...opts.tools ?? [], ...plugins?.tools ?? []];
|
|
6646
|
+
/** Plugin layers follow the host for the engine's life, across rebuilds. */
|
|
6647
|
+
const layers = plugins && createPluginLayers(plugins);
|
|
6648
|
+
const decorate = plugins ? (a) => plugins.decorate(a) : (a) => opts.decorateAnnotation?.(a);
|
|
6649
|
+
/** A plugin's `ctx.redecorate()` reaches every page this engine has mounted. */
|
|
6650
|
+
const stopRedecorate = plugins?.watchRedecorate(() => {
|
|
6651
|
+
for (const mount of pages.values()) mount.overlay.redecorate();
|
|
6652
|
+
});
|
|
5661
6653
|
/** Option values outlive rebuilds: every controller shares this registry. */
|
|
5662
6654
|
const toolOptions = opts.toolOptions ?? createToolOptions();
|
|
5663
6655
|
/** Without a host style, the engine's own follows the active tool across rebuilds. */
|
|
@@ -5686,7 +6678,8 @@ function mountViewer(opts) {
|
|
|
5686
6678
|
store: doc.annotations,
|
|
5687
6679
|
onActivate: (id) => opts.onActivate?.(id),
|
|
5688
6680
|
isCustom: () => isCustom,
|
|
5689
|
-
decorate
|
|
6681
|
+
decorate,
|
|
6682
|
+
...layers ? { layers } : {},
|
|
5690
6683
|
onError: (cause, layer) => {
|
|
5691
6684
|
failed.add(layer);
|
|
5692
6685
|
if (error) return;
|
|
@@ -5871,6 +6864,8 @@ function mountViewer(opts) {
|
|
|
5871
6864
|
if (destroyed) return;
|
|
5872
6865
|
destroyed = true;
|
|
5873
6866
|
teardown();
|
|
6867
|
+
layers?.destroy();
|
|
6868
|
+
stopRedecorate?.();
|
|
5874
6869
|
}
|
|
5875
6870
|
};
|
|
5876
6871
|
}
|
|
@@ -6043,8 +7038,14 @@ function createThreadForm(n, actions) {
|
|
|
6043
7038
|
}
|
|
6044
7039
|
//#endregion
|
|
6045
7040
|
//#region src/dom/viewer/create-viewer.ts
|
|
7041
|
+
const noop = () => void 0;
|
|
7042
|
+
const passThrough = {
|
|
7043
|
+
batch: (fn) => fn(),
|
|
7044
|
+
track: () => noop,
|
|
7045
|
+
react: () => noop
|
|
7046
|
+
};
|
|
6046
7047
|
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;
|
|
7048
|
+
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, ...view } = opts;
|
|
6048
7049
|
/** One library for the viewer's life, so templates and the menu outlive each document. */
|
|
6049
7050
|
const signatures = createSignatureLibrary(signatureStore ?? createMemorySignatureStore());
|
|
6050
7051
|
/** Tool options persist for the viewer's life; the tool style is per document. */
|
|
@@ -6056,6 +7057,7 @@ function createViewer(el, opts) {
|
|
|
6056
7057
|
const tools = writable(null);
|
|
6057
7058
|
/** The activated annotation: the note thread opens from it. */
|
|
6058
7059
|
const activated = writable(null);
|
|
7060
|
+
const rootEl = writable(null);
|
|
6059
7061
|
const root = h("div", {
|
|
6060
7062
|
"data-ink-root": "",
|
|
6061
7063
|
"data-state": "idle"
|
|
@@ -6074,6 +7076,24 @@ function createViewer(el, opts) {
|
|
|
6074
7076
|
}, scroll);
|
|
6075
7077
|
root.append(viewportRoot);
|
|
6076
7078
|
el.append(root);
|
|
7079
|
+
rootEl.set(scroll);
|
|
7080
|
+
/**
|
|
7081
|
+
* One host for the viewer's life. It reads the history when a document opens, and `current`
|
|
7082
|
+
* is set before it notifies, so that is the new document's. With no document open, a
|
|
7083
|
+
* command's `batch` just runs its function.
|
|
7084
|
+
*/
|
|
7085
|
+
const host = createPluginHost({
|
|
7086
|
+
plugins: plugins ?? [],
|
|
7087
|
+
document: readonly(current),
|
|
7088
|
+
history: () => current.get()?.history ?? passThrough,
|
|
7089
|
+
activated: readonly(activated),
|
|
7090
|
+
setActivated: (id) => {
|
|
7091
|
+
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
7092
|
+
},
|
|
7093
|
+
root: readonly(rootEl),
|
|
7094
|
+
decorate: () => view.decorateAnnotation,
|
|
7095
|
+
viewport: () => viewport.get()
|
|
7096
|
+
});
|
|
6077
7097
|
const focusReturn = createFocusReturn({
|
|
6078
7098
|
root: () => root,
|
|
6079
7099
|
activated: () => activated.get()
|
|
@@ -6123,6 +7143,7 @@ function createViewer(el, opts) {
|
|
|
6123
7143
|
};
|
|
6124
7144
|
const mounted = mountViewer({
|
|
6125
7145
|
...view,
|
|
7146
|
+
plugins: host,
|
|
6126
7147
|
container: scroll,
|
|
6127
7148
|
document: doc,
|
|
6128
7149
|
toolOptions,
|
|
@@ -6219,7 +7240,7 @@ function createViewer(el, opts) {
|
|
|
6219
7240
|
});
|
|
6220
7241
|
}
|
|
6221
7242
|
open(initial);
|
|
6222
|
-
|
|
7243
|
+
const viewer = {
|
|
6223
7244
|
element: root,
|
|
6224
7245
|
status: readonly(status),
|
|
6225
7246
|
document: readonly(current),
|
|
@@ -6227,18 +7248,33 @@ function createViewer(el, opts) {
|
|
|
6227
7248
|
viewport: readonly(viewport),
|
|
6228
7249
|
tools: readonly(tools),
|
|
6229
7250
|
activated: readonly(activated),
|
|
6230
|
-
setActivated(id) {
|
|
7251
|
+
setActivated: (id) => {
|
|
6231
7252
|
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
6232
7253
|
},
|
|
7254
|
+
commands: { get: (id) => host.command(id) },
|
|
6233
7255
|
open,
|
|
6234
7256
|
destroy() {
|
|
6235
7257
|
if (destroyed) return;
|
|
6236
7258
|
destroyed = true;
|
|
6237
7259
|
loads++;
|
|
6238
7260
|
close();
|
|
7261
|
+
host.destroy();
|
|
6239
7262
|
root.remove();
|
|
6240
7263
|
}
|
|
6241
7264
|
};
|
|
7265
|
+
toolsByViewer.set(viewer, [...view.tools ?? [], ...host.tools]);
|
|
7266
|
+
commandIdsByViewer.set(viewer, () => [...host.commands.keys()]);
|
|
7267
|
+
return viewer;
|
|
7268
|
+
}
|
|
7269
|
+
const toolsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7270
|
+
/** The host tools a viewer was given (its own and its plugins'), for the controls' labels and icons. */
|
|
7271
|
+
function hostToolsOf(viewer) {
|
|
7272
|
+
return toolsByViewer.get(viewer) ?? [];
|
|
7273
|
+
}
|
|
7274
|
+
const commandIdsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7275
|
+
/** The ids of the commands a viewer's plugins registered, for the error on an unknown one. */
|
|
7276
|
+
function hostCommandIdsOf(viewer) {
|
|
7277
|
+
return commandIdsByViewer.get(viewer)?.() ?? [];
|
|
6242
7278
|
}
|
|
6243
7279
|
//#endregion
|
|
6244
7280
|
//#region src/dom/viewer/controls.ts
|
|
@@ -6350,15 +7386,16 @@ function historyButton(viewer, action) {
|
|
|
6350
7386
|
return b;
|
|
6351
7387
|
}
|
|
6352
7388
|
/**
|
|
6353
|
-
* Sets the tool; pressed while it is the active one. A host tool's button
|
|
6354
|
-
*
|
|
7389
|
+
* Sets the tool; pressed while it is the active one. A host or plugin tool's button takes the
|
|
7390
|
+
* tool's own `label` and `icon`; `opts.label` wins, and without either it is named by the tool id.
|
|
6355
7391
|
*/
|
|
6356
7392
|
function createToolButton(viewer, tool, opts = {}) {
|
|
6357
|
-
const
|
|
7393
|
+
const own = hostToolsOf(viewer).find((t) => t.id === tool);
|
|
7394
|
+
const b = button(opts.label ?? TOOL_LABELS[tool] ?? own?.label ?? tool, {
|
|
6358
7395
|
class: "ink-tool-button",
|
|
6359
7396
|
"data-ink-tool-button": "",
|
|
6360
7397
|
"data-tool": tool
|
|
6361
|
-
}, TOOL_ICONS[tool]);
|
|
7398
|
+
}, TOOL_ICONS[tool] ?? own?.icon);
|
|
6362
7399
|
const show = (active) => {
|
|
6363
7400
|
const on = active === tool;
|
|
6364
7401
|
b.dataset.state = on ? "on" : "off";
|
|
@@ -6368,6 +7405,48 @@ function createToolButton(viewer, tool, opts = {}) {
|
|
|
6368
7405
|
follow(viewer.tools, b, (tools, watch) => watch(tools.active, show), () => show(null));
|
|
6369
7406
|
return b;
|
|
6370
7407
|
}
|
|
7408
|
+
/**
|
|
7409
|
+
* A plugin command as a button with its status text beside it. The button is named by the
|
|
7410
|
+
* command's label and disabled while the command is, except while it runs: then it keeps focus,
|
|
7411
|
+
* 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
|
|
7412
|
+
* plugin registered it logs `InkError('UnknownCommand')` to the console and returns the same
|
|
7413
|
+
* markup disabled, `aria-hidden` and unnamed.
|
|
7414
|
+
*/
|
|
7415
|
+
function createCommandButton(viewer, id) {
|
|
7416
|
+
const command = viewer.commands.get(id);
|
|
7417
|
+
const status = h("span", {
|
|
7418
|
+
class: "ink-command-status",
|
|
7419
|
+
role: "status"
|
|
7420
|
+
});
|
|
7421
|
+
if (!command) {
|
|
7422
|
+
console.error(unknownCommand(id, hostCommandIdsOf(viewer)));
|
|
7423
|
+
const pending = h("button", {
|
|
7424
|
+
type: "button",
|
|
7425
|
+
"data-command": id,
|
|
7426
|
+
"aria-hidden": "true"
|
|
7427
|
+
});
|
|
7428
|
+
pending.disabled = true;
|
|
7429
|
+
return h("div", { class: "ink-command" }, pending, status);
|
|
7430
|
+
}
|
|
7431
|
+
const b = button(command.label, { "data-command": id }, command.icon);
|
|
7432
|
+
if (!command.icon) b.textContent = command.label;
|
|
7433
|
+
b.addEventListener("click", () => void command.run());
|
|
7434
|
+
const sync = () => {
|
|
7435
|
+
const busy = command.busy.get();
|
|
7436
|
+
b.disabled = !command.enabled.get() && !busy;
|
|
7437
|
+
b.setAttribute("aria-busy", String(busy));
|
|
7438
|
+
if (busy) b.setAttribute("aria-disabled", "true");
|
|
7439
|
+
else b.removeAttribute("aria-disabled");
|
|
7440
|
+
};
|
|
7441
|
+
command.enabled.subscribe(sync);
|
|
7442
|
+
command.busy.subscribe(sync);
|
|
7443
|
+
sync();
|
|
7444
|
+
command.status.subscribe((text) => {
|
|
7445
|
+
status.textContent = text;
|
|
7446
|
+
});
|
|
7447
|
+
status.textContent = command.status.get();
|
|
7448
|
+
return h("div", { class: "ink-command" }, b, status);
|
|
7449
|
+
}
|
|
6371
7450
|
function createZoomIn(viewer) {
|
|
6372
7451
|
const b = button("Zoom in", {
|
|
6373
7452
|
class: "ink-zoom-in",
|
|
@@ -8110,6 +9189,6 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
8110
9189
|
return root;
|
|
8111
9190
|
}
|
|
8112
9191
|
//#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 };
|
|
9192
|
+
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, createFocusReturn, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPluginHost, createPluginLayers, 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, definePlugin, 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, unknownCommand, visibleRangeCss, withHostTools };
|
|
8114
9193
|
|
|
8115
9194
|
//# sourceMappingURL=dom.mjs.map
|