@inkbar/core 0.11.0 → 0.12.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 +136 -34
- package/dist/dom.d.mts +290 -16
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +1538 -85
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{sign-DZVDJhqT.mjs → sign-BSP7H3b7.mjs} +1799 -1479
- package/dist/sign-BSP7H3b7.mjs.map +1 -0
- package/dist/{sign-BTbtPHbM.d.mts → sign-UdMGeqRr.d.mts} +161 -13
- package/dist/sign-UdMGeqRr.d.mts.map +1 -0
- package/package.json +3 -3
- package/dist/sign-BTbtPHbM.d.mts.map +0 -1
- package/dist/sign-DZVDJhqT.mjs.map +0 -1
package/dist/dom.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleDashed, Download, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, Printer, RectangleHorizontal, Redo2, Search, Shapes, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
1
|
+
import { $ as withBox, A as threadRoot, C as isPointerOnlyKind, Cn as rectsIntersect, D as isThreadRoot, G as isTextMarkup, Gt as calloutStroke, H as hasCornerHandles, J as movedTip, Jt as withCalloutBounds, K as keepsAspect, Kt as freeTextBox, N as createToolStyle, On as InkError, Q as translated, S as defaultEnds, T as isAbandonedNote, U as hasRectGeometry, V as hasCallout, W as isBubble, X as resizedRect, Y as resizedBox, Yt as cssVariableDefaults, Z as scaled, _ as DEFAULT_CORNER_RADIUS, a as createInkTemplate, an as mergeLineRects, b as SHAPE_KINDS, bn as rectFromPoints, c as createSignatureLibrary, cn as readonly, ct as sameValue, d as placeSignature, en as LINE_ENDINGS, et as openDocument, fn as invertMatrix, ft as importedId, g as recogniseStroke, hn as quadContainsPoint, i as createImageTemplate, j as withoutReplies, k as threadOf, kn as isInkError, l as createStorageSignatureStore, ln as writable, lt as safeUrl, m as scopeStorage, on as createRecordStore, p as createMemoryStorage, q as movedEndpoint, qt as isStandardCallout, rn as isKnown, rt as deriveRect, sn as derived, st as isProduction, t as signField, tt as isBlankQuery, un as applyMatrix, v as DEFAULT_SHAPE_OPTIONS, w as shapeInput, x as constrain, xn as rectFromQuad, y as DEFAULT_SHAPE_SIZE, yn as rectContainsPoint } from "./sign-BSP7H3b7.mjs";
|
|
2
|
+
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, ArrowLeft, ArrowRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleDashed, Download, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, Printer, RectangleHorizontal, Redo2, Search, Shapes, 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. */
|
|
@@ -585,6 +585,7 @@ function createViewport(opts) {
|
|
|
585
585
|
const rotation = writable(opts.rotation ?? 0);
|
|
586
586
|
const mode = writable(opts.mode ?? "continuous");
|
|
587
587
|
const currentPage = writable(0);
|
|
588
|
+
const readingLine = writable(0);
|
|
588
589
|
const mounted = writable([]);
|
|
589
590
|
const layout = writable({
|
|
590
591
|
boxes: [],
|
|
@@ -703,6 +704,9 @@ function createViewport(opts) {
|
|
|
703
704
|
if (destroyed) return;
|
|
704
705
|
const l = layout.get();
|
|
705
706
|
const top = container.scrollTop - origin().y;
|
|
707
|
+
const end = container.scrollHeight - container.clientHeight;
|
|
708
|
+
const atEnd = end > 0 && container.scrollTop >= end - 1;
|
|
709
|
+
readingLine.set(top + (atEnd ? container.clientHeight : container.clientHeight / 3));
|
|
706
710
|
const wanted = visibleRangeCss(l, top, container.clientHeight, buffer);
|
|
707
711
|
const wantedSet = new Set(wanted);
|
|
708
712
|
for (const [page, host] of [...hosts]) {
|
|
@@ -725,6 +729,70 @@ function createViewport(opts) {
|
|
|
725
729
|
if (!sameList$1(mounted.get(), wanted)) mounted.set(wanted);
|
|
726
730
|
if (mode.get() === "continuous") currentPage.set(mostVisiblePageCss(l, top, container.clientHeight));
|
|
727
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Absolute container scroll positions that frame `dest`, or `undefined` when its page has no
|
|
734
|
+
* box. No `left` means "leave the horizontal scroll where it is". Shared by
|
|
735
|
+
* `scrollToDestination` and `destinationY` so they cannot disagree.
|
|
736
|
+
*/
|
|
737
|
+
function destinationScroll(dest) {
|
|
738
|
+
const box = boxFor(dest.page);
|
|
739
|
+
if (!box) return void 0;
|
|
740
|
+
const t = transformFor(dest.page);
|
|
741
|
+
const at = origin();
|
|
742
|
+
const pageTop = mode.get() === "single" ? 0 : Math.max(0, at.y + box.top - gap);
|
|
743
|
+
const absTop = (css) => Math.max(0, at.y + box.top + css);
|
|
744
|
+
const absLeft = (css) => Math.max(0, at.x + box.left + css);
|
|
745
|
+
const known = (x, y) => {
|
|
746
|
+
const o = t.toCss({
|
|
747
|
+
x: 0,
|
|
748
|
+
y: 0
|
|
749
|
+
});
|
|
750
|
+
const dx = t.toCss({
|
|
751
|
+
x: 1,
|
|
752
|
+
y: 0
|
|
753
|
+
});
|
|
754
|
+
const dy = t.toCss({
|
|
755
|
+
x: 0,
|
|
756
|
+
y: 1
|
|
757
|
+
});
|
|
758
|
+
const p = t.toCss({
|
|
759
|
+
x: x ?? 0,
|
|
760
|
+
y: y ?? 0
|
|
761
|
+
});
|
|
762
|
+
const r = {};
|
|
763
|
+
if ((x !== void 0 || dx.y === o.y) && (y !== void 0 || dy.y === o.y)) r.top = p.y;
|
|
764
|
+
if ((x !== void 0 || dx.x === o.x) && (y !== void 0 || dy.x === o.x)) r.left = p.x;
|
|
765
|
+
return r;
|
|
766
|
+
};
|
|
767
|
+
switch (dest.fit) {
|
|
768
|
+
case "xyz": {
|
|
769
|
+
const c = known(dest.left, dest.top);
|
|
770
|
+
const r = { top: c.top === void 0 ? pageTop : absTop(c.top) };
|
|
771
|
+
if (c.left !== void 0) r.left = absLeft(c.left);
|
|
772
|
+
return r;
|
|
773
|
+
}
|
|
774
|
+
case "fith":
|
|
775
|
+
case "fitbh": {
|
|
776
|
+
const c = known(void 0, dest.top);
|
|
777
|
+
return { top: c.top === void 0 ? pageTop : absTop(c.top) };
|
|
778
|
+
}
|
|
779
|
+
case "fitv":
|
|
780
|
+
case "fitbv": {
|
|
781
|
+
const c = known(dest.left, void 0);
|
|
782
|
+
const r = { top: pageTop };
|
|
783
|
+
if (c.left !== void 0) r.left = absLeft(c.left);
|
|
784
|
+
return r;
|
|
785
|
+
}
|
|
786
|
+
case "fitr": {
|
|
787
|
+
const css = t.toCssRect(dest.rect);
|
|
788
|
+
return {
|
|
789
|
+
top: Math.max(0, at.y + box.top + css.top - (container.clientHeight - css.height) / 2),
|
|
790
|
+
left: Math.max(0, at.x + box.left + css.left - (container.clientWidth - css.width) / 2)
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
default: return { top: pageTop };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
728
796
|
let scrollFrame = 0;
|
|
729
797
|
const onScroll = () => {
|
|
730
798
|
if (scrollFrame) return;
|
|
@@ -954,6 +1022,27 @@ function createViewport(opts) {
|
|
|
954
1022
|
if (to.top !== void 0 || to.left !== void 0) container.scrollTo(to);
|
|
955
1023
|
sync();
|
|
956
1024
|
},
|
|
1025
|
+
scrollToDestination(dest, o) {
|
|
1026
|
+
if (!Number.isInteger(dest.page) || dest.page < 0 || dest.page >= doc.pages.length) return;
|
|
1027
|
+
if (mode.get() === "single" && currentPage.get() !== dest.page) {
|
|
1028
|
+
currentPage.set(dest.page);
|
|
1029
|
+
relayout();
|
|
1030
|
+
}
|
|
1031
|
+
const to = destinationScroll(dest);
|
|
1032
|
+
if (!to) return;
|
|
1033
|
+
const scroll = {
|
|
1034
|
+
top: to.top,
|
|
1035
|
+
behavior: o?.behavior ?? "auto"
|
|
1036
|
+
};
|
|
1037
|
+
if (to.left !== void 0) scroll.left = to.left;
|
|
1038
|
+
container.scrollTo(scroll);
|
|
1039
|
+
sync();
|
|
1040
|
+
},
|
|
1041
|
+
destinationY(dest) {
|
|
1042
|
+
const to = destinationScroll(dest);
|
|
1043
|
+
return to ? to.top - origin().y : void 0;
|
|
1044
|
+
},
|
|
1045
|
+
readingLine: readonly(readingLine),
|
|
957
1046
|
transformFor,
|
|
958
1047
|
hostFor: (page) => hosts.get(page),
|
|
959
1048
|
pageAtCss,
|
|
@@ -1145,9 +1234,9 @@ function attachHistoryKeys(el, history) {
|
|
|
1145
1234
|
return () => el.removeEventListener("keydown", onKeydown);
|
|
1146
1235
|
}
|
|
1147
1236
|
/**
|
|
1148
|
-
* Keeps focus in the viewer when a focused Undo or
|
|
1149
|
-
* (it undid or
|
|
1150
|
-
* reach of the
|
|
1237
|
+
* Keeps focus in the viewer when a focused Undo, Redo, Back or Forward button inside `el` turns
|
|
1238
|
+
* disabled (it undid, redid or went back over the last step), which would otherwise drop focus
|
|
1239
|
+
* to `<body>`, out of reach of the viewer's keys. Focus moves to `scroll()`, the viewer's scroll element,
|
|
1151
1240
|
* without scrolling. Returns a function that removes the listeners.
|
|
1152
1241
|
*
|
|
1153
1242
|
* Chrome and Firefox blur the button as it is disabled: its `focusout` comes while the
|
|
@@ -1156,7 +1245,7 @@ function attachHistoryKeys(el, history) {
|
|
|
1156
1245
|
* `focusout` (a click on content that takes no focus) is left alone.
|
|
1157
1246
|
*/
|
|
1158
1247
|
function keepHistoryFocus(el, scroll) {
|
|
1159
|
-
const isHistoryButton = (node) => node instanceof HTMLButtonElement && node.disabled && node.matches("[data-ink-undo], [data-ink-redo]");
|
|
1248
|
+
const isHistoryButton = (node) => node instanceof HTMLButtonElement && node.disabled && node.matches("[data-ink-undo], [data-ink-redo], [data-ink-back], [data-ink-forward]");
|
|
1160
1249
|
const toScroll = () => {
|
|
1161
1250
|
const target = scroll();
|
|
1162
1251
|
if (!target?.isConnected) return;
|
|
@@ -4171,6 +4260,7 @@ function createPluginHost(opts) {
|
|
|
4171
4260
|
if (seen.has(plugin.id)) throw new InkError("DuplicatePlugin", `A plugin with id ${plugin.id} is registered twice`, { id: plugin.id });
|
|
4172
4261
|
seen.add(plugin.id);
|
|
4173
4262
|
}
|
|
4263
|
+
const storage = opts.storage ?? createMemoryStorage();
|
|
4174
4264
|
const registry = createCommandRegistry({ document: opts.document });
|
|
4175
4265
|
const tools = [];
|
|
4176
4266
|
const decorators = [];
|
|
@@ -4290,6 +4380,7 @@ function createPluginHost(opts) {
|
|
|
4290
4380
|
opts.viewport?.()?.scrollToRect(page, rect, scrollOpts);
|
|
4291
4381
|
},
|
|
4292
4382
|
search: opts.search ?? inertSearch,
|
|
4383
|
+
storage: scopeStorage(storage, `plugin.${plugin.id}`),
|
|
4293
4384
|
onPress: listenForPress("press"),
|
|
4294
4385
|
onLongPress: listenForPress("long-press")
|
|
4295
4386
|
};
|
|
@@ -6157,8 +6248,18 @@ function createToolController$1(opts) {
|
|
|
6157
6248
|
let madeEdit;
|
|
6158
6249
|
/** The press now under way went to `madeEdit`, not the tool. */
|
|
6159
6250
|
let editingMade = false;
|
|
6251
|
+
/** The root's cursor is the pointer for a link under it. */
|
|
6252
|
+
let overLink = false;
|
|
6253
|
+
/** The link last reported to `onLinkHover`. */
|
|
6254
|
+
let hovered;
|
|
6255
|
+
/** The press now under way had a modifier key down, so it does not follow a link. */
|
|
6256
|
+
let pressModified = false;
|
|
6160
6257
|
const presses = createPressTracker({
|
|
6161
|
-
onPress: (press) =>
|
|
6258
|
+
onPress: (press) => {
|
|
6259
|
+
const link = !pressModified && opts.onLink && opts.linkAt?.(press.page, press.point);
|
|
6260
|
+
if (link) opts.onLink(link);
|
|
6261
|
+
else opts.onPress?.("press", press);
|
|
6262
|
+
},
|
|
6162
6263
|
onLongPress: (press) => opts.onPress?.("long-press", press)
|
|
6163
6264
|
});
|
|
6164
6265
|
function clearMade() {
|
|
@@ -6207,6 +6308,8 @@ function createToolController$1(opts) {
|
|
|
6207
6308
|
const selected = store.selection.get();
|
|
6208
6309
|
if (!made.some((id) => selected.includes(id))) set("select-text");
|
|
6209
6310
|
}
|
|
6311
|
+
/** True under the tools whose still presses are reported and may follow a link. */
|
|
6312
|
+
const selecting = () => current?.id === "select-text" || current?.id === "select";
|
|
6210
6313
|
/** True for events from a pointer other than the one whose press is live. */
|
|
6211
6314
|
const foreign = (e) => pressed !== void 0 && pressed !== e.pointerId;
|
|
6212
6315
|
const readFirst = (e) => {
|
|
@@ -6233,7 +6336,7 @@ function createToolController$1(opts) {
|
|
|
6233
6336
|
const target = e.target instanceof Element ? e.target : null;
|
|
6234
6337
|
const annotationId = target?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
|
|
6235
6338
|
const hit = hitFor(ctx, e);
|
|
6236
|
-
const reportable = (
|
|
6339
|
+
const reportable = selecting() && !onAnnotation(target) && !pressDismisses && !closedEditor(e) && hit !== void 0;
|
|
6237
6340
|
presses.cancel();
|
|
6238
6341
|
if (current?.kind === "one-shot") {
|
|
6239
6342
|
if (pressFoundMade && annotationId === void 0 && !onScrollBar(e, ctx.root)) {
|
|
@@ -6253,6 +6356,7 @@ function createToolController$1(opts) {
|
|
|
6253
6356
|
}
|
|
6254
6357
|
pressed = e.pointerId;
|
|
6255
6358
|
if (current?.onPointerDown?.(e, hit, annotationId) === true) capture(e.pointerId);
|
|
6359
|
+
pressModified = e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
|
|
6256
6360
|
if (reportable && pressed === e.pointerId) presses.down(e, {
|
|
6257
6361
|
page: hit.page,
|
|
6258
6362
|
point: hit.user,
|
|
@@ -6272,7 +6376,29 @@ function createToolController$1(opts) {
|
|
|
6272
6376
|
return;
|
|
6273
6377
|
}
|
|
6274
6378
|
current?.onPointerMove?.(e, hitFor(ctx, e));
|
|
6275
|
-
|
|
6379
|
+
if (pressed === void 0) hoverLink(e);
|
|
6380
|
+
};
|
|
6381
|
+
/** Shows the pointer cursor over a link a press would follow (see the top of the file). */
|
|
6382
|
+
function hoverLink(e) {
|
|
6383
|
+
const link = selecting() && !onAnnotation(e.target) ? linkUnder(e) : void 0;
|
|
6384
|
+
hover(link);
|
|
6385
|
+
const over = link !== void 0;
|
|
6386
|
+
if (over === overLink || !current) return;
|
|
6387
|
+
overLink = over;
|
|
6388
|
+
ctx.root.style.cursor = over ? "pointer" : current.cursor;
|
|
6389
|
+
}
|
|
6390
|
+
/** Reports `link` as hovered when it is not already. */
|
|
6391
|
+
function hover(link) {
|
|
6392
|
+
if (link?.id === hovered?.id && link?.page === hovered?.page) return;
|
|
6393
|
+
hovered = link;
|
|
6394
|
+
opts.onLinkHover?.(link);
|
|
6395
|
+
}
|
|
6396
|
+
const onLeave = () => hover(void 0);
|
|
6397
|
+
function linkUnder(e) {
|
|
6398
|
+
if (!opts.linkAt || inCustomContent(e.target)) return void 0;
|
|
6399
|
+
const hit = hitFor(ctx, e);
|
|
6400
|
+
return hit && opts.linkAt(hit.page, hit.user);
|
|
6401
|
+
}
|
|
6276
6402
|
const onUp = (e) => {
|
|
6277
6403
|
if (!e.isPrimary || e.button !== 0 || foreign(e)) return;
|
|
6278
6404
|
if (pressed === e.pointerId) pressed = void 0;
|
|
@@ -6353,6 +6479,7 @@ function createToolController$1(opts) {
|
|
|
6353
6479
|
doc.addEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
6354
6480
|
ctx.root.addEventListener("pointerdown", onDown, { capture: true });
|
|
6355
6481
|
ctx.root.addEventListener("pointermove", onMove);
|
|
6482
|
+
ctx.root.addEventListener("pointerleave", onLeave);
|
|
6356
6483
|
ctx.root.addEventListener("pointerup", onUp);
|
|
6357
6484
|
ctx.root.addEventListener("pointercancel", onCancel);
|
|
6358
6485
|
ctx.root.addEventListener("lostpointercapture", onCancel);
|
|
@@ -6383,6 +6510,8 @@ function createToolController$1(opts) {
|
|
|
6383
6510
|
next.activate(ctx);
|
|
6384
6511
|
ctx.root.dataset.inkTool = id;
|
|
6385
6512
|
ctx.root.style.cursor = next.cursor;
|
|
6513
|
+
overLink = false;
|
|
6514
|
+
hover(void 0);
|
|
6386
6515
|
applyClaim();
|
|
6387
6516
|
active.set(id);
|
|
6388
6517
|
}
|
|
@@ -6426,6 +6555,8 @@ function createToolController$1(opts) {
|
|
|
6426
6555
|
doc.removeEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
6427
6556
|
ctx.root.removeEventListener("pointerdown", onDown, { capture: true });
|
|
6428
6557
|
ctx.root.removeEventListener("pointermove", onMove);
|
|
6558
|
+
ctx.root.removeEventListener("pointerleave", onLeave);
|
|
6559
|
+
hover(void 0);
|
|
6429
6560
|
ctx.root.removeEventListener("pointerup", onUp);
|
|
6430
6561
|
ctx.root.removeEventListener("pointercancel", onCancel);
|
|
6431
6562
|
ctx.root.removeEventListener("lostpointercapture", onCancel);
|
|
@@ -6452,6 +6583,11 @@ function isViewportEnter(e, root) {
|
|
|
6452
6583
|
if (target.matches(".ink-page[data-page]")) return true;
|
|
6453
6584
|
return target.closest("[data-ink-text-layer], [data-ink-canvas-layer]") !== null;
|
|
6454
6585
|
}
|
|
6586
|
+
/** True for a target on an annotation, a resize handle or a host's own link. */
|
|
6587
|
+
function onAnnotation(target) {
|
|
6588
|
+
if (!(target instanceof Element)) return false;
|
|
6589
|
+
return target.closest("[data-annotation], a[href], [data-handle]") !== null;
|
|
6590
|
+
}
|
|
6455
6591
|
function pointerTypeOf(e) {
|
|
6456
6592
|
return e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse";
|
|
6457
6593
|
}
|
|
@@ -7623,44 +7759,55 @@ function annotsOrder(items, widget) {
|
|
|
7623
7759
|
})).sort((a, b) => a.at - b.at || a.i - b.i).map(({ item }) => item);
|
|
7624
7760
|
}
|
|
7625
7761
|
/**
|
|
7626
|
-
* One page's
|
|
7627
|
-
*
|
|
7628
|
-
*
|
|
7629
|
-
*
|
|
7630
|
-
*
|
|
7762
|
+
* One page's stops in Tab order: `R` reads rows top to bottom and each row left to right, `C`
|
|
7763
|
+
* reads columns left to right and each column top to bottom, and `S` follows `structure` (the
|
|
7764
|
+
* page's stop ids in structure-tree order), with stops the tree leaves out after the rest in
|
|
7765
|
+
* `/Annots` order. `W` puts widgets first, each group in `/Annots` order; `A` or no `/Tabs`
|
|
7766
|
+
* keep `/Annots` order.
|
|
7631
7767
|
*/
|
|
7632
|
-
function
|
|
7633
|
-
const listed =
|
|
7634
|
-
if (
|
|
7768
|
+
function orderStops(items, tabs, structure = []) {
|
|
7769
|
+
const listed = [...items].sort((a, b) => a.index - b.index);
|
|
7770
|
+
if (tabs === "S") {
|
|
7635
7771
|
const at = new Map(structure.map((id, i) => [id, i]));
|
|
7636
|
-
return listed.map((
|
|
7637
|
-
id:
|
|
7772
|
+
return listed.map((item, i) => ({
|
|
7773
|
+
id: item.id,
|
|
7638
7774
|
i,
|
|
7639
|
-
at: at.get(
|
|
7640
|
-
})).sort((a, b) => a.at - b.at || a.i - b.i).map((
|
|
7775
|
+
at: at.get(item.id) ?? Infinity
|
|
7776
|
+
})).sort((a, b) => a.at - b.at || a.i - b.i).map((item) => item.id);
|
|
7641
7777
|
}
|
|
7642
|
-
if (
|
|
7778
|
+
if (tabs === "W") return [...listed.filter((i) => i.widget), ...listed.filter((i) => !i.widget)].map((i) => i.id);
|
|
7779
|
+
if (tabs !== "R" && tabs !== "C") return listed.map((i) => i.id);
|
|
7780
|
+
return (tabs === "R" ? banded(listed, (b) => b.top, (b) => b.height, (b) => b.left) : banded(listed, (b) => b.left, (b) => b.width, (b) => b.top)).map((p) => p.id);
|
|
7781
|
+
}
|
|
7782
|
+
/**
|
|
7783
|
+
* One page's widget and link ids in Tab order (`orderStops`), so fields and links share one
|
|
7784
|
+
* reading order. A link is placed by its first quad, the line it starts on. Positions are taken
|
|
7785
|
+
* as the page displays, after its `/Rotate`; without `info` the page is read unturned.
|
|
7786
|
+
* `structure` holds the page's annotation ids as PDF.js reports them, which link ids wrap.
|
|
7787
|
+
*/
|
|
7788
|
+
function pageStopOrder(widgets, links, order, info, page = 0, structure = []) {
|
|
7643
7789
|
const transform = createPageTransform(info ?? unturned(page), 1);
|
|
7644
|
-
const
|
|
7790
|
+
const listed = annotsOrder(widgets, (w) => w);
|
|
7791
|
+
const items = [...listed.map((w, i) => ({
|
|
7645
7792
|
id: w.id,
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7793
|
+
box: transform.toCssRect(w.rect),
|
|
7794
|
+
index: w.order ?? i,
|
|
7795
|
+
widget: true
|
|
7796
|
+
})), ...links.map((l, i) => ({
|
|
7797
|
+
id: l.id,
|
|
7798
|
+
box: transform.toCssRect(l.quads?.[0] ? rectFromQuad(l.quads[0]) : l.rect),
|
|
7799
|
+
index: l.order ?? listed.length + i
|
|
7800
|
+
}))];
|
|
7801
|
+
const prefix = importedId(page, "");
|
|
7802
|
+
const ids = new Set(links.map((l) => l.id));
|
|
7803
|
+
return orderStops(items, order, structure.map((id) => ids.has(prefix + id) ? prefix + id : id));
|
|
7650
7804
|
}
|
|
7651
7805
|
/**
|
|
7652
|
-
*
|
|
7653
|
-
*
|
|
7654
|
-
* unturned.
|
|
7806
|
+
* One page's widget ids in Tab order (`orderStops`). Positions are taken as the page displays,
|
|
7807
|
+
* after its `/Rotate`; without `info` the page is read unturned.
|
|
7655
7808
|
*/
|
|
7656
|
-
function
|
|
7657
|
-
|
|
7658
|
-
for (const field of fields) for (const w of field.widgets) {
|
|
7659
|
-
const list = byPage.get(w.page) ?? [];
|
|
7660
|
-
list.push(w);
|
|
7661
|
-
byPage.set(w.page, list);
|
|
7662
|
-
}
|
|
7663
|
-
return [...byPage.keys()].sort((a, b) => a - b).flatMap((page) => pageTabOrder(byPage.get(page), tabs.get(page), pages[page], page, structure.get(page)));
|
|
7809
|
+
function pageTabOrder(widgets, order, info, page = 0, structure = []) {
|
|
7810
|
+
return pageStopOrder(widgets, [], order, info, page, structure);
|
|
7664
7811
|
}
|
|
7665
7812
|
//#endregion
|
|
7666
7813
|
//#region src/dom/palette.ts
|
|
@@ -8640,13 +8787,195 @@ function createFormLayer(opts) {
|
|
|
8640
8787
|
return layer;
|
|
8641
8788
|
}
|
|
8642
8789
|
//#endregion
|
|
8790
|
+
//#region src/dom/link-layer.ts
|
|
8791
|
+
const UNSUPPORTED = "This link is not supported";
|
|
8792
|
+
/** The page a Named action goes to from the link's own page, as a reader on it would expect. */
|
|
8793
|
+
function namedPage$1(action, page, count) {
|
|
8794
|
+
const to = action === "NextPage" ? page + 1 : action === "PrevPage" ? page - 1 : action === "FirstPage" ? 0 : count - 1;
|
|
8795
|
+
return Math.min(Math.max(to, 0), count - 1);
|
|
8796
|
+
}
|
|
8797
|
+
/** Draws `page`'s links (`document.links(page)`) over the page in `host`. */
|
|
8798
|
+
function createLinkLayer(opts) {
|
|
8799
|
+
const { host, page, document: doc } = opts;
|
|
8800
|
+
const ownerDoc = host.ownerDocument;
|
|
8801
|
+
let transform = opts.transform;
|
|
8802
|
+
let destroyed = false;
|
|
8803
|
+
const element = ownerDoc.createElement("div");
|
|
8804
|
+
element.className = "ink-link-layer";
|
|
8805
|
+
element.dataset.inkLinkLayer = "";
|
|
8806
|
+
element.style.cssText = "position:absolute;inset:0;pointer-events:none;";
|
|
8807
|
+
host.append(element);
|
|
8808
|
+
const name = (e) => {
|
|
8809
|
+
if (e.text) return e.text;
|
|
8810
|
+
const target = e.link.target;
|
|
8811
|
+
if (target.kind === "url") return target.url;
|
|
8812
|
+
if (target.kind === "unsupported" || e.dead) return UNSUPPORTED;
|
|
8813
|
+
return e.to === void 0 ? "Go to link" : `Go to page ${e.to + 1}`;
|
|
8814
|
+
};
|
|
8815
|
+
const place = (el, rect) => {
|
|
8816
|
+
const box = transform.toCssRect(rect);
|
|
8817
|
+
el.style.left = `${box.left}px`;
|
|
8818
|
+
el.style.top = `${box.top}px`;
|
|
8819
|
+
el.style.width = `${box.width}px`;
|
|
8820
|
+
el.style.height = `${box.height}px`;
|
|
8821
|
+
};
|
|
8822
|
+
const follow = (e) => {
|
|
8823
|
+
if (!destroyed && !e.dead && e.link.target.kind !== "unsupported") opts.follow(e.link.target, { from: e.link.page });
|
|
8824
|
+
};
|
|
8825
|
+
/** Marks every piece of a wrapped link while its stop has a visible focus ring. */
|
|
8826
|
+
const markFocus = (e, on) => {
|
|
8827
|
+
for (const { el } of e.pieces) el.toggleAttribute("data-ink-focused", on);
|
|
8828
|
+
};
|
|
8829
|
+
/** Makes a link's stop inert: not followed, not a Tab stop, and titled with why. */
|
|
8830
|
+
const disable = (el) => {
|
|
8831
|
+
el.title = UNSUPPORTED;
|
|
8832
|
+
el.setAttribute("aria-disabled", "true");
|
|
8833
|
+
el.tabIndex = -1;
|
|
8834
|
+
};
|
|
8835
|
+
const readText = (e) => e.pieces.map((p) => opts.textUnder(p.rect).trim()).filter(Boolean).join(" ");
|
|
8836
|
+
const entries = doc.links(page).map((link) => {
|
|
8837
|
+
const target = link.target;
|
|
8838
|
+
const rects = link.quads?.length ? link.quads.map(rectFromQuad) : [link.rect];
|
|
8839
|
+
const e = {
|
|
8840
|
+
link,
|
|
8841
|
+
pieces: [],
|
|
8842
|
+
text: "",
|
|
8843
|
+
dead: false,
|
|
8844
|
+
to: target.kind === "resolved" ? target.dest.page : target.kind === "named" ? namedPage$1(target.action, page, doc.pages.length) : void 0
|
|
8845
|
+
};
|
|
8846
|
+
rects.forEach((rect, i) => {
|
|
8847
|
+
const stop = i === 0;
|
|
8848
|
+
let el;
|
|
8849
|
+
if (stop && target.kind === "url") {
|
|
8850
|
+
const a = ownerDoc.createElement("a");
|
|
8851
|
+
a.href = target.url;
|
|
8852
|
+
el = a;
|
|
8853
|
+
} else {
|
|
8854
|
+
el = ownerDoc.createElement("span");
|
|
8855
|
+
if (!stop) el.setAttribute("aria-hidden", "true");
|
|
8856
|
+
else {
|
|
8857
|
+
el.setAttribute("role", "link");
|
|
8858
|
+
if (target.kind === "unsupported") disable(el);
|
|
8859
|
+
else el.tabIndex = 0;
|
|
8860
|
+
}
|
|
8861
|
+
}
|
|
8862
|
+
el.className = "ink-link";
|
|
8863
|
+
el.dataset.inkLink = link.id;
|
|
8864
|
+
el.style.cssText = "position:absolute;pointer-events:none;";
|
|
8865
|
+
place(el, rect);
|
|
8866
|
+
element.append(el);
|
|
8867
|
+
e.pieces.push({
|
|
8868
|
+
el,
|
|
8869
|
+
rect
|
|
8870
|
+
});
|
|
8871
|
+
});
|
|
8872
|
+
const stop = e.pieces[0].el;
|
|
8873
|
+
stop.setAttribute("aria-label", name(e));
|
|
8874
|
+
stop.addEventListener("click", (ev) => {
|
|
8875
|
+
ev.preventDefault();
|
|
8876
|
+
follow(e);
|
|
8877
|
+
});
|
|
8878
|
+
if (stop.tagName !== "A") stop.addEventListener("keydown", (ev) => {
|
|
8879
|
+
if (ev.key !== "Enter" || ev.defaultPrevented) return;
|
|
8880
|
+
ev.preventDefault();
|
|
8881
|
+
follow(e);
|
|
8882
|
+
});
|
|
8883
|
+
stop.addEventListener("focus", () => markFocus(e, stop.matches(":focus-visible")));
|
|
8884
|
+
stop.addEventListener("blur", () => markFocus(e, false));
|
|
8885
|
+
return e;
|
|
8886
|
+
});
|
|
8887
|
+
const relabel = () => {
|
|
8888
|
+
if (destroyed) return;
|
|
8889
|
+
for (const e of entries) {
|
|
8890
|
+
e.text = readText(e);
|
|
8891
|
+
e.pieces[0].el.setAttribute("aria-label", name(e));
|
|
8892
|
+
}
|
|
8893
|
+
};
|
|
8894
|
+
relabel();
|
|
8895
|
+
for (const e of entries) {
|
|
8896
|
+
const target = e.link.target;
|
|
8897
|
+
if (target.kind !== "dest") continue;
|
|
8898
|
+
const stop = e.pieces[0].el;
|
|
8899
|
+
const settle = (page) => {
|
|
8900
|
+
if (destroyed) return;
|
|
8901
|
+
if (page === void 0) {
|
|
8902
|
+
e.dead = true;
|
|
8903
|
+
disable(stop);
|
|
8904
|
+
} else e.to = page;
|
|
8905
|
+
stop.setAttribute("aria-label", name(e));
|
|
8906
|
+
};
|
|
8907
|
+
doc.resolveDestination(target.raw).then((dest) => settle(dest?.page), () => settle(void 0));
|
|
8908
|
+
}
|
|
8909
|
+
return {
|
|
8910
|
+
element,
|
|
8911
|
+
update(next) {
|
|
8912
|
+
if (destroyed) return;
|
|
8913
|
+
transform = next;
|
|
8914
|
+
for (const e of entries) for (const p of e.pieces) place(p.el, p.rect);
|
|
8915
|
+
},
|
|
8916
|
+
tabStops() {
|
|
8917
|
+
if (destroyed) return [];
|
|
8918
|
+
return entries.flatMap((e) => {
|
|
8919
|
+
const stop = e.pieces[0].el;
|
|
8920
|
+
return stop.tabIndex >= 0 ? [stop] : [];
|
|
8921
|
+
});
|
|
8922
|
+
},
|
|
8923
|
+
relabel,
|
|
8924
|
+
destroy() {
|
|
8925
|
+
if (destroyed) return;
|
|
8926
|
+
destroyed = true;
|
|
8927
|
+
element.remove();
|
|
8928
|
+
}
|
|
8929
|
+
};
|
|
8930
|
+
}
|
|
8931
|
+
/** The text runs of `layer` (a text layer), measured once for any number of `textInBox` calls. */
|
|
8932
|
+
function textRuns(layer) {
|
|
8933
|
+
const walker = layer.ownerDocument.createTreeWalker(layer, NodeFilter.SHOW_TEXT);
|
|
8934
|
+
const runs = [];
|
|
8935
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
8936
|
+
const rect = node.parentElement?.getBoundingClientRect();
|
|
8937
|
+
if (rect) runs.push({
|
|
8938
|
+
node,
|
|
8939
|
+
rect
|
|
8940
|
+
});
|
|
8941
|
+
}
|
|
8942
|
+
return runs;
|
|
8943
|
+
}
|
|
8944
|
+
/**
|
|
8945
|
+
* The text of `runs` (from `textRuns`) whose characters sit in `box`, a client-coordinate box.
|
|
8946
|
+
* Runs that lie wholly inside count whole; runs that cross the edge count the characters whose
|
|
8947
|
+
* centre is inside, so a link over one word of a line is named by that word.
|
|
8948
|
+
*/
|
|
8949
|
+
function textInBox(runs, box) {
|
|
8950
|
+
const inside = (x, y) => x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
|
|
8951
|
+
const parts = [];
|
|
8952
|
+
let range;
|
|
8953
|
+
for (const { node: text, rect: run } of runs) {
|
|
8954
|
+
if (run.right < box.left || run.left > box.right || run.bottom < box.top || run.top > box.bottom) continue;
|
|
8955
|
+
let out = "";
|
|
8956
|
+
if (run.left >= box.left && run.right <= box.right && run.top >= box.top && run.bottom <= box.bottom) out = text.data;
|
|
8957
|
+
else {
|
|
8958
|
+
range ??= text.ownerDocument.createRange();
|
|
8959
|
+
for (let i = 0; i < text.data.length; i++) {
|
|
8960
|
+
range.setStart(text, i);
|
|
8961
|
+
range.setEnd(text, i + 1);
|
|
8962
|
+
const c = range.getBoundingClientRect();
|
|
8963
|
+
if (inside(c.left + c.width / 2, c.top + c.height / 2)) out += text.data[i];
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
8966
|
+
if (out.trim()) parts.push(out.trim());
|
|
8967
|
+
}
|
|
8968
|
+
return parts.join(" ").replace(/\s+/g, " ");
|
|
8969
|
+
}
|
|
8970
|
+
//#endregion
|
|
8643
8971
|
//#region src/dom/page-mount.ts
|
|
8644
8972
|
/**
|
|
8645
|
-
* Canvas, text layer, form layer, annotation overlay and any plugin
|
|
8646
|
-
* in that order.
|
|
8973
|
+
* Canvas, text layer, links, search highlights, form layer, annotation overlay and any plugin
|
|
8974
|
+
* layers for one page, stacked in that order.
|
|
8647
8975
|
*/
|
|
8648
8976
|
function createPageMount(opts) {
|
|
8649
|
-
const { document, page, host
|
|
8977
|
+
const { document, page, host } = opts;
|
|
8978
|
+
let transform = opts.transform;
|
|
8650
8979
|
const callbacks = (layer) => ({
|
|
8651
8980
|
...opts.onError ? { onError: (error) => opts.onError?.(error, layer) } : {},
|
|
8652
8981
|
...opts.onRendered ? { onRendered: () => opts.onRendered?.(layer) } : {}
|
|
@@ -8659,13 +8988,48 @@ function createPageMount(opts) {
|
|
|
8659
8988
|
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
8660
8989
|
...callbacks("canvas")
|
|
8661
8990
|
});
|
|
8991
|
+
let named = false;
|
|
8992
|
+
const text = callbacks("text");
|
|
8662
8993
|
const textLayer = createTextLayer({
|
|
8663
8994
|
document,
|
|
8664
8995
|
page,
|
|
8665
8996
|
host,
|
|
8666
8997
|
transform,
|
|
8667
|
-
...
|
|
8998
|
+
...text,
|
|
8999
|
+
onRendered: () => {
|
|
9000
|
+
if (!named) {
|
|
9001
|
+
named = true;
|
|
9002
|
+
links?.relabel();
|
|
9003
|
+
}
|
|
9004
|
+
text.onRendered?.();
|
|
9005
|
+
}
|
|
8668
9006
|
});
|
|
9007
|
+
let measured;
|
|
9008
|
+
const links = document.links(page).length > 0 ? createLinkLayer({
|
|
9009
|
+
host,
|
|
9010
|
+
page,
|
|
9011
|
+
transform,
|
|
9012
|
+
document,
|
|
9013
|
+
follow: (target, o) => opts.followLink?.(target, o),
|
|
9014
|
+
textUnder: (rect) => {
|
|
9015
|
+
if (!measured) {
|
|
9016
|
+
measured = {
|
|
9017
|
+
runs: textRuns(textLayer.element),
|
|
9018
|
+
origin: host.getBoundingClientRect()
|
|
9019
|
+
};
|
|
9020
|
+
queueMicrotask(() => measured = void 0);
|
|
9021
|
+
}
|
|
9022
|
+
const box = transform.toCssRect(rect);
|
|
9023
|
+
const left = measured.origin.left + box.left;
|
|
9024
|
+
const top = measured.origin.top + box.top;
|
|
9025
|
+
return textInBox(measured.runs, {
|
|
9026
|
+
left,
|
|
9027
|
+
top,
|
|
9028
|
+
right: left + box.width,
|
|
9029
|
+
bottom: top + box.height
|
|
9030
|
+
});
|
|
9031
|
+
}
|
|
9032
|
+
}) : void 0;
|
|
8669
9033
|
const marks = opts.highlights?.mount(page, host, transform);
|
|
8670
9034
|
const form = document.form.fields.get().length > 0 ? createFormLayer({
|
|
8671
9035
|
host,
|
|
@@ -8690,14 +9054,34 @@ function createPageMount(opts) {
|
|
|
8690
9054
|
...opts.decorate ? { decorate: opts.decorate } : {}
|
|
8691
9055
|
});
|
|
8692
9056
|
const layers = opts.layers?.mount(page, host, transform);
|
|
9057
|
+
/**
|
|
9058
|
+
* Each stop's place in the page's one order of fields and links, worked out on first use: the
|
|
9059
|
+
* two sit in separate layers, so the order comes from the page, not the DOM.
|
|
9060
|
+
*/
|
|
9061
|
+
let ranks;
|
|
9062
|
+
const rankOf = (stop) => {
|
|
9063
|
+
ranks ??= new Map(pageStopOrder(document.form.fields.get().flatMap((f) => f.widgets.filter((w) => w.page === page)), document.links(page), document.form._tabs.get(page), document.pages[page], page, document.form._structure.get(page)).map((id, i) => [id, i]));
|
|
9064
|
+
const id = stop.dataset.inkLink ?? stop.closest("[data-ink-widget]")?.dataset.inkWidget;
|
|
9065
|
+
return (id === void 0 ? void 0 : ranks.get(id)) ?? Infinity;
|
|
9066
|
+
};
|
|
8693
9067
|
return {
|
|
8694
9068
|
renderer,
|
|
8695
9069
|
textLayer,
|
|
8696
9070
|
overlay,
|
|
8697
9071
|
form,
|
|
9072
|
+
links,
|
|
9073
|
+
tabStops() {
|
|
9074
|
+
const fields = form?.tabStops() ?? [];
|
|
9075
|
+
const linked = links?.tabStops() ?? [];
|
|
9076
|
+
if (fields.length === 0) return linked;
|
|
9077
|
+
if (linked.length === 0) return fields;
|
|
9078
|
+
return [...fields, ...linked].sort((a, b) => rankOf(a) - rankOf(b));
|
|
9079
|
+
},
|
|
8698
9080
|
update(next) {
|
|
9081
|
+
transform = next;
|
|
8699
9082
|
renderer.update(next);
|
|
8700
9083
|
textLayer.update(next);
|
|
9084
|
+
links?.update(next);
|
|
8701
9085
|
overlay.update(next);
|
|
8702
9086
|
form?.update(next);
|
|
8703
9087
|
layers?.update(next);
|
|
@@ -8708,6 +9092,7 @@ function createPageMount(opts) {
|
|
|
8708
9092
|
form?.destroy();
|
|
8709
9093
|
overlay.destroy();
|
|
8710
9094
|
marks?.destroy();
|
|
9095
|
+
links?.destroy();
|
|
8711
9096
|
textLayer.destroy();
|
|
8712
9097
|
renderer.destroy();
|
|
8713
9098
|
}
|
|
@@ -9851,6 +10236,122 @@ function trapTab(e, container) {
|
|
|
9851
10236
|
}
|
|
9852
10237
|
}
|
|
9853
10238
|
//#endregion
|
|
10239
|
+
//#region src/dom/navigation.ts
|
|
10240
|
+
/** The positions kept on each side; past this, the oldest goes. */
|
|
10241
|
+
const LIMIT = 50;
|
|
10242
|
+
/**
|
|
10243
|
+
* Looks `raw` up in `doc` through its cached resolver.
|
|
10244
|
+
* `undefined` for anything unreadable, or when the document is not ready.
|
|
10245
|
+
*/
|
|
10246
|
+
async function resolve(doc, raw) {
|
|
10247
|
+
try {
|
|
10248
|
+
return await doc.resolveDestination(raw);
|
|
10249
|
+
} catch {
|
|
10250
|
+
return;
|
|
10251
|
+
}
|
|
10252
|
+
}
|
|
10253
|
+
/** The page a Named action goes to from `current`, within `count` pages. */
|
|
10254
|
+
function namedPage(action, current, count) {
|
|
10255
|
+
const to = action === "NextPage" ? current + 1 : action === "PrevPage" ? current - 1 : action === "FirstPage" ? 0 : count - 1;
|
|
10256
|
+
return Math.min(Math.max(to, 0), count - 1);
|
|
10257
|
+
}
|
|
10258
|
+
/** Within a point: closer than that, two positions show the same view. */
|
|
10259
|
+
const near = (a, b) => a === void 0 || b === void 0 ? a === b : Math.abs(a - b) <= 1;
|
|
10260
|
+
/** Whether `b` is where `a` was, give or take a point of rounding. */
|
|
10261
|
+
function samePlace(a, b) {
|
|
10262
|
+
if (!b || a.page !== b.page || a.fit !== b.fit) return false;
|
|
10263
|
+
const p = a;
|
|
10264
|
+
const q = b;
|
|
10265
|
+
return near(p.left, q.left) && near(p.top, q.top);
|
|
10266
|
+
}
|
|
10267
|
+
function createNavigation(deps) {
|
|
10268
|
+
const backStack = [];
|
|
10269
|
+
const forwardStack = [];
|
|
10270
|
+
const canGoBack = writable(false);
|
|
10271
|
+
const canGoForward = writable(false);
|
|
10272
|
+
const sync = () => {
|
|
10273
|
+
canGoBack.set(backStack.length > 0);
|
|
10274
|
+
canGoForward.set(forwardStack.length > 0);
|
|
10275
|
+
};
|
|
10276
|
+
const push = (stack, dest) => {
|
|
10277
|
+
stack.push(dest);
|
|
10278
|
+
if (stack.length > LIMIT) stack.shift();
|
|
10279
|
+
};
|
|
10280
|
+
const inRange = (page) => {
|
|
10281
|
+
const count = deps.document()?.pages.length ?? 0;
|
|
10282
|
+
return Number.isInteger(page) && page >= 0 && page < count;
|
|
10283
|
+
};
|
|
10284
|
+
/** Pops `from` and goes there, keeping the current position on `to`. Records no jump. */
|
|
10285
|
+
const step = (from, to) => {
|
|
10286
|
+
const vp = deps.viewport();
|
|
10287
|
+
const dest = from.at(-1);
|
|
10288
|
+
if (!vp || !dest) return;
|
|
10289
|
+
from.pop();
|
|
10290
|
+
const here = deps.here();
|
|
10291
|
+
if (here) push(to, here);
|
|
10292
|
+
vp.scrollToDestination(dest);
|
|
10293
|
+
sync();
|
|
10294
|
+
};
|
|
10295
|
+
const goTo = (dest) => {
|
|
10296
|
+
const vp = deps.viewport();
|
|
10297
|
+
if (!vp || !inRange(dest.page)) return;
|
|
10298
|
+
const here = deps.here();
|
|
10299
|
+
vp.scrollToDestination(dest);
|
|
10300
|
+
if (!here || samePlace(here, deps.here())) return;
|
|
10301
|
+
push(backStack, here);
|
|
10302
|
+
forwardStack.length = 0;
|
|
10303
|
+
sync();
|
|
10304
|
+
};
|
|
10305
|
+
return {
|
|
10306
|
+
canGoBack: readonly(canGoBack),
|
|
10307
|
+
canGoForward: readonly(canGoForward),
|
|
10308
|
+
back: () => step(backStack, forwardStack),
|
|
10309
|
+
forward: () => step(forwardStack, backStack),
|
|
10310
|
+
goTo,
|
|
10311
|
+
async follow(target, opts) {
|
|
10312
|
+
switch (target.kind) {
|
|
10313
|
+
case "resolved":
|
|
10314
|
+
goTo(target.dest);
|
|
10315
|
+
return;
|
|
10316
|
+
case "dest": {
|
|
10317
|
+
const doc = deps.document();
|
|
10318
|
+
if (!doc) return;
|
|
10319
|
+
const dest = await resolve(doc, target.raw);
|
|
10320
|
+
if (dest && deps.document() === doc) goTo(dest);
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
case "named": {
|
|
10324
|
+
const vp = deps.viewport();
|
|
10325
|
+
const count = deps.document()?.pages.length ?? 0;
|
|
10326
|
+
if (!vp || count === 0) return;
|
|
10327
|
+
const current = opts?.from ?? vp.currentPage.get();
|
|
10328
|
+
const page = namedPage(target.action, current, count);
|
|
10329
|
+
if (page !== current) goTo({
|
|
10330
|
+
page,
|
|
10331
|
+
fit: "fit"
|
|
10332
|
+
});
|
|
10333
|
+
return;
|
|
10334
|
+
}
|
|
10335
|
+
case "url": {
|
|
10336
|
+
const url = safeUrl(target.url);
|
|
10337
|
+
if (url) deps.report({
|
|
10338
|
+
kind: "open-url",
|
|
10339
|
+
url,
|
|
10340
|
+
newWindow: target.newWindow
|
|
10341
|
+
});
|
|
10342
|
+
return;
|
|
10343
|
+
}
|
|
10344
|
+
case "unsupported": return;
|
|
10345
|
+
}
|
|
10346
|
+
},
|
|
10347
|
+
clear() {
|
|
10348
|
+
backStack.length = 0;
|
|
10349
|
+
forwardStack.length = 0;
|
|
10350
|
+
sync();
|
|
10351
|
+
}
|
|
10352
|
+
};
|
|
10353
|
+
}
|
|
10354
|
+
//#endregion
|
|
9854
10355
|
//#region src/dom/search/highlights.ts
|
|
9855
10356
|
const NO_HITS = [];
|
|
9856
10357
|
/** One box over the quad: its CSS bounding box, clipped to the quad when it is not upright. */
|
|
@@ -10028,8 +10529,9 @@ function mountViewer(opts) {
|
|
|
10028
10529
|
tool: opts.tool
|
|
10029
10530
|
});
|
|
10030
10531
|
const toolStyle = opts.toolStyle ?? tracker.toolStyle;
|
|
10532
|
+
const storage = opts.storage ?? createMemoryStorage();
|
|
10031
10533
|
const host = opts.signatureLibrary;
|
|
10032
|
-
const own = host ? void 0 : createSignatureLibrary(opts.signatures ??
|
|
10534
|
+
const own = host ? void 0 : createSignatureLibrary(opts.signatures ?? createStorageSignatureStore(storage));
|
|
10033
10535
|
/** The library now: a host getter is read each time, so a swapped library takes effect. */
|
|
10034
10536
|
const signatures = () => typeof host === "function" ? host() : host ?? own;
|
|
10035
10537
|
signatures().refresh();
|
|
@@ -10050,6 +10552,7 @@ function mountViewer(opts) {
|
|
|
10050
10552
|
formHighlight: highlight,
|
|
10051
10553
|
formActive: formsActive,
|
|
10052
10554
|
onSignField: (req) => opts.onSignField?.(req),
|
|
10555
|
+
followLink: (target, o) => void navigation.follow(target, o),
|
|
10053
10556
|
...layers ? { layers } : {},
|
|
10054
10557
|
...highlights ? { highlights } : {},
|
|
10055
10558
|
onError: (cause, layer) => {
|
|
@@ -10077,26 +10580,25 @@ function mountViewer(opts) {
|
|
|
10077
10580
|
return id !== void 0 && FORM_TOOLS.has(id);
|
|
10078
10581
|
};
|
|
10079
10582
|
/**
|
|
10080
|
-
* The first
|
|
10081
|
-
*
|
|
10583
|
+
* The first stop (`last`: the last) in Tab order on `page`, by its user-space rect, among the
|
|
10584
|
+
* fields with a control to stop at (while fields take input) and the links Inkbar follows.
|
|
10585
|
+
* `widgets` holds those fields' widgets by page.
|
|
10082
10586
|
*/
|
|
10083
|
-
function
|
|
10084
|
-
const fields = doc.form.fields.get();
|
|
10085
|
-
const byId = new Map(fields.flatMap((f) => hasControl(f) ? f.widgets.map((w) => [w.id, w]) : []));
|
|
10587
|
+
function tabEdge(page, last, widgets) {
|
|
10086
10588
|
const state = doc.form.widgetState.get();
|
|
10087
|
-
const
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
return
|
|
10589
|
+
const shown = (widgets.get(page) ?? []).filter((w) => !(state[w.id]?.hidden ?? (w.hidden || w.printOnly)));
|
|
10590
|
+
const links = doc.links(page).filter((l) => l.target.kind !== "unsupported");
|
|
10591
|
+
if (shown.length === 0 && links.length === 0) return void 0;
|
|
10592
|
+
const rects = new Map([...shown.map((w) => [w.id, w.rect]), ...links.map((l) => [l.id, l.quads?.[0] ? rectFromQuad(l.quads[0]) : l.rect])]);
|
|
10593
|
+
const order = pageStopOrder(shown, links, doc.form._tabs.get(page), doc.pages[page], page, doc.form._structure.get(page));
|
|
10594
|
+
const id = last ? order.at(-1) : order[0];
|
|
10595
|
+
return id === void 0 ? void 0 : rects.get(id);
|
|
10094
10596
|
}
|
|
10095
10597
|
/** The page a Tab waits for: its stops were not mounted when the view scrolled to it. */
|
|
10096
10598
|
let tabWait;
|
|
10097
|
-
/** The first stop of `page` (`back`: its last), once
|
|
10599
|
+
/** The first stop of `page` (`back`: its last), once the page is mounted. */
|
|
10098
10600
|
const edgeStop = (page, back) => {
|
|
10099
|
-
const stops = pages.get(page)?.
|
|
10601
|
+
const stops = pages.get(page)?.tabStops() ?? [];
|
|
10100
10602
|
return back ? stops.at(-1) : stops[0];
|
|
10101
10603
|
};
|
|
10102
10604
|
/**
|
|
@@ -10131,45 +10633,117 @@ function mountViewer(opts) {
|
|
|
10131
10633
|
tabWait = wait;
|
|
10132
10634
|
}
|
|
10133
10635
|
/**
|
|
10134
|
-
* Tab
|
|
10135
|
-
*
|
|
10136
|
-
*
|
|
10137
|
-
*
|
|
10138
|
-
*
|
|
10636
|
+
* Tab among a page's fields and links, and across pages. On a page with both, Tab follows the
|
|
10637
|
+
* page's one order of them: they sit in two layers, so the browser's own Tab would visit one
|
|
10638
|
+
* layer and then the other. Tab on a page's last stop (Shift+Tab on its first) goes to the
|
|
10639
|
+
* first (last) stop of the next (previous) page that has any, mounting it and scrolling it
|
|
10640
|
+
* into view. A page that has not mounted once the view scrolls to it gets one more try, when
|
|
10641
|
+
* it mounts or on the next frame; after that, focus moves on as the browser's Tab would. Past
|
|
10642
|
+
* the last page's stops, Tab leaves the viewer as usual.
|
|
10139
10643
|
*/
|
|
10140
|
-
function
|
|
10644
|
+
function onPageTab(e) {
|
|
10141
10645
|
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey || e.defaultPrevented) return;
|
|
10142
10646
|
const target = e.target;
|
|
10143
|
-
if (!(target instanceof
|
|
10144
|
-
if (!formsActive()) return;
|
|
10647
|
+
if (!(target instanceof HTMLElement) || !target.closest("[data-ink-form-layer], [data-ink-link-layer]")) return;
|
|
10145
10648
|
const back = e.shiftKey;
|
|
10146
|
-
const from = [...pages].find(([, m]) => m.form?.element.contains(target));
|
|
10649
|
+
const from = [...pages].find(([, m]) => m.form?.element.contains(target) || m.links?.element.contains(target));
|
|
10147
10650
|
if (!from) return;
|
|
10148
10651
|
const [page, mount] = from;
|
|
10149
|
-
|
|
10150
|
-
const
|
|
10151
|
-
|
|
10152
|
-
|
|
10652
|
+
const stops = mount.tabStops();
|
|
10653
|
+
const at = stops.indexOf(target);
|
|
10654
|
+
if (at < 0) return;
|
|
10655
|
+
const within = stops[back ? at - 1 : at + 1];
|
|
10656
|
+
if (within) {
|
|
10657
|
+
if (mount.form && mount.links) {
|
|
10658
|
+
e.preventDefault();
|
|
10659
|
+
within.focus();
|
|
10660
|
+
}
|
|
10661
|
+
return;
|
|
10662
|
+
}
|
|
10663
|
+
const fields = formsActive() ? doc.form.fields.get() : [];
|
|
10664
|
+
const widgets = /* @__PURE__ */ new Map();
|
|
10665
|
+
for (const f of fields) if (hasControl(f)) for (const w of f.widgets) {
|
|
10666
|
+
const list = widgets.get(w.page) ?? [];
|
|
10667
|
+
list.push(w);
|
|
10668
|
+
widgets.set(w.page, list);
|
|
10669
|
+
}
|
|
10670
|
+
for (let next = page + (back ? -1 : 1); next >= 0 && next < doc.pages.length; next += back ? -1 : 1) {
|
|
10671
|
+
const rect = tabEdge(next, back, widgets);
|
|
10672
|
+
if (!rect) continue;
|
|
10673
|
+
viewport.scrollToRect(next, rect);
|
|
10153
10674
|
const to = edgeStop(next, back);
|
|
10154
10675
|
if (to) {
|
|
10155
10676
|
e.preventDefault();
|
|
10156
10677
|
to.focus();
|
|
10157
10678
|
return;
|
|
10158
10679
|
}
|
|
10159
|
-
if (!pages.
|
|
10680
|
+
if (!pages.has(next)) {
|
|
10160
10681
|
e.preventDefault();
|
|
10161
10682
|
awaitTabStop(next, back, target);
|
|
10162
10683
|
return;
|
|
10163
10684
|
}
|
|
10164
10685
|
}
|
|
10165
10686
|
}
|
|
10166
|
-
el.addEventListener("keydown",
|
|
10687
|
+
el.addEventListener("keydown", onPageTab);
|
|
10688
|
+
/**
|
|
10689
|
+
* The Tab key pressed now, and whether with Shift: set on its keydown, wherever focus is, and
|
|
10690
|
+
* cleared once the browser has moved focus for it, so a pointer press never counts.
|
|
10691
|
+
*/
|
|
10692
|
+
let tabbing;
|
|
10693
|
+
const onTabKey = (e) => {
|
|
10694
|
+
if (e.key !== "Tab") return;
|
|
10695
|
+
const press = { back: e.shiftKey };
|
|
10696
|
+
tabbing = press;
|
|
10697
|
+
setTimeout(() => {
|
|
10698
|
+
if (tabbing === press) tabbing = void 0;
|
|
10699
|
+
});
|
|
10700
|
+
};
|
|
10701
|
+
el.ownerDocument.addEventListener("keydown", onTabKey, true);
|
|
10702
|
+
/**
|
|
10703
|
+
* Tab from outside a page onto its fields and links starts at the page's first stop (Shift+Tab
|
|
10704
|
+
* at its last). The browser enters by DOM order, which on a page with both is the first link or
|
|
10705
|
+
* the last field, so stops before it in the page's order would be skipped.
|
|
10706
|
+
*/
|
|
10707
|
+
function onStopFocus(e) {
|
|
10708
|
+
const target = e.target;
|
|
10709
|
+
if (!tabbing || !(target instanceof HTMLElement)) return;
|
|
10710
|
+
const on = [...pages.values()].find((m) => m.form?.element.contains(target) || m.links?.element.contains(target));
|
|
10711
|
+
if (!on?.form || !on.links) return;
|
|
10712
|
+
const from = e.relatedTarget;
|
|
10713
|
+
if (from instanceof Node && (on.form.element.contains(from) || on.links.element.contains(from))) return;
|
|
10714
|
+
const stops = on.tabStops();
|
|
10715
|
+
if (!stops.includes(target)) return;
|
|
10716
|
+
const edge = tabbing.back ? stops.at(-1) : stops[0];
|
|
10717
|
+
if (edge && edge !== target) edge.focus();
|
|
10718
|
+
}
|
|
10719
|
+
el.addEventListener("focusin", onStopFocus);
|
|
10720
|
+
const reportAction = (a) => (opts.onAction ?? opts.onFormAction)?.(a);
|
|
10721
|
+
/**
|
|
10722
|
+
* History of the jumps made in this document. "Here" is the point the rebuild anchor keeps:
|
|
10723
|
+
* the top-left of the visible area as an `xyz` destination, so Back puts the view where it
|
|
10724
|
+
* was. When that corner is in a gap, `top` is left out and the page lands at its top.
|
|
10725
|
+
*/
|
|
10726
|
+
const navigation = createNavigation({
|
|
10727
|
+
viewport: () => destroyed ? void 0 : viewport,
|
|
10728
|
+
document: () => destroyed ? void 0 : doc,
|
|
10729
|
+
here: () => {
|
|
10730
|
+
const a = scrollAnchor(viewport);
|
|
10731
|
+
return a && {
|
|
10732
|
+
page: a.page,
|
|
10733
|
+
fit: "xyz",
|
|
10734
|
+
left: a.user.x,
|
|
10735
|
+
top: a.vertical ? a.user.y : void 0
|
|
10736
|
+
};
|
|
10737
|
+
},
|
|
10738
|
+
report: reportAction
|
|
10739
|
+
});
|
|
10167
10740
|
const stopForm = [
|
|
10168
|
-
doc.form.on("action",
|
|
10741
|
+
doc.form.on("action", reportAction),
|
|
10169
10742
|
doc.form.on("alert", (message) => opts.onFormAlert?.(message)),
|
|
10170
|
-
doc.form.on("goto", (page) => {
|
|
10171
|
-
|
|
10172
|
-
|
|
10743
|
+
doc.form.on("goto", (page) => navigation.goTo({
|
|
10744
|
+
page,
|
|
10745
|
+
fit: "fit"
|
|
10746
|
+
}))
|
|
10173
10747
|
];
|
|
10174
10748
|
/**
|
|
10175
10749
|
* The page whose open scripts ran last, and has not closed since. Page open and close events
|
|
@@ -10244,6 +10818,28 @@ function mountViewer(opts) {
|
|
|
10244
10818
|
el.scrollLeft += o.x + box.left + css.x - corner.x;
|
|
10245
10819
|
if (anchor.vertical) el.scrollTop += o.y + box.top + css.y - corner.y;
|
|
10246
10820
|
}
|
|
10821
|
+
/**
|
|
10822
|
+
* The link a press at user-space `point` on `page` follows: of the live links whose quads (or
|
|
10823
|
+
* rect, without quads) hold the point, the topmost, which is the last in the file. Unsupported
|
|
10824
|
+
* links are not live, and nor are destinations the page's link layer found go nowhere (it
|
|
10825
|
+
* marks their stop `aria-disabled`).
|
|
10826
|
+
*/
|
|
10827
|
+
function linkAt(vp, page, point) {
|
|
10828
|
+
const host = vp.hostFor(page);
|
|
10829
|
+
const dead = (l) => host?.querySelector(`[data-ink-link-layer] [data-ink-link="${CSS.escape(l.id)}"]`)?.getAttribute("aria-disabled") === "true";
|
|
10830
|
+
const links = doc.links(page);
|
|
10831
|
+
for (let i = links.length - 1; i >= 0; i--) {
|
|
10832
|
+
const l = links[i];
|
|
10833
|
+
if ((l.quads?.length ? l.quads.some((q) => quadContainsPoint(q, point)) : rectContainsPoint(l.rect, point)) && l.target.kind !== "unsupported" && !dead(l)) return l;
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
/** The pieces of the hovered link, which carry `data-ink-hover` for the theme's tint. */
|
|
10837
|
+
let hoveredPieces = [];
|
|
10838
|
+
function markHover(vp, link) {
|
|
10839
|
+
for (const piece of hoveredPieces) piece.removeAttribute("data-ink-hover");
|
|
10840
|
+
hoveredPieces = link ? [...vp.hostFor(link.page)?.querySelectorAll(`[data-ink-link-layer] [data-ink-link="${CSS.escape(link.id)}"]`) ?? []] : [];
|
|
10841
|
+
for (const piece of hoveredPieces) piece.setAttribute("data-ink-hover", "");
|
|
10842
|
+
}
|
|
10247
10843
|
function build(state) {
|
|
10248
10844
|
const vp = createViewport({
|
|
10249
10845
|
container: el,
|
|
@@ -10264,6 +10860,9 @@ function mountViewer(opts) {
|
|
|
10264
10860
|
tools: list,
|
|
10265
10861
|
options: toolOptions,
|
|
10266
10862
|
...plugins && { onPress: (kind, press) => plugins.emitPress(kind, press) },
|
|
10863
|
+
linkAt: (page, point) => linkAt(vp, page, point),
|
|
10864
|
+
onLink: (link) => void navigation.follow(link.target, { from: link.page }),
|
|
10865
|
+
onLinkHover: (link) => markHover(vp, link),
|
|
10267
10866
|
ctx: {
|
|
10268
10867
|
toolStyle,
|
|
10269
10868
|
store: doc.annotations,
|
|
@@ -10334,8 +10933,10 @@ function mountViewer(opts) {
|
|
|
10334
10933
|
get signatures() {
|
|
10335
10934
|
return signatures();
|
|
10336
10935
|
},
|
|
10936
|
+
storage,
|
|
10337
10937
|
signatureFonts: opts.signatureFonts ? [...opts.signatureFonts] : SIGNATURE_FONTS,
|
|
10338
10938
|
pages,
|
|
10939
|
+
navigation,
|
|
10339
10940
|
rebuild(next) {
|
|
10340
10941
|
if (destroyed) return;
|
|
10341
10942
|
const state = {
|
|
@@ -10372,9 +10973,12 @@ function mountViewer(opts) {
|
|
|
10372
10973
|
layers?.destroy();
|
|
10373
10974
|
highlights?.destroy();
|
|
10374
10975
|
stopRedecorate?.();
|
|
10375
|
-
el.removeEventListener("keydown",
|
|
10976
|
+
el.removeEventListener("keydown", onPageTab);
|
|
10977
|
+
el.ownerDocument.removeEventListener("keydown", onTabKey, true);
|
|
10978
|
+
el.removeEventListener("focusin", onStopFocus);
|
|
10376
10979
|
tabWait?.cancel();
|
|
10377
10980
|
for (const stop of stopForm) stop();
|
|
10981
|
+
navigation.clear();
|
|
10378
10982
|
if (!highlight) delete el.dataset.inkFormHighlight;
|
|
10379
10983
|
}
|
|
10380
10984
|
};
|
|
@@ -11674,9 +12278,9 @@ const passThrough = {
|
|
|
11674
12278
|
react: () => noop
|
|
11675
12279
|
};
|
|
11676
12280
|
function createViewer(el, opts) {
|
|
11677
|
-
const { source: initial, assets, password, defaults, history, fields, annotations, plugins, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, formHighlight: initialHighlight, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, onSignField: _onSignField, onFormAlert: _onFormAlert, signatures: signatureStore, search: _search, ...view } = opts;
|
|
12281
|
+
const { source: initial, assets, password, defaults, history, fields, annotations, plugins, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, formHighlight: initialHighlight, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, onSignField: _onSignField, onFormAlert: _onFormAlert, signatures: signatureStore, storage = createMemoryStorage(), search: _search, ...view } = opts;
|
|
11678
12282
|
/** One library for the viewer's life, so templates and the menu outlive each document. */
|
|
11679
|
-
const signatures = createSignatureLibrary(signatureStore ??
|
|
12283
|
+
const signatures = createSignatureLibrary(signatureStore ?? createStorageSignatureStore(storage));
|
|
11680
12284
|
/** Tool options persist for the viewer's life; the tool style is per document. */
|
|
11681
12285
|
const toolOptions = createToolOptions();
|
|
11682
12286
|
const status = writable({ state: "idle" });
|
|
@@ -11685,6 +12289,10 @@ function createViewer(el, opts) {
|
|
|
11685
12289
|
const engine = writable(null);
|
|
11686
12290
|
const viewport = writable(null);
|
|
11687
12291
|
const tools = writable(null);
|
|
12292
|
+
/** The open document's history, and whether it can move, following each engine. */
|
|
12293
|
+
const navigation = writable(null);
|
|
12294
|
+
const canGoBack = writable(false);
|
|
12295
|
+
const canGoForward = writable(false);
|
|
11688
12296
|
/** The activated annotation: the note thread opens from it. */
|
|
11689
12297
|
const activated = writable(null);
|
|
11690
12298
|
let formHighlight = initialHighlight ?? true;
|
|
@@ -11727,6 +12335,7 @@ function createViewer(el, opts) {
|
|
|
11727
12335
|
*/
|
|
11728
12336
|
const host = createPluginHost({
|
|
11729
12337
|
plugins: plugins ?? [],
|
|
12338
|
+
storage,
|
|
11730
12339
|
document: readonly(current),
|
|
11731
12340
|
history: () => current.get()?.history ?? passThrough,
|
|
11732
12341
|
activated: readonly(activated),
|
|
@@ -11807,6 +12416,7 @@ function createViewer(el, opts) {
|
|
|
11807
12416
|
formHighlight,
|
|
11808
12417
|
toolOptions,
|
|
11809
12418
|
signatureLibrary: signatures,
|
|
12419
|
+
storage,
|
|
11810
12420
|
onFormAlert: (message) => formAlert.show(message),
|
|
11811
12421
|
onSignField: (req) => {
|
|
11812
12422
|
signFieldMenu ??= createSignFieldMenu(viewer, {
|
|
@@ -11849,7 +12459,16 @@ function createViewer(el, opts) {
|
|
|
11849
12459
|
}
|
|
11850
12460
|
});
|
|
11851
12461
|
engine.set(mounted);
|
|
12462
|
+
const nav = mounted.navigation;
|
|
12463
|
+
const stopNav = [nav.canGoBack.subscribe(canGoBack.set), nav.canGoForward.subscribe(canGoForward.set)];
|
|
12464
|
+
canGoBack.set(nav.canGoBack.get());
|
|
12465
|
+
canGoForward.set(nav.canGoForward.get());
|
|
12466
|
+
navigation.set(nav);
|
|
11852
12467
|
return () => {
|
|
12468
|
+
for (const s of stopNav) s();
|
|
12469
|
+
navigation.set(null);
|
|
12470
|
+
canGoBack.set(false);
|
|
12471
|
+
canGoForward.set(false);
|
|
11853
12472
|
stopForm();
|
|
11854
12473
|
releaseFormErrors = () => void 0;
|
|
11855
12474
|
signFieldMenu?.close();
|
|
@@ -11944,6 +12563,11 @@ function createViewer(el, opts) {
|
|
|
11944
12563
|
formHighlight = on;
|
|
11945
12564
|
engine.get()?.setFormHighlight(on);
|
|
11946
12565
|
},
|
|
12566
|
+
canGoBack: readonly(canGoBack),
|
|
12567
|
+
canGoForward: readonly(canGoForward),
|
|
12568
|
+
back: () => navigation.get()?.back(),
|
|
12569
|
+
forward: () => navigation.get()?.forward(),
|
|
12570
|
+
follow: async (target) => navigation.get()?.follow(target),
|
|
11947
12571
|
commands: { get: (id) => host.command(id) },
|
|
11948
12572
|
search: viewerSearch,
|
|
11949
12573
|
records: (key) => host.records(key),
|
|
@@ -11968,6 +12592,7 @@ function createViewer(el, opts) {
|
|
|
11968
12592
|
internals.set(viewer, {
|
|
11969
12593
|
search,
|
|
11970
12594
|
host,
|
|
12595
|
+
navigation: readonly(navigation),
|
|
11971
12596
|
destroyers,
|
|
11972
12597
|
destroyed: () => destroyed
|
|
11973
12598
|
});
|
|
@@ -12000,6 +12625,15 @@ function pluginHostOf(viewer) {
|
|
|
12000
12625
|
return found.host;
|
|
12001
12626
|
}
|
|
12002
12627
|
/**
|
|
12628
|
+
* @internal The open document's navigation, `null` while none is ready, for controls that jump
|
|
12629
|
+
* (the outline) and so must record history.
|
|
12630
|
+
*/
|
|
12631
|
+
function navigationOf(viewer) {
|
|
12632
|
+
const found = internals.get(viewer);
|
|
12633
|
+
if (!found) throw new Error("Not a viewer made by createViewer");
|
|
12634
|
+
return found.navigation;
|
|
12635
|
+
}
|
|
12636
|
+
/**
|
|
12003
12637
|
* @internal Runs `fn` when the viewer is destroyed, for what a control set up on it; at once
|
|
12004
12638
|
* when it already is.
|
|
12005
12639
|
*/
|
|
@@ -12171,6 +12805,33 @@ function historyButton(viewer, action) {
|
|
|
12171
12805
|
return b;
|
|
12172
12806
|
}
|
|
12173
12807
|
/**
|
|
12808
|
+
* Returns to where the view was before the last jump (`viewer.back()`): a link, an outline
|
|
12809
|
+
* item or a form button that went to a page. Disabled while there is nowhere to go back to.
|
|
12810
|
+
*/
|
|
12811
|
+
function createBack(viewer) {
|
|
12812
|
+
return navigationButton(viewer, "back");
|
|
12813
|
+
}
|
|
12814
|
+
/** Goes forward again after `createBack` (`viewer.forward()`); disabled until Back is used. */
|
|
12815
|
+
function createForward(viewer) {
|
|
12816
|
+
return navigationButton(viewer, "forward");
|
|
12817
|
+
}
|
|
12818
|
+
function navigationButton(viewer, action) {
|
|
12819
|
+
const back = action === "back";
|
|
12820
|
+
const b = button(back ? "Back" : "Forward", {
|
|
12821
|
+
class: `ink-${action}`,
|
|
12822
|
+
[`data-ink-${action}`]: ""
|
|
12823
|
+
}, back ? ArrowLeft : ArrowRight);
|
|
12824
|
+
b.addEventListener("click", () => viewer[action]());
|
|
12825
|
+
keepHistoryFocus(b, () => viewer.element.querySelector("[data-ink-scroll]"));
|
|
12826
|
+
const can = back ? viewer.canGoBack : viewer.canGoForward;
|
|
12827
|
+
const show = (on) => {
|
|
12828
|
+
b.disabled = !on;
|
|
12829
|
+
};
|
|
12830
|
+
show(can.get());
|
|
12831
|
+
onViewerDestroy(viewer, can.subscribe(show));
|
|
12832
|
+
return b;
|
|
12833
|
+
}
|
|
12834
|
+
/**
|
|
12174
12835
|
* True for `select-text` and `select`, which every viewer registers, and for any other tool id
|
|
12175
12836
|
* the viewer was given.
|
|
12176
12837
|
*/
|
|
@@ -13229,6 +13890,29 @@ function sidebarBadge(badge) {
|
|
|
13229
13890
|
const text = String(badge).trim();
|
|
13230
13891
|
return text === "" || text === "0" ? null : text;
|
|
13231
13892
|
}
|
|
13893
|
+
/** The sidebar's widths in CSS px, unless the host gives its own. */
|
|
13894
|
+
const SIDEBAR_WIDTH = {
|
|
13895
|
+
default: 320,
|
|
13896
|
+
min: 240,
|
|
13897
|
+
max: 480
|
|
13898
|
+
};
|
|
13899
|
+
/** The widest the sidebar shows: `max`, and half the container once measured, but never below `min`. */
|
|
13900
|
+
const widthLimit = ({ min, max, container }) => container === void 0 ? max : Math.min(max, Math.max(min, Math.floor(container / 2)));
|
|
13901
|
+
/** The width the sidebar shows for the wanted `want`. */
|
|
13902
|
+
function shownSidebarWidth(want, limits) {
|
|
13903
|
+
return Math.min(Math.max(want, limits.min), widthLimit(limits));
|
|
13904
|
+
}
|
|
13905
|
+
/** How far an arrow key moves the handle. */
|
|
13906
|
+
const KEY_STEP = 16;
|
|
13907
|
+
const isThenable = (value) => typeof value?.then === "function";
|
|
13908
|
+
/** Calls `fn` on storage so that neither a throw nor a rejection escapes. */
|
|
13909
|
+
function quietly(fn, then) {
|
|
13910
|
+
try {
|
|
13911
|
+
const result = fn();
|
|
13912
|
+
if (isThenable(result)) result.then((value) => then?.(value), () => {});
|
|
13913
|
+
else then?.(result);
|
|
13914
|
+
} catch {}
|
|
13915
|
+
}
|
|
13232
13916
|
const TRIGGER = "[data-ink-sidebar-trigger]";
|
|
13233
13917
|
let count = 0;
|
|
13234
13918
|
function sameIds(a, b) {
|
|
@@ -13256,6 +13940,20 @@ function createSidebarController(opts) {
|
|
|
13256
13940
|
let visible = opts.visible ?? true;
|
|
13257
13941
|
/** The options that last gave `onOpenChange`, called as a method of them. */
|
|
13258
13942
|
let handler = opts;
|
|
13943
|
+
/** Likewise for `onWidthChange`. */
|
|
13944
|
+
let widthHandler = opts;
|
|
13945
|
+
const storage = opts.storage;
|
|
13946
|
+
const name = opts.name;
|
|
13947
|
+
let defaultWidth = opts.defaultWidth ?? SIDEBAR_WIDTH.default;
|
|
13948
|
+
let minWidth = opts.minWidth ?? SIDEBAR_WIDTH.min;
|
|
13949
|
+
let maxWidth = opts.maxWidth ?? SIDEBAR_WIDTH.max;
|
|
13950
|
+
let widthControlled = opts.width !== void 0;
|
|
13951
|
+
let width = opts.width ?? defaultWidth;
|
|
13952
|
+
let resizing = false;
|
|
13953
|
+
/** The width of the root's parent, once `bindHandle` has measured it. */
|
|
13954
|
+
let container;
|
|
13955
|
+
/** Set by a drag, key or reset: a late answer from storage no longer applies. */
|
|
13956
|
+
let touched = false;
|
|
13259
13957
|
/** The trigger that holds the Tab stop in rail and tabs, once one has had focus. */
|
|
13260
13958
|
let current = null;
|
|
13261
13959
|
let destroyed = false;
|
|
@@ -13288,15 +13986,49 @@ function createSidebarController(opts) {
|
|
|
13288
13986
|
side,
|
|
13289
13987
|
panels,
|
|
13290
13988
|
open: resolve(opts.open !== void 0 ? opts.open : panels[0]?.id ?? null),
|
|
13291
|
-
visible
|
|
13989
|
+
visible,
|
|
13990
|
+
width,
|
|
13991
|
+
resizing
|
|
13292
13992
|
});
|
|
13293
|
-
const publish = (open) => store.set({
|
|
13993
|
+
const publish = (open = store.get().open) => store.set({
|
|
13294
13994
|
layout,
|
|
13295
13995
|
side,
|
|
13296
13996
|
panels,
|
|
13297
13997
|
open,
|
|
13298
|
-
visible
|
|
13998
|
+
visible,
|
|
13999
|
+
width,
|
|
14000
|
+
resizing
|
|
14001
|
+
});
|
|
14002
|
+
const widthKey = () => `sidebar.${name ?? side}.width`;
|
|
14003
|
+
const limits = () => ({
|
|
14004
|
+
min: minWidth,
|
|
14005
|
+
max: maxWidth,
|
|
14006
|
+
container
|
|
13299
14007
|
});
|
|
14008
|
+
const shown = () => shownSidebarWidth(width, limits());
|
|
14009
|
+
const clampWidth = (w) => Math.min(Math.max(w, minWidth), maxWidth);
|
|
14010
|
+
if (!widthControlled && storage) quietly(() => storage.get(widthKey()), (saved) => {
|
|
14011
|
+
if (destroyed || touched || widthControlled || typeof saved !== "number" || !Number.isFinite(saved) || saved <= 0 || saved === width) return;
|
|
14012
|
+
width = saved;
|
|
14013
|
+
publish();
|
|
14014
|
+
});
|
|
14015
|
+
/** A width the user chose: kept and reported when uncontrolled, only reported when controlled. */
|
|
14016
|
+
function chooseWidth(next, save) {
|
|
14017
|
+
touched = true;
|
|
14018
|
+
if (!widthControlled) {
|
|
14019
|
+
if (next !== width) {
|
|
14020
|
+
width = next;
|
|
14021
|
+
publish();
|
|
14022
|
+
}
|
|
14023
|
+
if (save && storage) quietly(() => storage.set(widthKey(), next));
|
|
14024
|
+
}
|
|
14025
|
+
widthHandler.onWidthChange?.(next);
|
|
14026
|
+
}
|
|
14027
|
+
function resetWidth() {
|
|
14028
|
+
if (destroyed) return;
|
|
14029
|
+
chooseWidth(defaultWidth, false);
|
|
14030
|
+
if (!widthControlled && storage) quietly(() => storage.remove(widthKey()));
|
|
14031
|
+
}
|
|
13300
14032
|
const isOpen = (id) => store.get().open.includes(id);
|
|
13301
14033
|
function update(next) {
|
|
13302
14034
|
if (destroyed) return;
|
|
@@ -13309,11 +14041,24 @@ function createSidebarController(opts) {
|
|
|
13309
14041
|
if (next.panels !== void 0 && !samePanels(next.panels, panels)) panels = next.panels;
|
|
13310
14042
|
if ("onOpenChange" in next) handler = next;
|
|
13311
14043
|
if (next.open !== void 0) controlled = true;
|
|
14044
|
+
if ("onWidthChange" in next) widthHandler = next;
|
|
14045
|
+
const shownBefore = shown();
|
|
14046
|
+
const widthBefore = width;
|
|
14047
|
+
const minBefore = minWidth;
|
|
14048
|
+
const limitBefore = widthLimit(limits());
|
|
14049
|
+
if (next.defaultWidth !== void 0) defaultWidth = next.defaultWidth;
|
|
14050
|
+
if (next.minWidth !== void 0) minWidth = next.minWidth;
|
|
14051
|
+
if (next.maxWidth !== void 0) maxWidth = next.maxWidth;
|
|
14052
|
+
if (next.width !== void 0) {
|
|
14053
|
+
widthControlled = true;
|
|
14054
|
+
width = next.width;
|
|
14055
|
+
}
|
|
14056
|
+
const widthChanged = width !== widthBefore || shown() !== shownBefore || minWidth !== minBefore || widthLimit(limits()) !== limitBefore;
|
|
13312
14057
|
const arrived = !controlled && before.panels.length === 0 && before.open.length === 0 ? panels[0]?.id : void 0;
|
|
13313
14058
|
const asked = next.open !== void 0 ? idsOf(next.open) : arrived !== void 0 ? [arrived] : before.open;
|
|
13314
14059
|
let open = resolve(asked);
|
|
13315
14060
|
if (!controlled && layout === "tabs" && open.length === 0 && panels[0] !== void 0) open = [panels[0].id];
|
|
13316
|
-
if (pending !== wasPending || layout !== before.layout || side !== before.side || visible !== before.visible || panels !== before.panels || !sameIds(open, before.open)) publish(open);
|
|
14061
|
+
if (pending !== wasPending || layout !== before.layout || side !== before.side || visible !== before.visible || panels !== before.panels || widthChanged || !sameIds(open, before.open)) publish(open);
|
|
13317
14062
|
if (!(open.length === new Set(asked).size && open.every((id) => asked.includes(id)))) handler.onOpenChange?.(toValue(open));
|
|
13318
14063
|
}
|
|
13319
14064
|
function toggle(id) {
|
|
@@ -13444,6 +14189,123 @@ function createSidebarController(opts) {
|
|
|
13444
14189
|
bindings.add(unbind);
|
|
13445
14190
|
return unbind;
|
|
13446
14191
|
}
|
|
14192
|
+
const panelsId = `${prefix}-panels`;
|
|
14193
|
+
function handleAttrs() {
|
|
14194
|
+
const attrs = {
|
|
14195
|
+
"data-ink-sidebar-handle": "",
|
|
14196
|
+
role: "separator",
|
|
14197
|
+
"aria-orientation": "vertical",
|
|
14198
|
+
"aria-label": "Resize sidebar",
|
|
14199
|
+
"aria-valuemin": String(Math.round(minWidth)),
|
|
14200
|
+
"aria-valuemax": String(Math.round(widthLimit(limits()))),
|
|
14201
|
+
"aria-valuenow": String(Math.round(shown())),
|
|
14202
|
+
tabindex: "0"
|
|
14203
|
+
};
|
|
14204
|
+
if (layout !== "stack") attrs["aria-controls"] = panelsId;
|
|
14205
|
+
if (!visible || store.get().open.length === 0) attrs.hidden = "";
|
|
14206
|
+
return attrs;
|
|
14207
|
+
}
|
|
14208
|
+
function bindHandle(el) {
|
|
14209
|
+
/**
|
|
14210
|
+
* The drag under way: its pointer, where it started, the width then, the last one chosen and
|
|
14211
|
+
* whether it has chosen any.
|
|
14212
|
+
*/
|
|
14213
|
+
let drag = null;
|
|
14214
|
+
const wider = () => side === "left" ? 1 : -1;
|
|
14215
|
+
const setResizing = (on) => {
|
|
14216
|
+
if (resizing === on) return;
|
|
14217
|
+
resizing = on;
|
|
14218
|
+
publish();
|
|
14219
|
+
};
|
|
14220
|
+
const onPointerDown = (e) => {
|
|
14221
|
+
if (destroyed || drag) return;
|
|
14222
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
14223
|
+
e.preventDefault();
|
|
14224
|
+
try {
|
|
14225
|
+
el.setPointerCapture(e.pointerId);
|
|
14226
|
+
} catch {}
|
|
14227
|
+
drag = {
|
|
14228
|
+
pointer: e.pointerId,
|
|
14229
|
+
x: e.clientX,
|
|
14230
|
+
width: shown(),
|
|
14231
|
+
last: shown(),
|
|
14232
|
+
moved: false
|
|
14233
|
+
};
|
|
14234
|
+
setResizing(true);
|
|
14235
|
+
};
|
|
14236
|
+
/** Ends the drag; saves only when it moved, so a press or a double-click writes nothing. */
|
|
14237
|
+
const endDrag = () => {
|
|
14238
|
+
if (!drag) return;
|
|
14239
|
+
const { moved } = drag;
|
|
14240
|
+
drag = null;
|
|
14241
|
+
setResizing(false);
|
|
14242
|
+
if (moved && !widthControlled && storage) quietly(() => storage.set(widthKey(), width));
|
|
14243
|
+
};
|
|
14244
|
+
const onPointerMove = (e) => {
|
|
14245
|
+
if (!drag || e.pointerId !== drag.pointer) return;
|
|
14246
|
+
if (e.pointerType === "mouse" && e.buttons === 0) return endDrag();
|
|
14247
|
+
const next = clampWidth(drag.width + (e.clientX - drag.x) * wider());
|
|
14248
|
+
if (next === drag.last) return;
|
|
14249
|
+
drag.last = next;
|
|
14250
|
+
drag.moved = true;
|
|
14251
|
+
chooseWidth(next, false);
|
|
14252
|
+
};
|
|
14253
|
+
const onPointerEnd = (e) => {
|
|
14254
|
+
if (drag && e.pointerId === drag.pointer) endDrag();
|
|
14255
|
+
};
|
|
14256
|
+
const onKeyDown = (e) => {
|
|
14257
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
14258
|
+
const now = shown();
|
|
14259
|
+
let next;
|
|
14260
|
+
if (e.key === "ArrowRight") next = clampWidth(now + KEY_STEP * wider());
|
|
14261
|
+
else if (e.key === "ArrowLeft") next = clampWidth(now - KEY_STEP * wider());
|
|
14262
|
+
else if (e.key === "Home") next = minWidth;
|
|
14263
|
+
else if (e.key === "End") next = maxWidth;
|
|
14264
|
+
else if (e.key === "Enter") next = null;
|
|
14265
|
+
else return;
|
|
14266
|
+
e.preventDefault();
|
|
14267
|
+
if (next === null) resetWidth();
|
|
14268
|
+
else chooseWidth(next, true);
|
|
14269
|
+
};
|
|
14270
|
+
const onDoubleClick = () => resetWidth();
|
|
14271
|
+
const parent = (el.closest("[data-ink-sidebar]") ?? el.parentElement)?.parentElement;
|
|
14272
|
+
let observer;
|
|
14273
|
+
if (parent && typeof ResizeObserver !== "undefined") {
|
|
14274
|
+
observer = new ResizeObserver((entries) => {
|
|
14275
|
+
const measured = entries[entries.length - 1]?.contentRect.width;
|
|
14276
|
+
if (measured === void 0 || measured === container || destroyed) return;
|
|
14277
|
+
const before = shown();
|
|
14278
|
+
const limitBefore = widthLimit(limits());
|
|
14279
|
+
container = measured;
|
|
14280
|
+
if (shown() !== before || widthLimit(limits()) !== limitBefore) publish();
|
|
14281
|
+
});
|
|
14282
|
+
observer.observe(parent);
|
|
14283
|
+
}
|
|
14284
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
14285
|
+
el.addEventListener("pointermove", onPointerMove);
|
|
14286
|
+
el.addEventListener("pointerup", onPointerEnd);
|
|
14287
|
+
el.addEventListener("pointercancel", onPointerEnd);
|
|
14288
|
+
el.addEventListener("lostpointercapture", onPointerEnd);
|
|
14289
|
+
el.addEventListener("keydown", onKeyDown);
|
|
14290
|
+
el.addEventListener("dblclick", onDoubleClick);
|
|
14291
|
+
const unbind = () => {
|
|
14292
|
+
if (!bindings.delete(unbind)) return;
|
|
14293
|
+
observer?.disconnect();
|
|
14294
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
14295
|
+
el.removeEventListener("pointermove", onPointerMove);
|
|
14296
|
+
el.removeEventListener("pointerup", onPointerEnd);
|
|
14297
|
+
el.removeEventListener("pointercancel", onPointerEnd);
|
|
14298
|
+
el.removeEventListener("lostpointercapture", onPointerEnd);
|
|
14299
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
14300
|
+
el.removeEventListener("dblclick", onDoubleClick);
|
|
14301
|
+
if (drag) {
|
|
14302
|
+
drag = null;
|
|
14303
|
+
if (!destroyed) setResizing(false);
|
|
14304
|
+
}
|
|
14305
|
+
};
|
|
14306
|
+
bindings.add(unbind);
|
|
14307
|
+
return unbind;
|
|
14308
|
+
}
|
|
13447
14309
|
const controller = {
|
|
13448
14310
|
state: readonly(store),
|
|
13449
14311
|
update,
|
|
@@ -13454,8 +14316,14 @@ function createSidebarController(opts) {
|
|
|
13454
14316
|
"data-layout": layout,
|
|
13455
14317
|
"data-side": side,
|
|
13456
14318
|
"data-state": store.get().open.length > 0 || pending && !controlled ? "open" : "closed",
|
|
14319
|
+
...resizing && { "data-resizing": "" },
|
|
13457
14320
|
...!visible && { hidden: "" }
|
|
13458
14321
|
}),
|
|
14322
|
+
rootStyle: () => ({ "--ink-sidebar-width": `${shown()}px` }),
|
|
14323
|
+
panelsAttrs: () => ({ id: panelsId }),
|
|
14324
|
+
handleAttrs,
|
|
14325
|
+
bindHandle,
|
|
14326
|
+
resetWidth,
|
|
13459
14327
|
tablistAttrs,
|
|
13460
14328
|
triggerAttrs,
|
|
13461
14329
|
panelAttrs,
|
|
@@ -13489,6 +14357,8 @@ function createSidebar(opts) {
|
|
|
13489
14357
|
const { content, ...options } = opts;
|
|
13490
14358
|
const controller = createSidebarController(options);
|
|
13491
14359
|
const root = h("div", { class: "ink-sidebar" });
|
|
14360
|
+
const handle = h("div", { class: "ink-sidebar-handle" });
|
|
14361
|
+
let panelsEl = null;
|
|
13492
14362
|
let shape = "";
|
|
13493
14363
|
const triggers = /* @__PURE__ */ new Map();
|
|
13494
14364
|
const panels = /* @__PURE__ */ new Map();
|
|
@@ -13524,6 +14394,7 @@ function createSidebar(opts) {
|
|
|
13524
14394
|
for (const [p, panel] of built) panels.set(p.id, panel);
|
|
13525
14395
|
if (s.layout === "stack") {
|
|
13526
14396
|
list = null;
|
|
14397
|
+
panelsEl = null;
|
|
13527
14398
|
root.replaceChildren(...built.map(([p, panel]) => {
|
|
13528
14399
|
const header = makeTrigger(p, "ink-sidebar-header");
|
|
13529
14400
|
triggers.set(p.id, header);
|
|
@@ -13536,15 +14407,20 @@ function createSidebar(opts) {
|
|
|
13536
14407
|
triggers.set(p.id, t);
|
|
13537
14408
|
list.append(t);
|
|
13538
14409
|
}
|
|
13539
|
-
|
|
14410
|
+
panelsEl = h("div", { class: "ink-sidebar-panels" }, ...built.map(([, panel]) => panel));
|
|
14411
|
+
root.replaceChildren(list, panelsEl);
|
|
13540
14412
|
}
|
|
14413
|
+
root.append(handle);
|
|
13541
14414
|
paint(s);
|
|
13542
14415
|
if (focused !== void 0) triggers.get(focused)?.focus();
|
|
13543
14416
|
}
|
|
13544
14417
|
/** Attributes, labels and badges, in place: the focused trigger keeps focus. */
|
|
13545
14418
|
function paint(s) {
|
|
13546
14419
|
setAttrs(root, controller.rootAttrs());
|
|
14420
|
+
for (const [name, value] of Object.entries(controller.rootStyle())) if (root.style.getPropertyValue(name) !== value) root.style.setProperty(name, value);
|
|
13547
14421
|
if (list) setAttrs(list, controller.tablistAttrs());
|
|
14422
|
+
if (panelsEl) setAttrs(panelsEl, controller.panelsAttrs());
|
|
14423
|
+
setAttrs(handle, controller.handleAttrs());
|
|
13548
14424
|
for (const p of s.panels) {
|
|
13549
14425
|
const t = triggers.get(p.id);
|
|
13550
14426
|
const panel = panels.get(p.id);
|
|
@@ -13578,9 +14454,11 @@ function createSidebar(opts) {
|
|
|
13578
14454
|
show(controller.state.get());
|
|
13579
14455
|
const stopState = controller.state.subscribe(show);
|
|
13580
14456
|
const unbind = controller.bindTriggers(root);
|
|
14457
|
+
const unbindHandle = controller.bindHandle(handle);
|
|
13581
14458
|
onSidebarDestroy(controller, () => {
|
|
13582
14459
|
stopState();
|
|
13583
14460
|
unbind();
|
|
14461
|
+
unbindHandle();
|
|
13584
14462
|
});
|
|
13585
14463
|
return {
|
|
13586
14464
|
element: root,
|
|
@@ -14424,6 +15302,581 @@ function createThumbnails(viewer, opts = {}) {
|
|
|
14424
15302
|
return root;
|
|
14425
15303
|
}
|
|
14426
15304
|
//#endregion
|
|
15305
|
+
//#region src/dom/outline/controller.ts
|
|
15306
|
+
/** How far, in CSS px, the view may drift after a jump before the jumped-to item lets go. */
|
|
15307
|
+
const PIN_SLACK = 2;
|
|
15308
|
+
/** How long typed characters keep adding to one type-ahead search, in ms. */
|
|
15309
|
+
const TYPE_AHEAD_MS = 500;
|
|
15310
|
+
let instances = 0;
|
|
15311
|
+
const hasChildren = (node) => node.item.children.length > 0;
|
|
15312
|
+
/**
|
|
15313
|
+
* `role="tree"` and its label, for the element holding an outline's items. The same as a
|
|
15314
|
+
* controller's `treeAttrs()`, without one, so markup rendered on the server has them too.
|
|
15315
|
+
*/
|
|
15316
|
+
function outlineTreeAttrs() {
|
|
15317
|
+
return {
|
|
15318
|
+
role: "tree",
|
|
15319
|
+
"aria-label": "Outline"
|
|
15320
|
+
};
|
|
15321
|
+
}
|
|
15322
|
+
function createOutlineController(deps) {
|
|
15323
|
+
const prefix = deps.idPrefix ?? `ink-outline-${++instances}`;
|
|
15324
|
+
const domId = (id) => `${prefix}-${id.replaceAll(".", "-")}`;
|
|
15325
|
+
const items = writable([]);
|
|
15326
|
+
const loading = writable(false);
|
|
15327
|
+
const expanded = writable(/* @__PURE__ */ new Set());
|
|
15328
|
+
const current = writable(null);
|
|
15329
|
+
const focused = writable(null);
|
|
15330
|
+
/** Every item, in document order. */
|
|
15331
|
+
let nodes = [];
|
|
15332
|
+
const byId = /* @__PURE__ */ new Map();
|
|
15333
|
+
const byDomId = /* @__PURE__ */ new Map();
|
|
15334
|
+
/** The items with a destination on a laid-out page, in document order, with its y. */
|
|
15335
|
+
let placed = [];
|
|
15336
|
+
/** `suffixMin[i]`: the least y from `placed[i]` on, which never decreases with `i`. */
|
|
15337
|
+
let suffixMin = [];
|
|
15338
|
+
/** The item the reading line has reached, before collapsed ancestors are applied. */
|
|
15339
|
+
let reached = null;
|
|
15340
|
+
let destroyed = false;
|
|
15341
|
+
/** Bumped per document, so an outline read for an earlier one is dropped. */
|
|
15342
|
+
let generation = 0;
|
|
15343
|
+
/** The document the tree's elements live in, once a key or `bindScroll` has shown it. */
|
|
15344
|
+
let ownerDoc = null;
|
|
15345
|
+
/**
|
|
15346
|
+
* The item last jumped to from the outline. It stays current until the reader scrolls away
|
|
15347
|
+
* from where the jump landed (`line`, recorded once the jump settles): a destination centred
|
|
15348
|
+
* in the view, or one sharing a page with later items, may otherwise not be the one the rule
|
|
15349
|
+
* picks there.
|
|
15350
|
+
*/
|
|
15351
|
+
let pin = null;
|
|
15352
|
+
/** Cancels the wait for the jump to settle. */
|
|
15353
|
+
let cancelSettle = null;
|
|
15354
|
+
let typed = "";
|
|
15355
|
+
let typedTimer;
|
|
15356
|
+
function index(roots) {
|
|
15357
|
+
nodes = [];
|
|
15358
|
+
byId.clear();
|
|
15359
|
+
byDomId.clear();
|
|
15360
|
+
const walk = (list, parent) => list.forEach((item, i) => {
|
|
15361
|
+
const node = {
|
|
15362
|
+
item,
|
|
15363
|
+
parent,
|
|
15364
|
+
level: parent ? parent.level + 1 : 1,
|
|
15365
|
+
posinset: i + 1,
|
|
15366
|
+
setsize: list.length
|
|
15367
|
+
};
|
|
15368
|
+
nodes.push(node);
|
|
15369
|
+
byId.set(item.id, node);
|
|
15370
|
+
byDomId.set(domId(item.id), node);
|
|
15371
|
+
walk(item.children, node);
|
|
15372
|
+
});
|
|
15373
|
+
walk(roots, null);
|
|
15374
|
+
}
|
|
15375
|
+
const isExpanded = (node) => expanded.get().has(node.item.id);
|
|
15376
|
+
/** Whether every ancestor of `node` is expanded. */
|
|
15377
|
+
function shown(node) {
|
|
15378
|
+
for (let p = node.parent; p; p = p.parent) if (!isExpanded(p)) return false;
|
|
15379
|
+
return true;
|
|
15380
|
+
}
|
|
15381
|
+
/** The items a reader sees, in order: those whose ancestors are all expanded. */
|
|
15382
|
+
function visible() {
|
|
15383
|
+
const out = [];
|
|
15384
|
+
const walk = (list) => {
|
|
15385
|
+
for (const item of list) {
|
|
15386
|
+
const node = byId.get(item.id);
|
|
15387
|
+
out.push(node);
|
|
15388
|
+
if (isExpanded(node)) walk(item.children);
|
|
15389
|
+
}
|
|
15390
|
+
};
|
|
15391
|
+
walk(items.get());
|
|
15392
|
+
return out;
|
|
15393
|
+
}
|
|
15394
|
+
/** `node`, or its outermost collapsed ancestor when one hides it. */
|
|
15395
|
+
function showing(node) {
|
|
15396
|
+
let out = node;
|
|
15397
|
+
for (let p = node.parent; p; p = p.parent) if (!isExpanded(p)) out = p;
|
|
15398
|
+
return out;
|
|
15399
|
+
}
|
|
15400
|
+
function mark() {
|
|
15401
|
+
current.set(reached ? showing(reached).item.id : null);
|
|
15402
|
+
}
|
|
15403
|
+
/** Places every destination in the current layout. */
|
|
15404
|
+
function place() {
|
|
15405
|
+
const vp = deps.viewport.get();
|
|
15406
|
+
placed = [];
|
|
15407
|
+
if (vp) {
|
|
15408
|
+
const shown = vp.currentPage.get();
|
|
15409
|
+
for (const node of nodes) {
|
|
15410
|
+
const target = node.item.target;
|
|
15411
|
+
if (target.kind !== "resolved") continue;
|
|
15412
|
+
const y = vp.destinationY(target.dest);
|
|
15413
|
+
if (y !== void 0 && Number.isFinite(y)) placed.push({
|
|
15414
|
+
node,
|
|
15415
|
+
y
|
|
15416
|
+
});
|
|
15417
|
+
else if (y === void 0 && target.dest.page !== shown) placed.push({
|
|
15418
|
+
node,
|
|
15419
|
+
y: target.dest.page < shown ? -Infinity : Infinity
|
|
15420
|
+
});
|
|
15421
|
+
}
|
|
15422
|
+
}
|
|
15423
|
+
suffixMin = new Array(placed.length);
|
|
15424
|
+
let least = Infinity;
|
|
15425
|
+
for (let i = placed.length - 1; i >= 0; i--) {
|
|
15426
|
+
least = Math.min(least, placed[i].y);
|
|
15427
|
+
suffixMin[i] = least;
|
|
15428
|
+
}
|
|
15429
|
+
}
|
|
15430
|
+
/**
|
|
15431
|
+
* Finds the last placed item at or above the reading line. The last `i` with
|
|
15432
|
+
* `suffixMin[i] <= line` is that item: nothing after it is at or above the line.
|
|
15433
|
+
*/
|
|
15434
|
+
function reach() {
|
|
15435
|
+
const vp = deps.viewport.get();
|
|
15436
|
+
if (!vp || placed.length === 0) reached = null;
|
|
15437
|
+
else {
|
|
15438
|
+
const line = vp.readingLine.get();
|
|
15439
|
+
let lo = 0;
|
|
15440
|
+
let hi = placed.length;
|
|
15441
|
+
while (lo < hi) {
|
|
15442
|
+
const mid = lo + hi >> 1;
|
|
15443
|
+
if (suffixMin[mid] <= line) lo = mid + 1;
|
|
15444
|
+
else hi = mid;
|
|
15445
|
+
}
|
|
15446
|
+
reached = lo > 0 ? placed[lo - 1].node : null;
|
|
15447
|
+
if (pin && pin.line !== void 0 && Math.abs(line - pin.line) > PIN_SLACK) unpin();
|
|
15448
|
+
if (pin) reached = pin.node;
|
|
15449
|
+
}
|
|
15450
|
+
mark();
|
|
15451
|
+
}
|
|
15452
|
+
/** The item holding the Tab stop: the focused one, else the current one, else the first. */
|
|
15453
|
+
function tabStop() {
|
|
15454
|
+
const f = focused.get();
|
|
15455
|
+
if (f !== null && byId.has(f)) return f;
|
|
15456
|
+
return current.get() ?? nodes[0]?.item.id ?? null;
|
|
15457
|
+
}
|
|
15458
|
+
const elementFor = (id) => ownerDoc?.getElementById(domId(id)) ?? null;
|
|
15459
|
+
/** The item's own row: the item element holds its children too, which can run far below. */
|
|
15460
|
+
const rowFor = (id) => ownerDoc?.getElementById(`${domId(id)}-row`) ?? null;
|
|
15461
|
+
const focusInTree = () => {
|
|
15462
|
+
const active = ownerDoc?.activeElement;
|
|
15463
|
+
return !!active && byDomId.has(active.id);
|
|
15464
|
+
};
|
|
15465
|
+
/** Moves the Tab stop, and focus, to `id`. */
|
|
15466
|
+
function moveTo(id) {
|
|
15467
|
+
focused.set(id);
|
|
15468
|
+
elementFor(id)?.focus();
|
|
15469
|
+
}
|
|
15470
|
+
function setExpanded(next) {
|
|
15471
|
+
const hadFocus = focusInTree();
|
|
15472
|
+
expanded.set(next);
|
|
15473
|
+
const f = focused.get();
|
|
15474
|
+
const node = f === null ? void 0 : byId.get(f);
|
|
15475
|
+
if (node && !shown(node)) {
|
|
15476
|
+
const to = showing(node).item.id;
|
|
15477
|
+
focused.set(to);
|
|
15478
|
+
if (hadFocus) elementFor(to)?.focus();
|
|
15479
|
+
}
|
|
15480
|
+
mark();
|
|
15481
|
+
}
|
|
15482
|
+
function unpin() {
|
|
15483
|
+
pin = null;
|
|
15484
|
+
cancelSettle?.();
|
|
15485
|
+
cancelSettle = null;
|
|
15486
|
+
}
|
|
15487
|
+
/** Runs `fn` once the scroll a jump started has landed: on the next frame. */
|
|
15488
|
+
function afterFrame(fn) {
|
|
15489
|
+
const view = ownerDoc?.defaultView ?? globalThis;
|
|
15490
|
+
if (typeof view.requestAnimationFrame === "function") {
|
|
15491
|
+
const handle = view.requestAnimationFrame(fn);
|
|
15492
|
+
return () => view.cancelAnimationFrame(handle);
|
|
15493
|
+
}
|
|
15494
|
+
const timer = setTimeout(fn, 0);
|
|
15495
|
+
return () => clearTimeout(timer);
|
|
15496
|
+
}
|
|
15497
|
+
function load(doc) {
|
|
15498
|
+
const mine = ++generation;
|
|
15499
|
+
unpin();
|
|
15500
|
+
index([]);
|
|
15501
|
+
items.set([]);
|
|
15502
|
+
expanded.set(/* @__PURE__ */ new Set());
|
|
15503
|
+
focused.set(null);
|
|
15504
|
+
reached = null;
|
|
15505
|
+
place();
|
|
15506
|
+
mark();
|
|
15507
|
+
typed = "";
|
|
15508
|
+
if (!doc) {
|
|
15509
|
+
loading.set(false);
|
|
15510
|
+
return;
|
|
15511
|
+
}
|
|
15512
|
+
loading.set(true);
|
|
15513
|
+
const done = (list) => {
|
|
15514
|
+
if (destroyed || mine !== generation) return;
|
|
15515
|
+
index(list);
|
|
15516
|
+
expanded.set(new Set(nodes.filter((n) => n.item.open && hasChildren(n)).map((n) => n.item.id)));
|
|
15517
|
+
items.set(list);
|
|
15518
|
+
place();
|
|
15519
|
+
reach();
|
|
15520
|
+
loading.set(false);
|
|
15521
|
+
};
|
|
15522
|
+
doc.outline().then(done, () => done([]));
|
|
15523
|
+
}
|
|
15524
|
+
let stopLine = null;
|
|
15525
|
+
function watchViewport(vp) {
|
|
15526
|
+
stopLine?.();
|
|
15527
|
+
stopLine = null;
|
|
15528
|
+
if (vp) {
|
|
15529
|
+
const offLine = vp.readingLine.subscribe(reach);
|
|
15530
|
+
const replace = () => {
|
|
15531
|
+
place();
|
|
15532
|
+
reach();
|
|
15533
|
+
};
|
|
15534
|
+
const offLayout = vp.layout.subscribe(() => {
|
|
15535
|
+
if (pin?.line !== void 0) unpin();
|
|
15536
|
+
replace();
|
|
15537
|
+
});
|
|
15538
|
+
const offPage = vp.currentPage.subscribe(replace);
|
|
15539
|
+
stopLine = () => {
|
|
15540
|
+
offLine();
|
|
15541
|
+
offLayout();
|
|
15542
|
+
offPage();
|
|
15543
|
+
};
|
|
15544
|
+
}
|
|
15545
|
+
place();
|
|
15546
|
+
reach();
|
|
15547
|
+
}
|
|
15548
|
+
const stopDocument = deps.document.subscribe(load);
|
|
15549
|
+
const stopViewport = deps.viewport.subscribe(watchViewport);
|
|
15550
|
+
load(deps.document.get());
|
|
15551
|
+
watchViewport(deps.viewport.get());
|
|
15552
|
+
function toggle(id) {
|
|
15553
|
+
const node = byId.get(id);
|
|
15554
|
+
if (destroyed || !node || !hasChildren(node)) return;
|
|
15555
|
+
const next = new Set(expanded.get());
|
|
15556
|
+
if (!next.delete(id)) next.add(id);
|
|
15557
|
+
setExpanded(next);
|
|
15558
|
+
}
|
|
15559
|
+
function activate(id) {
|
|
15560
|
+
const node = byId.get(id);
|
|
15561
|
+
if (destroyed || !node) return;
|
|
15562
|
+
focused.set(id);
|
|
15563
|
+
const target = node.item.target;
|
|
15564
|
+
const navigation = deps.navigation.get();
|
|
15565
|
+
if (target.kind === "unsupported" || !navigation) return;
|
|
15566
|
+
const follow = navigation.follow(target);
|
|
15567
|
+
if (target.kind !== "resolved") return;
|
|
15568
|
+
unpin();
|
|
15569
|
+
const mine = {
|
|
15570
|
+
node,
|
|
15571
|
+
line: void 0
|
|
15572
|
+
};
|
|
15573
|
+
pin = mine;
|
|
15574
|
+
reach();
|
|
15575
|
+
follow.then(() => {
|
|
15576
|
+
if (pin !== mine) return;
|
|
15577
|
+
cancelSettle = afterFrame(() => {
|
|
15578
|
+
cancelSettle = null;
|
|
15579
|
+
const vp = deps.viewport.get();
|
|
15580
|
+
if (pin !== mine || !vp) return;
|
|
15581
|
+
mine.line = vp.readingLine.get();
|
|
15582
|
+
reach();
|
|
15583
|
+
});
|
|
15584
|
+
});
|
|
15585
|
+
}
|
|
15586
|
+
/** The visible item whose title starts with what was typed, searching on from `from`. */
|
|
15587
|
+
function typeAhead(char, from) {
|
|
15588
|
+
clearTimeout(typedTimer);
|
|
15589
|
+
typed += char.toLowerCase();
|
|
15590
|
+
typedTimer = setTimeout(() => {
|
|
15591
|
+
typed = "";
|
|
15592
|
+
}, TYPE_AHEAD_MS);
|
|
15593
|
+
const list = visible();
|
|
15594
|
+
const at = list.indexOf(from);
|
|
15595
|
+
const start = typed.length === 1 ? at + 1 : at;
|
|
15596
|
+
for (let k = 0; k < list.length; k++) {
|
|
15597
|
+
const node = list[(start + k) % list.length];
|
|
15598
|
+
if (node.item.title.toLowerCase().startsWith(typed)) return node;
|
|
15599
|
+
}
|
|
15600
|
+
}
|
|
15601
|
+
function onKeydown(e) {
|
|
15602
|
+
if (destroyed || e.defaultPrevented || e.isComposing) return;
|
|
15603
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
15604
|
+
const target = e.target;
|
|
15605
|
+
if (target?.ownerDocument) ownerDoc = target.ownerDocument;
|
|
15606
|
+
const el = target && typeof target.closest === "function" ? target.closest("[role=\"treeitem\"]") : null;
|
|
15607
|
+
const stop = tabStop();
|
|
15608
|
+
const node = el && byDomId.get(el.id) || (stop === null ? void 0 : byId.get(stop));
|
|
15609
|
+
if (!node) return;
|
|
15610
|
+
const list = visible();
|
|
15611
|
+
const at = list.indexOf(node);
|
|
15612
|
+
let to;
|
|
15613
|
+
switch (e.key) {
|
|
15614
|
+
case "ArrowDown":
|
|
15615
|
+
to = list[at + 1] ?? node;
|
|
15616
|
+
break;
|
|
15617
|
+
case "ArrowUp":
|
|
15618
|
+
to = list[at - 1] ?? node;
|
|
15619
|
+
break;
|
|
15620
|
+
case "Home":
|
|
15621
|
+
to = list[0];
|
|
15622
|
+
break;
|
|
15623
|
+
case "End":
|
|
15624
|
+
to = list.at(-1);
|
|
15625
|
+
break;
|
|
15626
|
+
case "ArrowRight":
|
|
15627
|
+
to = node;
|
|
15628
|
+
if (hasChildren(node)) {
|
|
15629
|
+
if (!isExpanded(node)) toggle(node.item.id);
|
|
15630
|
+
else to = byId.get(node.item.children[0].id);
|
|
15631
|
+
}
|
|
15632
|
+
break;
|
|
15633
|
+
case "ArrowLeft":
|
|
15634
|
+
to = node;
|
|
15635
|
+
if (hasChildren(node) && isExpanded(node)) toggle(node.item.id);
|
|
15636
|
+
else if (node.parent) to = node.parent;
|
|
15637
|
+
break;
|
|
15638
|
+
case "Enter":
|
|
15639
|
+
e.preventDefault();
|
|
15640
|
+
activate(node.item.id);
|
|
15641
|
+
return;
|
|
15642
|
+
case "*": {
|
|
15643
|
+
const siblings = node.parent ? node.parent.item.children : items.get();
|
|
15644
|
+
const next = new Set(expanded.get());
|
|
15645
|
+
for (const s of siblings) if (s.children.length > 0) next.add(s.id);
|
|
15646
|
+
setExpanded(next);
|
|
15647
|
+
to = node;
|
|
15648
|
+
break;
|
|
15649
|
+
}
|
|
15650
|
+
default:
|
|
15651
|
+
if (e.key.length !== 1 || e.key === " " && typed === "") return;
|
|
15652
|
+
to = typeAhead(e.key, node) ?? node;
|
|
15653
|
+
}
|
|
15654
|
+
e.preventDefault();
|
|
15655
|
+
if (to) moveTo(to.item.id);
|
|
15656
|
+
}
|
|
15657
|
+
/** Scrolls `row` into view inside `scroller` by the least amount; never any ancestor. */
|
|
15658
|
+
function scrollWithin(scroller, row) {
|
|
15659
|
+
const box = scroller.getBoundingClientRect();
|
|
15660
|
+
const r = row.getBoundingClientRect();
|
|
15661
|
+
if (r.top < box.top) scroller.scrollTop -= box.top - r.top;
|
|
15662
|
+
else if (r.bottom > box.bottom) scroller.scrollTop += Math.min(r.bottom - box.bottom, r.top - box.top);
|
|
15663
|
+
}
|
|
15664
|
+
/** The live `bindScroll` bindings, undone by `destroy`. */
|
|
15665
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
15666
|
+
function bindScroll(scroller) {
|
|
15667
|
+
if (destroyed) return () => void 0;
|
|
15668
|
+
ownerDoc = scroller.ownerDocument;
|
|
15669
|
+
const view = ownerDoc.defaultView;
|
|
15670
|
+
/** Set while the current item waits for a hidden list to show. */
|
|
15671
|
+
let pending = false;
|
|
15672
|
+
let frame = 0;
|
|
15673
|
+
function reveal() {
|
|
15674
|
+
const id = current.get();
|
|
15675
|
+
if (id === null) return;
|
|
15676
|
+
const active = ownerDoc?.activeElement;
|
|
15677
|
+
if (active && scroller.contains(active)) return;
|
|
15678
|
+
if (scroller.clientHeight === 0) {
|
|
15679
|
+
pending = true;
|
|
15680
|
+
return;
|
|
15681
|
+
}
|
|
15682
|
+
pending = false;
|
|
15683
|
+
const row = rowFor(id);
|
|
15684
|
+
if (row && scroller.contains(row)) return scrollWithin(scroller, row);
|
|
15685
|
+
if (view) {
|
|
15686
|
+
view.cancelAnimationFrame(frame);
|
|
15687
|
+
frame = view.requestAnimationFrame(() => {
|
|
15688
|
+
const later = current.get() === id ? rowFor(id) : null;
|
|
15689
|
+
if (later && scroller.contains(later)) scrollWithin(scroller, later);
|
|
15690
|
+
});
|
|
15691
|
+
}
|
|
15692
|
+
}
|
|
15693
|
+
const stopCurrent = current.subscribe(reveal);
|
|
15694
|
+
const stopItems = items.subscribe(reveal);
|
|
15695
|
+
const resize = view && "ResizeObserver" in view ? new view.ResizeObserver(() => {
|
|
15696
|
+
if (pending) reveal();
|
|
15697
|
+
}) : null;
|
|
15698
|
+
resize?.observe(scroller);
|
|
15699
|
+
reveal();
|
|
15700
|
+
const unbind = () => {
|
|
15701
|
+
if (!bindings.delete(unbind)) return;
|
|
15702
|
+
stopCurrent();
|
|
15703
|
+
stopItems();
|
|
15704
|
+
resize?.disconnect();
|
|
15705
|
+
if (view) view.cancelAnimationFrame(frame);
|
|
15706
|
+
};
|
|
15707
|
+
bindings.add(unbind);
|
|
15708
|
+
return unbind;
|
|
15709
|
+
}
|
|
15710
|
+
return {
|
|
15711
|
+
items: readonly(items),
|
|
15712
|
+
loading: readonly(loading),
|
|
15713
|
+
expanded: readonly(expanded),
|
|
15714
|
+
current: readonly(current),
|
|
15715
|
+
focused: readonly(focused),
|
|
15716
|
+
toggle,
|
|
15717
|
+
expandAll() {
|
|
15718
|
+
if (destroyed) return;
|
|
15719
|
+
setExpanded(new Set(nodes.filter(hasChildren).map((n) => n.item.id)));
|
|
15720
|
+
},
|
|
15721
|
+
collapseAll() {
|
|
15722
|
+
if (destroyed) return;
|
|
15723
|
+
setExpanded(/* @__PURE__ */ new Set());
|
|
15724
|
+
},
|
|
15725
|
+
activate,
|
|
15726
|
+
treeAttrs: outlineTreeAttrs,
|
|
15727
|
+
titleAttrs: (id) => ({ id: `${domId(id)}-title` }),
|
|
15728
|
+
rowAttrs: (id) => ({ id: `${domId(id)}-row` }),
|
|
15729
|
+
itemAttrs(id) {
|
|
15730
|
+
const node = byId.get(id);
|
|
15731
|
+
return {
|
|
15732
|
+
role: "treeitem",
|
|
15733
|
+
id: domId(id),
|
|
15734
|
+
"aria-level": node ? String(node.level) : void 0,
|
|
15735
|
+
"aria-setsize": node ? String(node.setsize) : void 0,
|
|
15736
|
+
"aria-posinset": node ? String(node.posinset) : void 0,
|
|
15737
|
+
"aria-expanded": node && hasChildren(node) ? String(isExpanded(node)) : void 0,
|
|
15738
|
+
"aria-current": current.get() === id ? "location" : void 0,
|
|
15739
|
+
"aria-disabled": node?.item.target.kind === "unsupported" ? "true" : void 0,
|
|
15740
|
+
"aria-labelledby": `${domId(id)}-title`,
|
|
15741
|
+
tabindex: tabStop() === id ? "0" : "-1"
|
|
15742
|
+
};
|
|
15743
|
+
},
|
|
15744
|
+
onKeydown,
|
|
15745
|
+
bindScroll,
|
|
15746
|
+
destroy() {
|
|
15747
|
+
if (destroyed) return;
|
|
15748
|
+
for (const unbind of [...bindings]) unbind();
|
|
15749
|
+
destroyed = true;
|
|
15750
|
+
unpin();
|
|
15751
|
+
generation++;
|
|
15752
|
+
clearTimeout(typedTimer);
|
|
15753
|
+
stopDocument();
|
|
15754
|
+
stopViewport();
|
|
15755
|
+
stopLine?.();
|
|
15756
|
+
stopLine = null;
|
|
15757
|
+
}
|
|
15758
|
+
};
|
|
15759
|
+
}
|
|
15760
|
+
//#endregion
|
|
15761
|
+
//#region src/dom/outline/outline.ts
|
|
15762
|
+
/**
|
|
15763
|
+
* The open document's outline as a tree. Give it a height (it fills its parent's) and place it
|
|
15764
|
+
* anywhere, a sidebar panel for instance. Clicking an item, or Enter on it, follows it and can be
|
|
15765
|
+
* gone back from; its chevron expands and collapses it. The item the reader has reached is
|
|
15766
|
+
* marked and kept in view. It stops when the viewer is destroyed.
|
|
15767
|
+
*/
|
|
15768
|
+
function createOutline(viewer, opts = {}) {
|
|
15769
|
+
const controller = createOutlineController({
|
|
15770
|
+
document: viewer.document,
|
|
15771
|
+
viewport: viewer.viewport,
|
|
15772
|
+
navigation: navigationOf(viewer)
|
|
15773
|
+
});
|
|
15774
|
+
const tree = h("ul", {
|
|
15775
|
+
class: "ink-outline-tree",
|
|
15776
|
+
...controller.treeAttrs()
|
|
15777
|
+
});
|
|
15778
|
+
const empty = h("p", { class: "ink-outline-empty" }, opts.emptyText ?? "This document has no outline");
|
|
15779
|
+
const root = h("div", {
|
|
15780
|
+
class: "ink-outline",
|
|
15781
|
+
"data-ink-outline": ""
|
|
15782
|
+
}, tree, empty);
|
|
15783
|
+
/** Each item's element and, for items with children, its group. */
|
|
15784
|
+
const rows = /* @__PURE__ */ new Map();
|
|
15785
|
+
function build(list, into) {
|
|
15786
|
+
for (const item of list) {
|
|
15787
|
+
const parent = item.children.length > 0;
|
|
15788
|
+
const row = h("li", {
|
|
15789
|
+
class: "ink-outline-item",
|
|
15790
|
+
"data-ink-outline-item": item.id
|
|
15791
|
+
}, h("div", {
|
|
15792
|
+
class: "ink-outline-row",
|
|
15793
|
+
...controller.rowAttrs(item.id)
|
|
15794
|
+
}, parent ? h("span", {
|
|
15795
|
+
class: "ink-outline-toggle",
|
|
15796
|
+
"aria-hidden": "true"
|
|
15797
|
+
}, createIcon(ChevronRight)) : h("span", {
|
|
15798
|
+
class: "ink-outline-spacer",
|
|
15799
|
+
"aria-hidden": "true"
|
|
15800
|
+
}), h("span", {
|
|
15801
|
+
class: "ink-outline-title",
|
|
15802
|
+
...controller.titleAttrs(item.id)
|
|
15803
|
+
}, item.title)));
|
|
15804
|
+
if (item.bold) row.setAttribute("data-bold", "");
|
|
15805
|
+
if (item.italic) row.setAttribute("data-italic", "");
|
|
15806
|
+
row.style.setProperty("--ink-outline-level", String(item.id.split(".").length - 1));
|
|
15807
|
+
let group = null;
|
|
15808
|
+
if (parent) {
|
|
15809
|
+
group = h("ul", {
|
|
15810
|
+
class: "ink-outline-group",
|
|
15811
|
+
role: "group"
|
|
15812
|
+
});
|
|
15813
|
+
build(item.children, group);
|
|
15814
|
+
row.append(group);
|
|
15815
|
+
}
|
|
15816
|
+
rows.set(item.id, {
|
|
15817
|
+
row,
|
|
15818
|
+
group
|
|
15819
|
+
});
|
|
15820
|
+
into.append(row);
|
|
15821
|
+
}
|
|
15822
|
+
}
|
|
15823
|
+
/** Brings every item's attributes and its group's visibility up to date. */
|
|
15824
|
+
function sync() {
|
|
15825
|
+
const open = controller.expanded.get();
|
|
15826
|
+
for (const [id, { row, group }] of rows) {
|
|
15827
|
+
for (const [name, value] of Object.entries(controller.itemAttrs(id))) if (value === void 0) row.removeAttribute(name);
|
|
15828
|
+
else if (row.getAttribute(name) !== value) row.setAttribute(name, value);
|
|
15829
|
+
if (group) group.hidden = !open.has(id);
|
|
15830
|
+
}
|
|
15831
|
+
}
|
|
15832
|
+
/** The empty text shows once a document is read and has no outline. */
|
|
15833
|
+
function status() {
|
|
15834
|
+
const loading = controller.loading.get();
|
|
15835
|
+
const none = controller.items.get().length === 0;
|
|
15836
|
+
tree.hidden = none;
|
|
15837
|
+
empty.hidden = loading || !none;
|
|
15838
|
+
if (loading) root.setAttribute("aria-busy", "true");
|
|
15839
|
+
else root.removeAttribute("aria-busy");
|
|
15840
|
+
}
|
|
15841
|
+
function render() {
|
|
15842
|
+
rows.clear();
|
|
15843
|
+
tree.replaceChildren();
|
|
15844
|
+
build(controller.items.get(), tree);
|
|
15845
|
+
sync();
|
|
15846
|
+
status();
|
|
15847
|
+
}
|
|
15848
|
+
/**
|
|
15849
|
+
* The item whose row holds `target`. Only rows take clicks, as in the bindings: a click on a
|
|
15850
|
+
* group's padding or the item around its row belongs to no item, not to the parent.
|
|
15851
|
+
*/
|
|
15852
|
+
const itemOf = (target) => {
|
|
15853
|
+
const row = target instanceof Element ? target.closest(".ink-outline-row")?.closest("[data-ink-outline-item]") : null;
|
|
15854
|
+
return row && root.contains(row) ? row.dataset.inkOutlineItem ?? null : null;
|
|
15855
|
+
};
|
|
15856
|
+
tree.addEventListener("click", (e) => {
|
|
15857
|
+
const id = itemOf(e.target);
|
|
15858
|
+
if (id === null) return;
|
|
15859
|
+
if (e.target.closest(".ink-outline-toggle")) controller.toggle(id);
|
|
15860
|
+
else controller.activate(id);
|
|
15861
|
+
});
|
|
15862
|
+
tree.addEventListener("keydown", (e) => controller.onKeydown(e));
|
|
15863
|
+
render();
|
|
15864
|
+
const stops = [
|
|
15865
|
+
controller.items.subscribe(render),
|
|
15866
|
+
controller.loading.subscribe(status),
|
|
15867
|
+
controller.expanded.subscribe(sync),
|
|
15868
|
+
controller.current.subscribe(sync),
|
|
15869
|
+
controller.focused.subscribe(sync)
|
|
15870
|
+
];
|
|
15871
|
+
const unbind = controller.bindScroll(root);
|
|
15872
|
+
onViewerDestroy(viewer, () => {
|
|
15873
|
+
unbind();
|
|
15874
|
+
stops.forEach((stop) => stop());
|
|
15875
|
+
controller.destroy();
|
|
15876
|
+
});
|
|
15877
|
+
return root;
|
|
15878
|
+
}
|
|
15879
|
+
//#endregion
|
|
14427
15880
|
//#region src/dom/viewer/style-menu.ts
|
|
14428
15881
|
/** A button that opens the line style menu: weight, solid or dashed, and arrowheads. */
|
|
14429
15882
|
function createLineStyleMenu(viewer) {
|
|
@@ -15120,6 +16573,6 @@ function createHighlightMenu(viewer) {
|
|
|
15120
16573
|
return root;
|
|
15121
16574
|
}
|
|
15122
16575
|
//#endregion
|
|
15123
|
-
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, FORM_LABELS, 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, THUMBNAIL_WIDTH, TOOL_LABELS, ZOOM_LEVELS, allTools, annotationElementFor, arrowheadChoice, attachFileKeys, attachHistoryKeys, attachTouchPinch, attachTrackpadPinch, attachViewportKeys, clampFontSize, colorMenuModel, computeCssLayout, createColorMenu, createCommandButton, createDownloadButton, createFindBar, createFocusReturn, createFormAlert, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPluginHost, createPluginLayers, createPopover, createPrevPage, createPrintButton, createRecordList, createRecordListController, createRedo, createSearchButton, createSearchController, createSearchResults, createShapeMenu, createSidebar, createSidebarController, createSignFieldMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createThumbnail, createThumbnailPainter, createThumbnails, createThumbnailsController, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomPreview, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, definePlugin, downloadDocument, drawTool, editFreeText, findAnnouncer, findCountText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleFindKey, handleMenuKeys, handleRadioKeys, handleResultsFieldKey, handleTabKeys, highlightMenuModel, highlightTool, hitFor, isBlankQuery, keepHistoryFocus, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, pageSelectionText, pinchFrame, placePanel, printDocument, recordsSource, redactAreaTool, renderAnnotationContent, renderTypedSignature, searchSource, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, sidebarBadge, signField, signatureMenuModel, signaturePreview, signatureTool, sketchTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, thumbnailSize, trackToolStyle, trapTab, unknownCommand, unknownTool, viewerSearchOf, visibleRangeCss, watchDetached, wheelFactor, withHostTools };
|
|
16576
|
+
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, FORM_LABELS, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIDEBAR_WIDTH, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, THUMBNAIL_WIDTH, TOOL_LABELS, ZOOM_LEVELS, allTools, annotationElementFor, arrowheadChoice, attachFileKeys, attachHistoryKeys, attachTouchPinch, attachTrackpadPinch, attachViewportKeys, clampFontSize, colorMenuModel, computeCssLayout, createBack, createColorMenu, createCommandButton, createDownloadButton, createFindBar, createFocusReturn, createFormAlert, createForward, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOutline, createOutlineController, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPluginHost, createPluginLayers, createPopover, createPrevPage, createPrintButton, createRecordList, createRecordListController, createRedo, createSearchButton, createSearchController, createSearchResults, createShapeMenu, createSidebar, createSidebarController, createSignFieldMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createThumbnail, createThumbnailPainter, createThumbnails, createThumbnailsController, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomPreview, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, definePlugin, downloadDocument, drawTool, editFreeText, findAnnouncer, findCountText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleFindKey, handleMenuKeys, handleRadioKeys, handleResultsFieldKey, handleTabKeys, highlightMenuModel, highlightTool, hitFor, isBlankQuery, keepHistoryFocus, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, outlineTreeAttrs, pageSelectionText, pinchFrame, placePanel, printDocument, recordsSource, redactAreaTool, renderAnnotationContent, renderTypedSignature, searchSource, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, sidebarBadge, signField, signatureMenuModel, signaturePreview, signatureTool, sketchTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, thumbnailSize, trackToolStyle, trapTab, unknownCommand, unknownTool, viewerSearchOf, visibleRangeCss, watchDetached, wheelFactor, withHostTools };
|
|
15124
16577
|
|
|
15125
16578
|
//# sourceMappingURL=dom.mjs.map
|