@inkbar/core 0.10.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/dist/dom.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as deriveRect, B as isBubble, Bt as freeTextBox, C as isThreadRoot, Cn as isInkError, D as withoutReplies, E as threadRoot, G as resizedBox, H as keepsAspect, Ht as withCalloutBounds, J as translated, K as resizedRect, L as hasCallout, Qt as isKnown, R as hasCornerHandles, Sn as InkError, T as threadOf, U as movedEndpoint, V as isTextMarkup, Vt as isStandardCallout, W as movedTip, Wt as cssVariableDefaults, X as openDocument, Y as withBox, Yt as LINE_ENDINGS, Z as isBlankQuery, _ as constrain, _n as rectsIntersect, a as createInkTemplate, an as applyMatrix, b as shapeInput, c as createSignatureLibrary, en as mergeLineRects, f as recogniseStroke, g as SHAPE_KINDS, h as DEFAULT_SHAPE_SIZE, i as createImageTemplate, in as writable, k as createToolStyle, m as DEFAULT_SHAPE_OPTIONS, mn as rectFromPoints, nn as derived, p as DEFAULT_CORNER_RADIUS, q as scaled, qt as sameValue, rn as readonly, rt as isProduction, s as createMemorySignatureStore, sn as invertMatrix, t as signField, tn as createRecordStore, u as placeSignature, v as defaultEnds, x as isAbandonedNote, y as isPointerOnlyKind, z as hasRectGeometry, zt as calloutStroke } from "./sign-DZVDJhqT.mjs";
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. */
@@ -228,6 +228,347 @@ function mostVisiblePageCss(layout, scrollTop, viewportHeight) {
228
228
  return best;
229
229
  }
230
230
  //#endregion
231
+ //#region src/dom/pinch.ts
232
+ const midpoint = (a, b) => ({
233
+ x: (a.x + b.x) / 2,
234
+ y: (a.y + b.y) / 2
235
+ });
236
+ function pinchFrame(start, now, startScale, minZoom, maxZoom) {
237
+ const from = Math.hypot(start[1].x - start[0].x, start[1].y - start[0].y);
238
+ const to = Math.hypot(now[1].x - now[0].x, now[1].y - now[0].y);
239
+ const zoom = from > 0 ? Math.min(maxZoom, Math.max(minZoom, startScale * to / from)) : startScale;
240
+ const anchor = midpoint(start[0], start[1]);
241
+ const mid = midpoint(now[0], now[1]);
242
+ return {
243
+ zoom,
244
+ factor: from > 0 ? zoom / startScale : 1,
245
+ anchor,
246
+ pan: {
247
+ x: mid.x - anchor.x,
248
+ y: mid.y - anchor.y
249
+ }
250
+ };
251
+ }
252
+ const SETTLE_MS = 150;
253
+ /**
254
+ * The pinch preview: a transform on the pages element, which the browser composites without
255
+ * laying anything out. `settle` covers what a preview can show but the committed layout
256
+ * cannot reach (mainly scroll clamped at the content edges): the pages start where the fingers
257
+ * left them and slide into place.
258
+ */
259
+ function createZoomPreview(container, pagesEl) {
260
+ const win = container.ownerDocument.defaultView ?? window;
261
+ let frame = 0;
262
+ let timer;
263
+ function stopSettling() {
264
+ if (frame) win.cancelAnimationFrame(frame);
265
+ frame = 0;
266
+ if (timer !== void 0) clearTimeout(timer);
267
+ timer = void 0;
268
+ pagesEl.removeEventListener("transitionend", onTransitionEnd);
269
+ pagesEl.style.removeProperty("transition");
270
+ }
271
+ function settled() {
272
+ stopSettling();
273
+ pagesEl.style.removeProperty("transform");
274
+ }
275
+ const onTransitionEnd = (e) => {
276
+ if (e.target === pagesEl && e.propertyName === "transform") settled();
277
+ };
278
+ function clear() {
279
+ stopSettling();
280
+ pagesEl.style.removeProperty("transform");
281
+ pagesEl.style.removeProperty("transform-origin");
282
+ }
283
+ return {
284
+ show(factor, anchor, pan) {
285
+ stopSettling();
286
+ const x = anchor.x + container.scrollLeft - pagesEl.offsetLeft;
287
+ const y = anchor.y + container.scrollTop - pagesEl.offsetTop;
288
+ pagesEl.style.transformOrigin = `${x}px ${y}px`;
289
+ pagesEl.style.transform = `translate(${pan.x}px, ${pan.y}px) scale(${factor})`;
290
+ },
291
+ clear,
292
+ settle(offset) {
293
+ clear();
294
+ pagesEl.style.transform = `translate(${offset.x}px, ${offset.y}px)`;
295
+ frame = win.requestAnimationFrame(() => {
296
+ frame = 0;
297
+ getComputedStyle(pagesEl).transform;
298
+ pagesEl.style.transition = `transform ${SETTLE_MS}ms ease-out`;
299
+ pagesEl.style.removeProperty("transform");
300
+ pagesEl.addEventListener("transitionend", onTransitionEnd);
301
+ timer = setTimeout(settled, 200);
302
+ });
303
+ },
304
+ destroy: clear
305
+ };
306
+ }
307
+ /**
308
+ * Tracks touches on the container with touch events, which every touch browser sends as one
309
+ * stream (pointer events may be cancelled once the browser decides to pan). The first two
310
+ * fingers down start a pinch; a third is ignored. The pinch ends when either of its fingers
311
+ * lifts or is cancelled, and any finger still down then starts nothing until every finger has
312
+ * lifted. While two fingers are down their moves are prevented, so the browser neither pans
313
+ * nor zooms underneath; one finger's moves never are, so a one-finger scroll stays native. A
314
+ * move the browser will not let us cancel means it is scrolling natively already: that travel
315
+ * is left to it, and only what the fingers do after it is scrolled here.
316
+ * The viewport claims pinch on the container (`touch-action: pan-x pan-y`), which keeps the
317
+ * page from zooming as well; the tool controller turns off one-finger panning on the pages
318
+ * element while a drag tool is ready, and two fingers still land here. A finger
319
+ * that lands alone starts afresh, so an end the browser never sent leaves nothing behind.
320
+ */
321
+ function attachTouchPinch(opts) {
322
+ const { container, preview } = opts;
323
+ let pinch;
324
+ let spent = false;
325
+ /** The touches that went down on the container, by identifier. */
326
+ const mine = /* @__PURE__ */ new Set();
327
+ const down = (list) => [...list].filter((t) => mine.has(t.identifier));
328
+ function at(t) {
329
+ const r = container.getBoundingClientRect();
330
+ return {
331
+ x: t.clientX - r.left - container.clientLeft,
332
+ y: t.clientY - r.top - container.clientTop
333
+ };
334
+ }
335
+ const onStart = (e) => {
336
+ const live = new Set([...e.touches].map((t) => t.identifier));
337
+ for (const id of mine) if (!live.has(id)) mine.delete(id);
338
+ for (const t of e.changedTouches) mine.add(t.identifier);
339
+ const fingers = down(e.touches);
340
+ if (fingers.length === 1) {
341
+ if (pinch) preview.clear();
342
+ pinch = void 0;
343
+ spent = false;
344
+ }
345
+ if (pinch || spent || fingers.length < 2) return;
346
+ const [a, b] = fingers;
347
+ const start = [at(a), at(b)];
348
+ const scale = opts.scale();
349
+ pinch = {
350
+ ids: [a.identifier, b.identifier],
351
+ start,
352
+ scale,
353
+ last: pinchFrame(start, start, scale, opts.minZoom, opts.maxZoom),
354
+ scrolled: {
355
+ x: 0,
356
+ y: 0
357
+ },
358
+ mid: midpoint(start[0], start[1])
359
+ };
360
+ };
361
+ const onMove = (e) => {
362
+ if (down(e.touches).length >= 2 && e.cancelable) e.preventDefault();
363
+ if (!pinch) return;
364
+ const ids = pinch.ids;
365
+ const touches = [...e.touches];
366
+ const a = touches.find((t) => t.identifier === ids[0]);
367
+ const b = touches.find((t) => t.identifier === ids[1]);
368
+ if (!a || !b) return;
369
+ const now = [at(a), at(b)];
370
+ const mid = midpoint(now[0], now[1]);
371
+ const native = !e.cancelable;
372
+ if (native) {
373
+ const dx = mid.x - pinch.mid.x;
374
+ const dy = mid.y - pinch.mid.y;
375
+ pinch.start = [{
376
+ x: pinch.start[0].x + dx,
377
+ y: pinch.start[0].y + dy
378
+ }, {
379
+ x: pinch.start[1].x + dx,
380
+ y: pinch.start[1].y + dy
381
+ }];
382
+ }
383
+ pinch.mid = mid;
384
+ const frame = pinchFrame(pinch.start, now, pinch.scale, opts.minZoom, opts.maxZoom);
385
+ const { scrolled } = pinch;
386
+ if (!native) {
387
+ const took = opts.scrollBy({
388
+ x: frame.pan.x - scrolled.x,
389
+ y: frame.pan.y - scrolled.y
390
+ });
391
+ scrolled.x += took.x;
392
+ scrolled.y += took.y;
393
+ }
394
+ pinch.last = {
395
+ ...frame,
396
+ anchor: {
397
+ x: frame.anchor.x + scrolled.x,
398
+ y: frame.anchor.y + scrolled.y
399
+ },
400
+ pan: {
401
+ x: frame.pan.x - scrolled.x,
402
+ y: frame.pan.y - scrolled.y
403
+ }
404
+ };
405
+ preview.show(pinch.last.factor, pinch.last.anchor, pinch.last.pan);
406
+ };
407
+ const onEnd = (e) => {
408
+ const ended = [...e.changedTouches].map((t) => t.identifier);
409
+ for (const id of ended) mine.delete(id);
410
+ if (pinch && pinch.ids.some((id) => ended.includes(id))) {
411
+ const { last } = pinch;
412
+ pinch = void 0;
413
+ spent = true;
414
+ preview.clear();
415
+ opts.commit(last);
416
+ }
417
+ if (down(e.touches).length === 0) spent = false;
418
+ };
419
+ const listen = { passive: false };
420
+ container.addEventListener("touchstart", onStart, listen);
421
+ container.addEventListener("touchmove", onMove, listen);
422
+ container.addEventListener("touchend", onEnd, listen);
423
+ container.addEventListener("touchcancel", onEnd, listen);
424
+ return { destroy() {
425
+ container.removeEventListener("touchstart", onStart);
426
+ container.removeEventListener("touchmove", onMove);
427
+ container.removeEventListener("touchend", onEnd);
428
+ container.removeEventListener("touchcancel", onEnd);
429
+ mine.clear();
430
+ pinch = void 0;
431
+ spent = false;
432
+ } };
433
+ }
434
+ const WHEEL_STEP = 1.1;
435
+ /**
436
+ * The zoom factor one wheel event stands for. A trackpad pinch arrives as ctrl+wheel events
437
+ * with small pixel deltas, which zoom smoothly. A mouse wheel notch with ctrl held arrives in
438
+ * line or page mode, or as one large pixel delta, and is one small step, whatever its size.
439
+ */
440
+ function wheelFactor(e) {
441
+ if (e.deltaMode !== 0 || Math.abs(e.deltaY) >= 50) return e.deltaY < 0 ? WHEEL_STEP : e.deltaY > 0 ? 1 / WHEEL_STEP : 1;
442
+ return Math.exp(-e.deltaY / 100);
443
+ }
444
+ const IDLE_MS = 150;
445
+ const TOUCH_TYPES = [
446
+ "touchstart",
447
+ "touchend",
448
+ "touchcancel"
449
+ ];
450
+ /**
451
+ * A trackpad pinch over the container. Browsers report it as ctrl+wheel events, which are
452
+ * taken over so the page does not zoom; a run of them is one pinch, anchored at the pointer
453
+ * where it began and committed once the events stop, or as soon as the container scrolls (a
454
+ * plain wheel, a scroll bar), so the scroll moves the committed layout and not the preview.
455
+ * Safari also has gesture events, which give the scale directly; while one of those runs,
456
+ * ctrl+wheel is ignored so nothing counts twice. Safari on a touch screen sends gesture events
457
+ * for a touch pinch too, which `attachTouchPinch` already handles: while a finger is down on
458
+ * the container they are prevented and otherwise ignored. The wheel listener is non-passive
459
+ * because it has to call `preventDefault`.
460
+ */
461
+ function attachTrackpadPinch(opts) {
462
+ const { container, preview } = opts;
463
+ const win = container.ownerDocument.defaultView ?? window;
464
+ let run;
465
+ let gesturing = false;
466
+ /** A finger is down on the container. */
467
+ let touching = false;
468
+ let timer;
469
+ function at(e) {
470
+ const r = container.getBoundingClientRect();
471
+ return {
472
+ x: e.clientX - r.left - container.clientLeft,
473
+ y: e.clientY - r.top - container.clientTop
474
+ };
475
+ }
476
+ function update(e, next) {
477
+ if (!run) {
478
+ const scale = opts.scale();
479
+ const anchor = at(e);
480
+ run = {
481
+ anchor,
482
+ scale,
483
+ factor: 1,
484
+ last: pinchFrame([anchor, anchor], [anchor, anchor], scale, opts.minZoom, opts.maxZoom)
485
+ };
486
+ }
487
+ run.factor = next(run.factor);
488
+ const zoom = Math.min(opts.maxZoom, Math.max(opts.minZoom, run.scale * run.factor));
489
+ run.last = {
490
+ zoom,
491
+ factor: zoom / run.scale,
492
+ anchor: run.anchor,
493
+ pan: {
494
+ x: 0,
495
+ y: 0
496
+ }
497
+ };
498
+ preview.show(run.last.factor, run.last.anchor, run.last.pan);
499
+ }
500
+ function end() {
501
+ if (timer !== void 0) clearTimeout(timer);
502
+ timer = void 0;
503
+ if (!run) return;
504
+ const { last } = run;
505
+ run = void 0;
506
+ preview.clear();
507
+ opts.commit(last);
508
+ }
509
+ const onWheel = (e) => {
510
+ if (!e.ctrlKey) {
511
+ if (!gesturing) end();
512
+ return;
513
+ }
514
+ if (gesturing) return;
515
+ e.preventDefault();
516
+ update(e, (f) => f * wheelFactor(e));
517
+ if (timer !== void 0) clearTimeout(timer);
518
+ timer = setTimeout(end, IDLE_MS);
519
+ };
520
+ const onScroll = () => {
521
+ if (!gesturing) end();
522
+ };
523
+ const onTouch = (e) => {
524
+ touching = e.type === "touchstart" || e.touches.length > 0;
525
+ };
526
+ const onGestureStart = (e) => {
527
+ e.preventDefault();
528
+ if (touching) return;
529
+ if (timer !== void 0) clearTimeout(timer);
530
+ timer = void 0;
531
+ end();
532
+ gesturing = true;
533
+ update(e, () => 1);
534
+ };
535
+ const onGestureChange = (e) => {
536
+ e.preventDefault();
537
+ if (!gesturing) return;
538
+ update(e, () => e.scale);
539
+ };
540
+ const onGestureEnd = (e) => {
541
+ e.preventDefault();
542
+ if (!gesturing) return;
543
+ gesturing = false;
544
+ end();
545
+ };
546
+ container.addEventListener("wheel", onWheel, { passive: false });
547
+ container.addEventListener("scroll", onScroll, { passive: true });
548
+ const hasGestures = "GestureEvent" in win;
549
+ if (hasGestures) {
550
+ for (const type of TOUCH_TYPES) container.addEventListener(type, onTouch, { passive: true });
551
+ container.addEventListener("gesturestart", onGestureStart);
552
+ container.addEventListener("gesturechange", onGestureChange);
553
+ container.addEventListener("gestureend", onGestureEnd);
554
+ }
555
+ return { destroy() {
556
+ container.removeEventListener("wheel", onWheel);
557
+ container.removeEventListener("scroll", onScroll);
558
+ if (hasGestures) {
559
+ for (const type of TOUCH_TYPES) container.removeEventListener(type, onTouch);
560
+ container.removeEventListener("gesturestart", onGestureStart);
561
+ container.removeEventListener("gesturechange", onGestureChange);
562
+ container.removeEventListener("gestureend", onGestureEnd);
563
+ }
564
+ if (timer !== void 0) clearTimeout(timer);
565
+ timer = void 0;
566
+ run = void 0;
567
+ gesturing = false;
568
+ preview.clear();
569
+ } };
570
+ }
571
+ //#endregion
231
572
  //#region src/dom/viewport.ts
232
573
  function sameList$1(a, b) {
233
574
  return a.length === b.length && a.every((v, i) => v === b[i]);
@@ -244,6 +585,7 @@ function createViewport(opts) {
244
585
  const rotation = writable(opts.rotation ?? 0);
245
586
  const mode = writable(opts.mode ?? "continuous");
246
587
  const currentPage = writable(0);
588
+ const readingLine = writable(0);
247
589
  const mounted = writable([]);
248
590
  const layout = writable({
249
591
  boxes: [],
@@ -252,10 +594,13 @@ function createViewport(opts) {
252
594
  });
253
595
  const previousStyle = {
254
596
  overflow: container.style.overflow,
255
- position: container.style.position
597
+ position: container.style.position,
598
+ touchAction: container.style.touchAction
256
599
  };
257
600
  container.dataset.inkViewport = "";
258
601
  container.style.overflow = "auto";
602
+ const pinchZoom = opts.pinchZoom ?? true;
603
+ if (pinchZoom) container.style.touchAction = "pan-x pan-y";
259
604
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
260
605
  const pagesEl = container.ownerDocument.createElement("div");
261
606
  pagesEl.className = "ink-pages";
@@ -359,6 +704,9 @@ function createViewport(opts) {
359
704
  if (destroyed) return;
360
705
  const l = layout.get();
361
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));
362
710
  const wanted = visibleRangeCss(l, top, container.clientHeight, buffer);
363
711
  const wantedSet = new Set(wanted);
364
712
  for (const [page, host] of [...hosts]) {
@@ -381,6 +729,70 @@ function createViewport(opts) {
381
729
  if (!sameList$1(mounted.get(), wanted)) mounted.set(wanted);
382
730
  if (mode.get() === "continuous") currentPage.set(mostVisiblePageCss(l, top, container.clientHeight));
383
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
+ }
384
796
  let scrollFrame = 0;
385
797
  const onScroll = () => {
386
798
  if (scrollFrame) return;
@@ -422,6 +834,21 @@ function createViewport(opts) {
422
834
  }
423
835
  return best;
424
836
  }
837
+ /** The page and user-space point under `anchor` (container-relative), from the current layout. */
838
+ function userAt(anchor) {
839
+ const o = origin();
840
+ const x = container.scrollLeft + anchor.x - o.x;
841
+ const y = container.scrollTop + anchor.y - o.y;
842
+ const box = anchorBox(layout.get(), y);
843
+ if (!box) return void 0;
844
+ return {
845
+ page: box.index,
846
+ user: transformFor(box.index).fromCss({
847
+ x: x - box.left,
848
+ y: y - box.top
849
+ })
850
+ };
851
+ }
425
852
  /**
426
853
  * Keeps the user-space point under `anchorCss` still while `change` alters the layout.
427
854
  * The anchor is taken in layout space, so it holds in the margins and gaps too: there it
@@ -433,25 +860,103 @@ function createViewport(opts) {
433
860
  x: container.clientWidth / 2,
434
861
  y: container.clientHeight / 2
435
862
  };
436
- const o = origin();
437
- const x = container.scrollLeft + anchor.x - o.x;
438
- const y = container.scrollTop + anchor.y - o.y;
439
- const old = anchorBox(layout.get(), y);
440
- const user = old ? transformFor(old.index).fromCss({
441
- x: x - old.left,
442
- y: y - old.top
443
- }) : void 0;
863
+ const at = userAt(anchor);
444
864
  change();
445
865
  relayout();
446
- if (!old || !user) return;
447
- const box = boxFor(old.index);
866
+ if (!at) return;
867
+ const box = boxFor(at.page);
448
868
  if (!box) return;
449
- const after = transformFor(old.index).toCss(user);
450
- const o2 = origin();
451
- container.scrollLeft = o2.x + box.left + after.x - anchor.x;
452
- container.scrollTop = o2.y + box.top + after.y - anchor.y;
869
+ const after = transformFor(at.page).toCss(at.user);
870
+ const o = origin();
871
+ container.scrollLeft = o.x + box.left + after.x - anchor.x;
872
+ container.scrollTop = o.y + box.top + after.y - anchor.y;
873
+ sync();
874
+ }
875
+ /**
876
+ * The translation a pinch preview applies, from the current layout. While the zoomed pages
877
+ * would still be narrower than the container, the committed layout centres them (and leaves
878
+ * nothing to scroll sideways), so the preview puts them there instead of following the fingers.
879
+ */
880
+ function previewPan(factor, anchor, pan) {
881
+ const l = layout.get();
882
+ const widest = l.boxes.reduce((max, b) => Math.max(max, b.width), 0);
883
+ const inner = innerSize().width;
884
+ if (widest * factor >= inner) return pan;
885
+ const o = origin();
886
+ const ox = anchor.x + container.scrollLeft - o.x;
887
+ const shown = o.x - container.scrollLeft + ox + (l.width / 2 - ox) * factor + pan.x;
888
+ const centred = o.x + inner / 2;
889
+ return {
890
+ x: pan.x + centred - shown,
891
+ y: pan.y
892
+ };
893
+ }
894
+ /**
895
+ * Ends a pinch: one zoom change around the point that started between the fingers, then any
896
+ * travel the touch pinch could not scroll as it went (all of it, for a trackpad pinch) as a
897
+ * scroll, so that point ends up where the preview last showed it.
898
+ * When the scroll's edges stop it getting there, the preview eases the difference out. A
899
+ * pinch that did not zoom changes no zoom, so a fit mode stays: a two-finger pan only
900
+ * scrolls, and a two-finger tap changes nothing.
901
+ */
902
+ function commitPinch(frame) {
903
+ if (destroyed) return;
904
+ const zoomed = Math.abs(frame.factor - 1) >= .001;
905
+ if (!zoomed && Math.hypot(frame.pan.x, frame.pan.y) < 1) return;
906
+ const at = userAt(frame.anchor);
907
+ const shown = previewPan(frame.factor, frame.anchor, frame.pan);
908
+ if (zoomed) withAnchor(frame.anchor, () => zoom.set(clamp(frame.zoom)));
909
+ container.scrollLeft -= frame.pan.x;
910
+ container.scrollTop -= frame.pan.y;
453
911
  sync();
912
+ const box = at && boxFor(at.page);
913
+ if (!at || !box) return;
914
+ const css = transformFor(at.page).toCss(at.user);
915
+ const o = origin();
916
+ const actual = {
917
+ x: o.x + box.left + css.x - container.scrollLeft,
918
+ y: o.y + box.top + css.y - container.scrollTop
919
+ };
920
+ const dx = frame.anchor.x + shown.x - actual.x;
921
+ const dy = frame.anchor.y + shown.y - actual.y;
922
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) preview?.settle({
923
+ x: dx,
924
+ y: dy
925
+ });
454
926
  }
927
+ const basePreview = pinchZoom ? createZoomPreview(container, pagesEl) : void 0;
928
+ const preview = basePreview && {
929
+ ...basePreview,
930
+ show: (factor, anchor, pan) => basePreview.show(factor, anchor, previewPan(factor, anchor, pan))
931
+ };
932
+ const pinch = preview ? attachTouchPinch({
933
+ container,
934
+ preview,
935
+ scale: () => scale.get(),
936
+ minZoom,
937
+ maxZoom,
938
+ scrollBy: (delta) => {
939
+ const { scrollLeft, scrollTop } = container;
940
+ container.scrollTo({
941
+ left: scrollLeft - delta.x,
942
+ top: scrollTop - delta.y,
943
+ behavior: "instant"
944
+ });
945
+ return {
946
+ x: scrollLeft - container.scrollLeft,
947
+ y: scrollTop - container.scrollTop
948
+ };
949
+ },
950
+ commit: commitPinch
951
+ }) : void 0;
952
+ const trackpadPinch = preview ? attachTrackpadPinch({
953
+ container,
954
+ preview,
955
+ scale: () => scale.get(),
956
+ minZoom,
957
+ maxZoom,
958
+ commit: commitPinch
959
+ }) : void 0;
455
960
  const viewport = {
456
961
  zoom: readonly(zoom),
457
962
  scale: readonly(scale),
@@ -517,12 +1022,36 @@ function createViewport(opts) {
517
1022
  if (to.top !== void 0 || to.left !== void 0) container.scrollTo(to);
518
1023
  sync();
519
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),
520
1046
  transformFor,
521
1047
  hostFor: (page) => hosts.get(page),
522
1048
  pageAtCss,
523
1049
  destroy() {
524
1050
  if (destroyed) return;
525
1051
  destroyed = true;
1052
+ pinch?.destroy();
1053
+ trackpadPinch?.destroy();
1054
+ preview?.destroy();
526
1055
  ro.disconnect();
527
1056
  container.removeEventListener("scroll", onScroll);
528
1057
  if (scrollFrame) cancelAnimationFrame(scrollFrame);
@@ -536,6 +1065,7 @@ function createViewport(opts) {
536
1065
  delete container.dataset.inkViewport;
537
1066
  container.style.overflow = previousStyle.overflow;
538
1067
  container.style.position = previousStyle.position;
1068
+ container.style.touchAction = previousStyle.touchAction;
539
1069
  }
540
1070
  };
541
1071
  relayout();
@@ -704,9 +1234,9 @@ function attachHistoryKeys(el, history) {
704
1234
  return () => el.removeEventListener("keydown", onKeydown);
705
1235
  }
706
1236
  /**
707
- * Keeps focus in the viewer when a focused Undo or Redo button inside `el` turns disabled
708
- * (it undid or redid the last step), which would otherwise drop focus to `<body>`, out of
709
- * reach of the undo and redo keys. Focus moves to `scroll()`, the viewer's scroll element,
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,
710
1240
  * without scrolling. Returns a function that removes the listeners.
711
1241
  *
712
1242
  * Chrome and Firefox blur the button as it is disabled: its `focusout` comes while the
@@ -715,7 +1245,7 @@ function attachHistoryKeys(el, history) {
715
1245
  * `focusout` (a click on content that takes no focus) is left alone.
716
1246
  */
717
1247
  function keepHistoryFocus(el, scroll) {
718
- 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]");
719
1249
  const toScroll = () => {
720
1250
  const target = scroll();
721
1251
  if (!target?.isConnected) return;
@@ -3730,6 +4260,7 @@ function createPluginHost(opts) {
3730
4260
  if (seen.has(plugin.id)) throw new InkError("DuplicatePlugin", `A plugin with id ${plugin.id} is registered twice`, { id: plugin.id });
3731
4261
  seen.add(plugin.id);
3732
4262
  }
4263
+ const storage = opts.storage ?? createMemoryStorage();
3733
4264
  const registry = createCommandRegistry({ document: opts.document });
3734
4265
  const tools = [];
3735
4266
  const decorators = [];
@@ -3744,6 +4275,7 @@ function createPluginHost(opts) {
3744
4275
  const editFns = [];
3745
4276
  const setupCleanups = [];
3746
4277
  const redecorators = /* @__PURE__ */ new Set();
4278
+ const pressListeners = /* @__PURE__ */ new Set();
3747
4279
  const marks = createMarksRegistry();
3748
4280
  const selections = /* @__PURE__ */ new Map();
3749
4281
  let opened = null;
@@ -3784,8 +4316,21 @@ function createPluginHost(opts) {
3784
4316
  for (const request of requests) request.current.set(null);
3785
4317
  for (const cleanup of state.cleanups.reverse()) guard(cleanup);
3786
4318
  };
3787
- const contextFor = (plugin) => {
4319
+ const contextFor = (plugin, owned) => {
3788
4320
  const storeNames = /* @__PURE__ */ new Set();
4321
+ const listenForPress = (kind) => (fn) => {
4322
+ const entry = {
4323
+ plugin: plugin.id,
4324
+ kind,
4325
+ fn
4326
+ };
4327
+ pressListeners.add(entry);
4328
+ owned.add(entry);
4329
+ return () => {
4330
+ pressListeners.delete(entry);
4331
+ owned.delete(entry);
4332
+ };
4333
+ };
3789
4334
  const ctx = {
3790
4335
  id: plugin.id,
3791
4336
  document: opts.document,
@@ -3834,7 +4379,10 @@ function createPluginHost(opts) {
3834
4379
  scrollTo(page, rect, scrollOpts) {
3835
4380
  opts.viewport?.()?.scrollToRect(page, rect, scrollOpts);
3836
4381
  },
3837
- search: opts.search ?? inertSearch
4382
+ search: opts.search ?? inertSearch,
4383
+ storage: scopeStorage(storage, `plugin.${plugin.id}`),
4384
+ onPress: listenForPress("press"),
4385
+ onLongPress: listenForPress("long-press")
3838
4386
  };
3839
4387
  internals$1.set(ctx, {
3840
4388
  marks,
@@ -3913,13 +4461,22 @@ function createPluginHost(opts) {
3913
4461
  };
3914
4462
  for (const plugin of opts.plugins) {
3915
4463
  let cleanup;
4464
+ const owned = /* @__PURE__ */ new Set();
4465
+ const dropPresses = () => {
4466
+ for (const entry of owned) pressListeners.delete(entry);
4467
+ owned.clear();
4468
+ };
3916
4469
  try {
3917
- cleanup = plugin.setup(contextFor(plugin));
4470
+ cleanup = plugin.setup(contextFor(plugin, owned));
3918
4471
  } catch (error) {
4472
+ dropPresses();
3919
4473
  for (const done of setupCleanups.reverse()) guard(done);
3920
4474
  throw error;
3921
4475
  }
3922
- if (typeof cleanup === "function") setupCleanups.push(cleanup);
4476
+ setupCleanups.push(() => {
4477
+ dropPresses();
4478
+ if (typeof cleanup === "function") cleanup();
4479
+ });
3923
4480
  }
3924
4481
  const current = opts.document.get();
3925
4482
  if (current) open(current);
@@ -3968,6 +4525,16 @@ function createPluginHost(opts) {
3968
4525
  redecorators.delete(run);
3969
4526
  };
3970
4527
  },
4528
+ emitPress(kind, press) {
4529
+ for (const entry of [...pressListeners]) {
4530
+ if (entry.kind !== kind || !pressListeners.has(entry)) continue;
4531
+ try {
4532
+ entry.fn(press);
4533
+ } catch (error) {
4534
+ console.error(`Plugin ${entry.plugin} threw in a ${kind} listener`, error);
4535
+ }
4536
+ }
4537
+ },
3971
4538
  destroy() {
3972
4539
  if (destroyed) return;
3973
4540
  destroyed = true;
@@ -5138,6 +5705,48 @@ function createToolOptions(tools = []) {
5138
5705
  register
5139
5706
  };
5140
5707
  }
5708
+ const defaultSlop = (pointerType) => pointerType === "mouse" ? 4 : 10;
5709
+ function createPressTracker(opts) {
5710
+ const longPressMs = opts.longPressMs ?? 500;
5711
+ const slopFor = opts.slop ?? defaultSlop;
5712
+ let live;
5713
+ function cancel() {
5714
+ if (live?.timer !== void 0) clearTimeout(live.timer);
5715
+ live = void 0;
5716
+ }
5717
+ return {
5718
+ down(e, press) {
5719
+ cancel();
5720
+ const l = {
5721
+ pointerId: e.pointerId,
5722
+ x: e.clientX,
5723
+ y: e.clientY,
5724
+ slop: slopFor(press.pointerType),
5725
+ press,
5726
+ timer: void 0,
5727
+ done: false
5728
+ };
5729
+ if (press.pointerType !== "mouse") l.timer = setTimeout(() => {
5730
+ l.timer = void 0;
5731
+ l.done = true;
5732
+ opts.onLongPress(press);
5733
+ }, longPressMs);
5734
+ live = l;
5735
+ },
5736
+ move(e) {
5737
+ if (live?.pointerId !== e.pointerId) return;
5738
+ if (Math.hypot(e.clientX - live.x, e.clientY - live.y) > live.slop) cancel();
5739
+ },
5740
+ up(e) {
5741
+ if (live?.pointerId !== e.pointerId) return;
5742
+ const { press, done } = live;
5743
+ cancel();
5744
+ if (!done) opts.onPress(press);
5745
+ },
5746
+ cancel,
5747
+ destroy: cancel
5748
+ };
5749
+ }
5141
5750
  //#endregion
5142
5751
  //#region src/dom/tools/selection-edit.ts
5143
5752
  const CORNERS = [
@@ -5612,6 +6221,7 @@ function createToolController$1(opts) {
5612
6221
  clearMade();
5613
6222
  made = [...ids];
5614
6223
  ctx.root.dataset.inkMade = "";
6224
+ applyClaim();
5615
6225
  stopMade = [ctx.store.list.subscribe(checkMade), ctx.store.selection.subscribe(checkMade)];
5616
6226
  },
5617
6227
  done() {
@@ -5623,6 +6233,8 @@ function createToolController$1(opts) {
5623
6233
  const originalCursor = ctx.root.style.cursor;
5624
6234
  /** The pointer between a primary press and its release or cancel. */
5625
6235
  let pressed;
6236
+ /** Whether `pressed` is a touch, which a second touch turns into a pinch. */
6237
+ let pressedTouch = false;
5626
6238
  /** Pointers whose release belongs to a tool that is no longer active. */
5627
6239
  const swallow = /* @__PURE__ */ new Set();
5628
6240
  /** What the active one-shot tool made; undefined while it is ready. */
@@ -5630,16 +6242,61 @@ function createToolController$1(opts) {
5630
6242
  let stopMade = [];
5631
6243
  /** Whether the press now under way found the tool made (see the top of the file). */
5632
6244
  let pressFoundMade = false;
6245
+ /** Whether the press now under way commits an open editor or closes an open popover. */
6246
+ let pressDismisses = false;
5633
6247
  /** Moves and resizes what the active one-shot tool made. */
5634
6248
  let madeEdit;
5635
6249
  /** The press now under way went to `madeEdit`, not the tool. */
5636
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;
6257
+ const presses = createPressTracker({
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
+ },
6263
+ onLongPress: (press) => opts.onPress?.("long-press", press)
6264
+ });
5637
6265
  function clearMade() {
5638
6266
  stopMade.forEach((stop) => stop());
5639
6267
  stopMade = [];
5640
6268
  made = void 0;
5641
6269
  delete ctx.root.dataset.inkMade;
6270
+ applyClaim();
6271
+ }
6272
+ /**
6273
+ * Turns off one-finger panning on the pages element while a claiming tool is ready. The
6274
+ * viewport never sets `touch-action` there, so removing the property restores it. A host
6275
+ * without a pages element (no `createViewport`) gets no claim.
6276
+ */
6277
+ function applyClaim() {
6278
+ const pages = pagesOf();
6279
+ if (!pages) return;
6280
+ if (claiming()) pages.style.touchAction = "pinch-zoom";
6281
+ else pages.style.removeProperty("touch-action");
6282
+ }
6283
+ function pagesOf() {
6284
+ return ctx.root.querySelector(":scope > [data-ink-pages]");
6285
+ }
6286
+ /** True while the active tool claims touch: it claims it and, if one-shot, is still ready. */
6287
+ function claiming() {
6288
+ return current?.claimsTouch === true && made === void 0;
5642
6289
  }
6290
+ /**
6291
+ * Safari on iPad can let a one-finger drag scroll the container despite the claim's
6292
+ * `touch-action`, then cancel the tool's gesture. So a claimed one-finger move on the pages
6293
+ * also cancels the browser's scroll. Two fingers are left to the pinch handler or the browser.
6294
+ */
6295
+ const onTouchMove = (e) => {
6296
+ if (!e.cancelable || e.touches.length !== 1 || !claiming()) return;
6297
+ const pages = pagesOf();
6298
+ if (pages && e.target instanceof Node && pages.contains(e.target)) e.preventDefault();
6299
+ };
5643
6300
  /** Leaves the made state when its items are gone, or switches when the selection moved on. */
5644
6301
  function checkMade() {
5645
6302
  if (!made) return;
@@ -5651,19 +6308,36 @@ function createToolController$1(opts) {
5651
6308
  const selected = store.selection.get();
5652
6309
  if (!made.some((id) => selected.includes(id))) set("select-text");
5653
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";
5654
6313
  /** True for events from a pointer other than the one whose press is live. */
5655
6314
  const foreign = (e) => pressed !== void 0 && pressed !== e.pointerId;
5656
- const readFirst = () => {
6315
+ const readFirst = (e) => {
5657
6316
  pressFoundMade = made !== void 0;
6317
+ pressDismisses = dismisses(e.target, ctx.root.ownerDocument);
5658
6318
  };
5659
6319
  const readAfterEditor = () => {
5660
6320
  pressFoundMade ||= made !== void 0;
5661
6321
  };
5662
6322
  const onDown = (e) => {
6323
+ if (e.pointerType === "touch" && !e.isPrimary) {
6324
+ presses.cancel();
6325
+ if (pressedTouch) cancelPressed();
6326
+ return;
6327
+ }
6328
+ if (e.pointerType === "pen" && pressedTouch && pressed !== void 0) {
6329
+ presses.cancel();
6330
+ cancelPressed();
6331
+ }
5663
6332
  if (!e.isPrimary || e.button !== 0 || inCustomContent(e.target)) return;
5664
6333
  if (foreign(e)) return;
5665
6334
  swallow.delete(e.pointerId);
5666
- const annotationId = (e.target instanceof Element ? e.target : null)?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
6335
+ pressedTouch = e.pointerType === "touch";
6336
+ const target = e.target instanceof Element ? e.target : null;
6337
+ const annotationId = target?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
6338
+ const hit = hitFor(ctx, e);
6339
+ const reportable = selecting() && !onAnnotation(target) && !pressDismisses && !closedEditor(e) && hit !== void 0;
6340
+ presses.cancel();
5667
6341
  if (current?.kind === "one-shot") {
5668
6342
  if (pressFoundMade && annotationId === void 0 && !onScrollBar(e, ctx.root)) {
5669
6343
  e.preventDefault();
@@ -5675,25 +6349,56 @@ function createToolController$1(opts) {
5675
6349
  if (a && made?.includes(a.id) && madeEdit) {
5676
6350
  pressed = e.pointerId;
5677
6351
  editingMade = true;
5678
- if (madeEdit.onPointerDown(e, hitFor(ctx, e), a.id) === true) capture(e.pointerId);
6352
+ if (madeEdit.onPointerDown(e, hit, a.id) === true) capture(e.pointerId);
5679
6353
  return;
5680
6354
  }
5681
6355
  if (a && (!isTextMarkup(a) || pressFoundMade)) set("select-text");
5682
6356
  }
5683
6357
  pressed = e.pointerId;
5684
- if (current?.onPointerDown?.(e, hitFor(ctx, e), annotationId) === true) capture(e.pointerId);
6358
+ if (current?.onPointerDown?.(e, hit, annotationId) === true) capture(e.pointerId);
6359
+ pressModified = e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
6360
+ if (reportable && pressed === e.pointerId) presses.down(e, {
6361
+ page: hit.page,
6362
+ point: hit.user,
6363
+ pointerType: pointerTypeOf(e)
6364
+ });
5685
6365
  };
5686
6366
  const onMove = (e) => {
5687
6367
  if (!e.isPrimary || foreign(e)) return;
5688
6368
  const missed = pressed === e.pointerId && e.buttons === 0;
5689
- if (missed) pressed = void 0;
6369
+ if (missed) {
6370
+ pressed = void 0;
6371
+ presses.cancel();
6372
+ } else presses.move(e);
5690
6373
  if (editingMade) {
5691
6374
  madeEdit?.onPointerMove(e, hitFor(ctx, e));
5692
6375
  if (missed) editingMade = false;
5693
6376
  return;
5694
6377
  }
5695
6378
  current?.onPointerMove?.(e, hitFor(ctx, e));
5696
- };
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
+ }
5697
6402
  const onUp = (e) => {
5698
6403
  if (!e.isPrimary || e.button !== 0 || foreign(e)) return;
5699
6404
  if (pressed === e.pointerId) pressed = void 0;
@@ -5706,12 +6411,14 @@ function createToolController$1(opts) {
5706
6411
  madeEdit?.onPointerUp(e, hitFor(ctx, e));
5707
6412
  } else current?.onPointerUp?.(e, hitFor(ctx, e));
5708
6413
  release(e.pointerId);
6414
+ presses.up(e);
5709
6415
  };
5710
6416
  const onCancel = (e) => {
5711
6417
  if (!e.isPrimary || foreign(e)) return;
5712
6418
  if (e.type === "lostpointercapture" && pressed !== e.pointerId) return;
5713
6419
  if (pressed === e.pointerId) pressed = void 0;
5714
6420
  swallow.delete(e.pointerId);
6421
+ presses.cancel();
5715
6422
  if (editingMade) {
5716
6423
  editingMade = false;
5717
6424
  madeEdit?.onPointerCancel();
@@ -5772,11 +6479,13 @@ function createToolController$1(opts) {
5772
6479
  doc.addEventListener("pointerdown", readAfterEditor, { capture: true });
5773
6480
  ctx.root.addEventListener("pointerdown", onDown, { capture: true });
5774
6481
  ctx.root.addEventListener("pointermove", onMove);
6482
+ ctx.root.addEventListener("pointerleave", onLeave);
5775
6483
  ctx.root.addEventListener("pointerup", onUp);
5776
6484
  ctx.root.addEventListener("pointercancel", onCancel);
5777
6485
  ctx.root.addEventListener("lostpointercapture", onCancel);
5778
6486
  ctx.root.addEventListener("keydown", onKey);
5779
6487
  ctx.root.addEventListener("keydown", onEditorEscape, { capture: true });
6488
+ ctx.root.addEventListener("touchmove", onTouchMove, { passive: false });
5780
6489
  const loggedUnknown = /* @__PURE__ */ new Set();
5781
6490
  function logUnknownOnce(id) {
5782
6491
  if (loggedUnknown.has(id)) return;
@@ -5790,15 +6499,8 @@ function createToolController$1(opts) {
5790
6499
  return;
5791
6500
  }
5792
6501
  if (options) registry.merge(id, options);
5793
- if (pressed !== void 0) {
5794
- if (editingMade) madeEdit?.onPointerCancel();
5795
- else current?.onPointerCancel?.(new PointerEvent("pointercancel", {
5796
- pointerId: pressed,
5797
- isPrimary: true
5798
- }));
5799
- swallow.add(pressed);
5800
- pressed = void 0;
5801
- }
6502
+ presses.cancel();
6503
+ cancelPressed();
5802
6504
  editingMade = false;
5803
6505
  madeEdit?.reset();
5804
6506
  current?.deactivate();
@@ -5808,8 +6510,24 @@ function createToolController$1(opts) {
5808
6510
  next.activate(ctx);
5809
6511
  ctx.root.dataset.inkTool = id;
5810
6512
  ctx.root.style.cursor = next.cursor;
6513
+ overLink = false;
6514
+ hover(void 0);
6515
+ applyClaim();
5811
6516
  active.set(id);
5812
6517
  }
6518
+ /** Ends the live press's gesture, if any, and keeps its release from every tool. */
6519
+ function cancelPressed() {
6520
+ if (pressed === void 0) return;
6521
+ if (editingMade) {
6522
+ editingMade = false;
6523
+ madeEdit?.onPointerCancel();
6524
+ } else current?.onPointerCancel?.(new PointerEvent("pointercancel", {
6525
+ pointerId: pressed,
6526
+ isPrimary: true
6527
+ }));
6528
+ swallow.add(pressed);
6529
+ pressed = void 0;
6530
+ }
5813
6531
  set(opts.initial ?? "select-text");
5814
6532
  if (current === void 0 && opts.initial !== void 0 && opts.initial !== "select-text") set("select-text");
5815
6533
  return {
@@ -5827,20 +6545,24 @@ function createToolController$1(opts) {
5827
6545
  return registry.readable(id);
5828
6546
  },
5829
6547
  destroy() {
6548
+ presses.destroy();
5830
6549
  madeEdit?.reset();
5831
6550
  madeEdit = void 0;
5832
6551
  current?.deactivate();
5833
- clearMade();
5834
6552
  current = void 0;
6553
+ clearMade();
5835
6554
  doc.defaultView?.removeEventListener("pointerdown", readFirst, { capture: true });
5836
6555
  doc.removeEventListener("pointerdown", readAfterEditor, { capture: true });
5837
6556
  ctx.root.removeEventListener("pointerdown", onDown, { capture: true });
5838
6557
  ctx.root.removeEventListener("pointermove", onMove);
6558
+ ctx.root.removeEventListener("pointerleave", onLeave);
6559
+ hover(void 0);
5839
6560
  ctx.root.removeEventListener("pointerup", onUp);
5840
6561
  ctx.root.removeEventListener("pointercancel", onCancel);
5841
6562
  ctx.root.removeEventListener("lostpointercapture", onCancel);
5842
6563
  ctx.root.removeEventListener("keydown", onKey);
5843
6564
  ctx.root.removeEventListener("keydown", onEditorEscape, { capture: true });
6565
+ ctx.root.removeEventListener("touchmove", onTouchMove);
5844
6566
  delete ctx.root.dataset.inkTool;
5845
6567
  ctx.root.style.cursor = originalCursor;
5846
6568
  }
@@ -5861,6 +6583,23 @@ function isViewportEnter(e, root) {
5861
6583
  if (target.matches(".ink-page[data-page]")) return true;
5862
6584
  return target.closest("[data-ink-text-layer], [data-ink-canvas-layer]") !== null;
5863
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
+ }
6591
+ function pointerTypeOf(e) {
6592
+ return e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse";
6593
+ }
6594
+ /**
6595
+ * True when a press on `target` commits an open free-text editor (which has focus while open) or
6596
+ * closes an open Inkbar popover (a press outside it does).
6597
+ */
6598
+ function dismisses(target, doc) {
6599
+ if (doc.activeElement?.closest("[data-ink-editor]")) return true;
6600
+ const node = target instanceof Node ? target : null;
6601
+ return [...doc.querySelectorAll("[data-ink-popover]")].some((panel) => !panel.hidden && !panel.contains(node));
6602
+ }
5864
6603
  /**
5865
6604
  * True for a press on `root` itself outside its client area: on a scroll bar (or its border).
5866
6605
  * Client coordinates only, so a synthetic press gives the same answer as a real one.
@@ -5914,6 +6653,7 @@ function freehandTool(opts) {
5914
6653
  /** Adds a sample. A release reports 0 pressure, so it repeats the last sample's instead. */
5915
6654
  const sample = (s, e, release = false) => {
5916
6655
  const css = clampedCssIn(s.from.host, e);
6656
+ s.travel = Math.max(s.travel, Math.hypot(e.clientX - s.origin.x, e.clientY - s.origin.y));
5917
6657
  s.css.push(css);
5918
6658
  s.user.push(s.from.transform.fromCss(css));
5919
6659
  const p = release ? s.pressure?.[s.pressure.length - 1] : pressureOf(e);
@@ -5956,6 +6696,7 @@ function freehandTool(opts) {
5956
6696
  label: opts.label,
5957
6697
  icon: opts.icon,
5958
6698
  kind: "sticky",
6699
+ claimsTouch: true,
5959
6700
  cursor: "crosshair",
5960
6701
  styleGroup: "ink",
5961
6702
  activate(c) {
@@ -5974,7 +6715,13 @@ function freehandTool(opts) {
5974
6715
  css: [],
5975
6716
  user: [],
5976
6717
  pressure: void 0,
5977
- startedAt: Date.now()
6718
+ startedAt: Date.now(),
6719
+ pointerType: e.pointerType,
6720
+ origin: {
6721
+ x: e.clientX,
6722
+ y: e.clientY
6723
+ },
6724
+ travel: 0
5978
6725
  };
5979
6726
  sample(stroke, e);
5980
6727
  e.stopPropagation();
@@ -6005,6 +6752,7 @@ function freehandTool(opts) {
6005
6752
  const end = clampedCssIn(s.from.host, e);
6006
6753
  if (end.x !== last.x || end.y !== last.y) sample(s, e, true);
6007
6754
  clear();
6755
+ if (s.pointerType === "touch" && s.travel <= 10) return;
6008
6756
  opts.commit(ctx, {
6009
6757
  page: s.from.page,
6010
6758
  points: s.user,
@@ -7011,44 +7759,55 @@ function annotsOrder(items, widget) {
7011
7759
  })).sort((a, b) => a.at - b.at || a.i - b.i).map(({ item }) => item);
7012
7760
  }
7013
7761
  /**
7014
- * One page's widget ids in Tab order: `R` reads rows top to bottom and each row left to right,
7015
- * `C` reads columns left to right and each column top to bottom, and `S` follows `structure`
7016
- * (the page's annotation ids in structure-tree order), with widgets the tree leaves out after
7017
- * the rest in `/Annots` order. `W`, `A` or no `/Tabs` keep `/Annots` order. Positions are taken
7018
- * as the page displays, after its `/Rotate`; without `info` the page is read unturned.
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.
7019
7767
  */
7020
- function pageTabOrder(widgets, order, info, page = 0, structure = []) {
7021
- const listed = annotsOrder(widgets, (w) => w);
7022
- if (order === "S") {
7768
+ function orderStops(items, tabs, structure = []) {
7769
+ const listed = [...items].sort((a, b) => a.index - b.index);
7770
+ if (tabs === "S") {
7023
7771
  const at = new Map(structure.map((id, i) => [id, i]));
7024
- return listed.map((w, i) => ({
7025
- id: w.id,
7772
+ return listed.map((item, i) => ({
7773
+ id: item.id,
7026
7774
  i,
7027
- at: at.get(w.id) ?? Infinity
7028
- })).sort((a, b) => a.at - b.at || a.i - b.i).map((w) => w.id);
7775
+ at: at.get(item.id) ?? Infinity
7776
+ })).sort((a, b) => a.at - b.at || a.i - b.i).map((item) => item.id);
7029
7777
  }
7030
- if (order !== "R" && order !== "C") return listed.map((w) => w.id);
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 = []) {
7031
7789
  const transform = createPageTransform(info ?? unturned(page), 1);
7032
- const placed = listed.map((w, index) => ({
7790
+ const listed = annotsOrder(widgets, (w) => w);
7791
+ const items = [...listed.map((w, i) => ({
7033
7792
  id: w.id,
7034
- index,
7035
- box: transform.toCssRect(w.rect)
7036
- }));
7037
- return (order === "R" ? banded(placed, (b) => b.top, (b) => b.height, (b) => b.left) : banded(placed, (b) => b.left, (b) => b.width, (b) => b.top)).map((p) => p.id);
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));
7038
7804
  }
7039
7805
  /**
7040
- * Widget ids in Tab order: pages in order, each by its `/Tabs` (`pageTabOrder`), with
7041
- * `structure` the structure order of each `S` page. A page missing from `pages` is read
7042
- * 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.
7043
7808
  */
7044
- function tabOrder(fields, tabs, pages = [], structure = /* @__PURE__ */ new Map()) {
7045
- const byPage = /* @__PURE__ */ new Map();
7046
- for (const field of fields) for (const w of field.widgets) {
7047
- const list = byPage.get(w.page) ?? [];
7048
- list.push(w);
7049
- byPage.set(w.page, list);
7050
- }
7051
- 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);
7052
7811
  }
7053
7812
  //#endregion
7054
7813
  //#region src/dom/palette.ts
@@ -8028,37 +8787,254 @@ function createFormLayer(opts) {
8028
8787
  return layer;
8029
8788
  }
8030
8789
  //#endregion
8031
- //#region src/dom/page-mount.ts
8032
- /**
8033
- * Canvas, text layer, form layer, annotation overlay and any plugin layers for one page, stacked
8034
- * in that order.
8035
- */
8036
- function createPageMount(opts) {
8037
- const { document, page, host, transform } = opts;
8038
- const callbacks = (layer) => ({
8039
- ...opts.onError ? { onError: (error) => opts.onError?.(error, layer) } : {},
8040
- ...opts.onRendered ? { onRendered: () => opts.onRendered?.(layer) } : {}
8041
- });
8042
- const renderer = createPageRenderer({
8043
- document,
8044
- page,
8045
- host,
8046
- transform,
8047
- ...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
8048
- ...callbacks("canvas")
8049
- });
8050
- const textLayer = createTextLayer({
8051
- document,
8052
- page,
8053
- host,
8054
- transform,
8055
- ...callbacks("text")
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;
8056
8886
  });
8057
- const marks = opts.highlights?.mount(page, host, transform);
8058
- const form = document.form.fields.get().length > 0 ? createFormLayer({
8059
- host,
8060
- page,
8061
- transform,
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
8971
+ //#region src/dom/page-mount.ts
8972
+ /**
8973
+ * Canvas, text layer, links, search highlights, form layer, annotation overlay and any plugin
8974
+ * layers for one page, stacked in that order.
8975
+ */
8976
+ function createPageMount(opts) {
8977
+ const { document, page, host } = opts;
8978
+ let transform = opts.transform;
8979
+ const callbacks = (layer) => ({
8980
+ ...opts.onError ? { onError: (error) => opts.onError?.(error, layer) } : {},
8981
+ ...opts.onRendered ? { onRendered: () => opts.onRendered?.(layer) } : {}
8982
+ });
8983
+ const renderer = createPageRenderer({
8984
+ document,
8985
+ page,
8986
+ host,
8987
+ transform,
8988
+ ...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
8989
+ ...callbacks("canvas")
8990
+ });
8991
+ let named = false;
8992
+ const text = callbacks("text");
8993
+ const textLayer = createTextLayer({
8994
+ document,
8995
+ page,
8996
+ host,
8997
+ transform,
8998
+ ...text,
8999
+ onRendered: () => {
9000
+ if (!named) {
9001
+ named = true;
9002
+ links?.relabel();
9003
+ }
9004
+ text.onRendered?.();
9005
+ }
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;
9033
+ const marks = opts.highlights?.mount(page, host, transform);
9034
+ const form = document.form.fields.get().length > 0 ? createFormLayer({
9035
+ host,
9036
+ page,
9037
+ transform,
8062
9038
  document,
8063
9039
  highlight: opts.formHighlight ?? true,
8064
9040
  isActive: () => opts.formActive?.() ?? true,
@@ -8078,14 +9054,34 @@ function createPageMount(opts) {
8078
9054
  ...opts.decorate ? { decorate: opts.decorate } : {}
8079
9055
  });
8080
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
+ };
8081
9067
  return {
8082
9068
  renderer,
8083
9069
  textLayer,
8084
9070
  overlay,
8085
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
+ },
8086
9080
  update(next) {
9081
+ transform = next;
8087
9082
  renderer.update(next);
8088
9083
  textLayer.update(next);
9084
+ links?.update(next);
8089
9085
  overlay.update(next);
8090
9086
  form?.update(next);
8091
9087
  layers?.update(next);
@@ -8096,6 +9092,7 @@ function createPageMount(opts) {
8096
9092
  form?.destroy();
8097
9093
  overlay.destroy();
8098
9094
  marks?.destroy();
9095
+ links?.destroy();
8099
9096
  textLayer.destroy();
8100
9097
  renderer.destroy();
8101
9098
  }
@@ -8287,6 +9284,7 @@ function freetextTool() {
8287
9284
  label: "Text box",
8288
9285
  icon: Type,
8289
9286
  kind: "one-shot",
9287
+ claimsTouch: true,
8290
9288
  cursor: "text",
8291
9289
  styleGroup: "text",
8292
9290
  defaultOptions: {},
@@ -8543,6 +9541,7 @@ function redactAreaTool() {
8543
9541
  label: "Mark for redaction",
8544
9542
  icon: Square,
8545
9543
  kind: "one-shot",
9544
+ claimsTouch: true,
8546
9545
  cursor: "crosshair",
8547
9546
  styleGroup: "redact",
8548
9547
  activate(c) {
@@ -8833,6 +9832,7 @@ function shapeTool() {
8833
9832
  icon: Shapes,
8834
9833
  kind: "one-shot",
8835
9834
  cursor: "crosshair",
9835
+ claimsTouch: true,
8836
9836
  styleGroup: "shape",
8837
9837
  defaultOptions: { ...DEFAULT_SHAPE_OPTIONS },
8838
9838
  activate(c) {
@@ -9236,6 +10236,122 @@ function trapTab(e, container) {
9236
10236
  }
9237
10237
  }
9238
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
9239
10355
  //#region src/dom/search/highlights.ts
9240
10356
  const NO_HITS = [];
9241
10357
  /** One box over the quad: its CSS bounding box, clipped to the quad when it is not upright. */
@@ -9383,6 +10499,7 @@ function mountViewer(opts) {
9383
10499
  const pages = /* @__PURE__ */ new Map();
9384
10500
  let gap = opts.gap;
9385
10501
  let buffer = opts.buffer;
10502
+ let pinchZoom = opts.pinchZoom;
9386
10503
  let custom = opts.customAnnotations ?? false;
9387
10504
  let viewport;
9388
10505
  let tools;
@@ -9412,8 +10529,9 @@ function mountViewer(opts) {
9412
10529
  tool: opts.tool
9413
10530
  });
9414
10531
  const toolStyle = opts.toolStyle ?? tracker.toolStyle;
10532
+ const storage = opts.storage ?? createMemoryStorage();
9415
10533
  const host = opts.signatureLibrary;
9416
- const own = host ? void 0 : createSignatureLibrary(opts.signatures ?? createMemorySignatureStore());
10534
+ const own = host ? void 0 : createSignatureLibrary(opts.signatures ?? createStorageSignatureStore(storage));
9417
10535
  /** The library now: a host getter is read each time, so a swapped library takes effect. */
9418
10536
  const signatures = () => typeof host === "function" ? host() : host ?? own;
9419
10537
  signatures().refresh();
@@ -9434,6 +10552,7 @@ function mountViewer(opts) {
9434
10552
  formHighlight: highlight,
9435
10553
  formActive: formsActive,
9436
10554
  onSignField: (req) => opts.onSignField?.(req),
10555
+ followLink: (target, o) => void navigation.follow(target, o),
9437
10556
  ...layers ? { layers } : {},
9438
10557
  ...highlights ? { highlights } : {},
9439
10558
  onError: (cause, layer) => {
@@ -9461,26 +10580,25 @@ function mountViewer(opts) {
9461
10580
  return id !== void 0 && FORM_TOOLS.has(id);
9462
10581
  };
9463
10582
  /**
9464
- * The first widget (`last`: the last) in Tab order on each page with a control to stop at,
9465
- * by page index.
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.
9466
10586
  */
9467
- function tabEdges(last) {
9468
- const fields = doc.form.fields.get();
9469
- const byId = new Map(fields.flatMap((f) => hasControl(f) ? f.widgets.map((w) => [w.id, w]) : []));
10587
+ function tabEdge(page, last, widgets) {
9470
10588
  const state = doc.form.widgetState.get();
9471
- const edges = /* @__PURE__ */ new Map();
9472
- for (const id of tabOrder(fields, doc.form._tabs, doc.pages, doc.form._structure)) {
9473
- const w = byId.get(id);
9474
- if (!w || (state[id]?.hidden ?? (w.hidden || w.printOnly))) continue;
9475
- if (last || !edges.has(w.page)) edges.set(w.page, w);
9476
- }
9477
- return edges;
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);
9478
10596
  }
9479
10597
  /** The page a Tab waits for: its stops were not mounted when the view scrolled to it. */
9480
10598
  let tabWait;
9481
- /** The first stop of `page` (`back`: its last), once its form layer is mounted. */
10599
+ /** The first stop of `page` (`back`: its last), once the page is mounted. */
9482
10600
  const edgeStop = (page, back) => {
9483
- const stops = pages.get(page)?.form?.tabStops() ?? [];
10601
+ const stops = pages.get(page)?.tabStops() ?? [];
9484
10602
  return back ? stops.at(-1) : stops[0];
9485
10603
  };
9486
10604
  /**
@@ -9515,45 +10633,117 @@ function mountViewer(opts) {
9515
10633
  tabWait = wait;
9516
10634
  }
9517
10635
  /**
9518
- * Tab on a page's last field stop (Shift+Tab on its first) goes to the first (last) stop of
9519
- * the next (previous) page that has fields, mounting it and scrolling it into view. A page
9520
- * that has not mounted once the view scrolls to it gets one more try, when it mounts or on the
9521
- * next frame; after that, focus moves on as the browser's Tab would. Past the last page's
9522
- * fields, Tab leaves the viewer as usual.
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.
9523
10643
  */
9524
- function onFormTab(e) {
10644
+ function onPageTab(e) {
9525
10645
  if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey || e.defaultPrevented) return;
9526
10646
  const target = e.target;
9527
- if (!(target instanceof Element) || !target.closest("[data-ink-form-layer]")) return;
9528
- if (!formsActive()) return;
10647
+ if (!(target instanceof HTMLElement) || !target.closest("[data-ink-form-layer], [data-ink-link-layer]")) return;
9529
10648
  const back = e.shiftKey;
9530
- 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));
9531
10650
  if (!from) return;
9532
10651
  const [page, mount] = from;
9533
- if (mount.form?.tabStops().at(back ? 0 : -1) !== target) return;
9534
- const edges = [...tabEdges(back)].filter(([p]) => back ? p < page : p > page).sort(([a], [b]) => back ? b - a : a - b);
9535
- for (const [next, widget] of edges) {
9536
- viewport.scrollToRect(next, widget.rect);
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);
9537
10674
  const to = edgeStop(next, back);
9538
10675
  if (to) {
9539
10676
  e.preventDefault();
9540
10677
  to.focus();
9541
10678
  return;
9542
10679
  }
9543
- if (!pages.get(next)?.form) {
10680
+ if (!pages.has(next)) {
9544
10681
  e.preventDefault();
9545
10682
  awaitTabStop(next, back, target);
9546
10683
  return;
9547
10684
  }
9548
10685
  }
9549
10686
  }
9550
- el.addEventListener("keydown", onFormTab);
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
+ });
9551
10740
  const stopForm = [
9552
- doc.form.on("action", (a) => opts.onFormAction?.(a)),
10741
+ doc.form.on("action", reportAction),
9553
10742
  doc.form.on("alert", (message) => opts.onFormAlert?.(message)),
9554
- doc.form.on("goto", (page) => {
9555
- if (!destroyed && page >= 0 && page < doc.pages.length) viewport.scrollToPage(page);
9556
- })
10743
+ doc.form.on("goto", (page) => navigation.goTo({
10744
+ page,
10745
+ fit: "fit"
10746
+ }))
9557
10747
  ];
9558
10748
  /**
9559
10749
  * The page whose open scripts ran last, and has not closed since. Page open and close events
@@ -9628,6 +10818,28 @@ function mountViewer(opts) {
9628
10818
  el.scrollLeft += o.x + box.left + css.x - corner.x;
9629
10819
  if (anchor.vertical) el.scrollTop += o.y + box.top + css.y - corner.y;
9630
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
+ }
9631
10843
  function build(state) {
9632
10844
  const vp = createViewport({
9633
10845
  container: el,
@@ -9637,6 +10849,7 @@ function mountViewer(opts) {
9637
10849
  ...state.rotation === void 0 ? {} : { rotation: state.rotation },
9638
10850
  ...gap === void 0 ? {} : { gap },
9639
10851
  ...buffer === void 0 ? {} : { buffer },
10852
+ ...pinchZoom === void 0 ? {} : { pinchZoom },
9640
10853
  onMount: mountPage,
9641
10854
  onUpdate: (page, _host, transform) => pages.get(page)?.update(transform),
9642
10855
  onUnmount: unmountPage
@@ -9646,6 +10859,10 @@ function mountViewer(opts) {
9646
10859
  ...state.tool === void 0 ? {} : { initial: state.tool },
9647
10860
  tools: list,
9648
10861
  options: toolOptions,
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),
9649
10866
  ctx: {
9650
10867
  toolStyle,
9651
10868
  store: doc.annotations,
@@ -9716,8 +10933,10 @@ function mountViewer(opts) {
9716
10933
  get signatures() {
9717
10934
  return signatures();
9718
10935
  },
10936
+ storage,
9719
10937
  signatureFonts: opts.signatureFonts ? [...opts.signatureFonts] : SIGNATURE_FONTS,
9720
10938
  pages,
10939
+ navigation,
9721
10940
  rebuild(next) {
9722
10941
  if (destroyed) return;
9723
10942
  const state = {
@@ -9731,6 +10950,7 @@ function mountViewer(opts) {
9731
10950
  teardown();
9732
10951
  if (next.gap !== void 0) gap = next.gap;
9733
10952
  if (next.buffer !== void 0) buffer = next.buffer;
10953
+ if (next.pinchZoom !== void 0) pinchZoom = next.pinchZoom;
9734
10954
  if (next.customAnnotations !== void 0) custom = next.customAnnotations;
9735
10955
  build(state);
9736
10956
  },
@@ -9753,9 +10973,12 @@ function mountViewer(opts) {
9753
10973
  layers?.destroy();
9754
10974
  highlights?.destroy();
9755
10975
  stopRedecorate?.();
9756
- el.removeEventListener("keydown", onFormTab);
10976
+ el.removeEventListener("keydown", onPageTab);
10977
+ el.ownerDocument.removeEventListener("keydown", onTabKey, true);
10978
+ el.removeEventListener("focusin", onStopFocus);
9757
10979
  tabWait?.cancel();
9758
10980
  for (const stop of stopForm) stop();
10981
+ navigation.clear();
9759
10982
  if (!highlight) delete el.dataset.inkFormHighlight;
9760
10983
  }
9761
10984
  };
@@ -11055,9 +12278,9 @@ const passThrough = {
11055
12278
  react: () => noop
11056
12279
  };
11057
12280
  function createViewer(el, opts) {
11058
- 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;
11059
12282
  /** One library for the viewer's life, so templates and the menu outlive each document. */
11060
- const signatures = createSignatureLibrary(signatureStore ?? createMemorySignatureStore());
12283
+ const signatures = createSignatureLibrary(signatureStore ?? createStorageSignatureStore(storage));
11061
12284
  /** Tool options persist for the viewer's life; the tool style is per document. */
11062
12285
  const toolOptions = createToolOptions();
11063
12286
  const status = writable({ state: "idle" });
@@ -11066,6 +12289,10 @@ function createViewer(el, opts) {
11066
12289
  const engine = writable(null);
11067
12290
  const viewport = writable(null);
11068
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);
11069
12296
  /** The activated annotation: the note thread opens from it. */
11070
12297
  const activated = writable(null);
11071
12298
  let formHighlight = initialHighlight ?? true;
@@ -11108,6 +12335,7 @@ function createViewer(el, opts) {
11108
12335
  */
11109
12336
  const host = createPluginHost({
11110
12337
  plugins: plugins ?? [],
12338
+ storage,
11111
12339
  document: readonly(current),
11112
12340
  history: () => current.get()?.history ?? passThrough,
11113
12341
  activated: readonly(activated),
@@ -11188,6 +12416,7 @@ function createViewer(el, opts) {
11188
12416
  formHighlight,
11189
12417
  toolOptions,
11190
12418
  signatureLibrary: signatures,
12419
+ storage,
11191
12420
  onFormAlert: (message) => formAlert.show(message),
11192
12421
  onSignField: (req) => {
11193
12422
  signFieldMenu ??= createSignFieldMenu(viewer, {
@@ -11230,7 +12459,16 @@ function createViewer(el, opts) {
11230
12459
  }
11231
12460
  });
11232
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);
11233
12467
  return () => {
12468
+ for (const s of stopNav) s();
12469
+ navigation.set(null);
12470
+ canGoBack.set(false);
12471
+ canGoForward.set(false);
11234
12472
  stopForm();
11235
12473
  releaseFormErrors = () => void 0;
11236
12474
  signFieldMenu?.close();
@@ -11325,6 +12563,11 @@ function createViewer(el, opts) {
11325
12563
  formHighlight = on;
11326
12564
  engine.get()?.setFormHighlight(on);
11327
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),
11328
12571
  commands: { get: (id) => host.command(id) },
11329
12572
  search: viewerSearch,
11330
12573
  records: (key) => host.records(key),
@@ -11349,6 +12592,7 @@ function createViewer(el, opts) {
11349
12592
  internals.set(viewer, {
11350
12593
  search,
11351
12594
  host,
12595
+ navigation: readonly(navigation),
11352
12596
  destroyers,
11353
12597
  destroyed: () => destroyed
11354
12598
  });
@@ -11381,6 +12625,15 @@ function pluginHostOf(viewer) {
11381
12625
  return found.host;
11382
12626
  }
11383
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
+ /**
11384
12637
  * @internal Runs `fn` when the viewer is destroyed, for what a control set up on it; at once
11385
12638
  * when it already is.
11386
12639
  */
@@ -11552,6 +12805,33 @@ function historyButton(viewer, action) {
11552
12805
  return b;
11553
12806
  }
11554
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
+ /**
11555
12835
  * True for `select-text` and `select`, which every viewer registers, and for any other tool id
11556
12836
  * the viewer was given.
11557
12837
  */
@@ -12610,6 +13890,29 @@ function sidebarBadge(badge) {
12610
13890
  const text = String(badge).trim();
12611
13891
  return text === "" || text === "0" ? null : text;
12612
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
+ }
12613
13916
  const TRIGGER = "[data-ink-sidebar-trigger]";
12614
13917
  let count = 0;
12615
13918
  function sameIds(a, b) {
@@ -12637,6 +13940,20 @@ function createSidebarController(opts) {
12637
13940
  let visible = opts.visible ?? true;
12638
13941
  /** The options that last gave `onOpenChange`, called as a method of them. */
12639
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;
12640
13957
  /** The trigger that holds the Tab stop in rail and tabs, once one has had focus. */
12641
13958
  let current = null;
12642
13959
  let destroyed = false;
@@ -12669,15 +13986,49 @@ function createSidebarController(opts) {
12669
13986
  side,
12670
13987
  panels,
12671
13988
  open: resolve(opts.open !== void 0 ? opts.open : panels[0]?.id ?? null),
12672
- visible
13989
+ visible,
13990
+ width,
13991
+ resizing
12673
13992
  });
12674
- const publish = (open) => store.set({
13993
+ const publish = (open = store.get().open) => store.set({
12675
13994
  layout,
12676
13995
  side,
12677
13996
  panels,
12678
13997
  open,
12679
- 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
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();
12680
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
+ }
12681
14032
  const isOpen = (id) => store.get().open.includes(id);
12682
14033
  function update(next) {
12683
14034
  if (destroyed) return;
@@ -12690,11 +14041,24 @@ function createSidebarController(opts) {
12690
14041
  if (next.panels !== void 0 && !samePanels(next.panels, panels)) panels = next.panels;
12691
14042
  if ("onOpenChange" in next) handler = next;
12692
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;
12693
14057
  const arrived = !controlled && before.panels.length === 0 && before.open.length === 0 ? panels[0]?.id : void 0;
12694
14058
  const asked = next.open !== void 0 ? idsOf(next.open) : arrived !== void 0 ? [arrived] : before.open;
12695
14059
  let open = resolve(asked);
12696
14060
  if (!controlled && layout === "tabs" && open.length === 0 && panels[0] !== void 0) open = [panels[0].id];
12697
- 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);
12698
14062
  if (!(open.length === new Set(asked).size && open.every((id) => asked.includes(id)))) handler.onOpenChange?.(toValue(open));
12699
14063
  }
12700
14064
  function toggle(id) {
@@ -12825,6 +14189,123 @@ function createSidebarController(opts) {
12825
14189
  bindings.add(unbind);
12826
14190
  return unbind;
12827
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
+ }
12828
14309
  const controller = {
12829
14310
  state: readonly(store),
12830
14311
  update,
@@ -12835,8 +14316,14 @@ function createSidebarController(opts) {
12835
14316
  "data-layout": layout,
12836
14317
  "data-side": side,
12837
14318
  "data-state": store.get().open.length > 0 || pending && !controlled ? "open" : "closed",
14319
+ ...resizing && { "data-resizing": "" },
12838
14320
  ...!visible && { hidden: "" }
12839
14321
  }),
14322
+ rootStyle: () => ({ "--ink-sidebar-width": `${shown()}px` }),
14323
+ panelsAttrs: () => ({ id: panelsId }),
14324
+ handleAttrs,
14325
+ bindHandle,
14326
+ resetWidth,
12840
14327
  tablistAttrs,
12841
14328
  triggerAttrs,
12842
14329
  panelAttrs,
@@ -12870,6 +14357,8 @@ function createSidebar(opts) {
12870
14357
  const { content, ...options } = opts;
12871
14358
  const controller = createSidebarController(options);
12872
14359
  const root = h("div", { class: "ink-sidebar" });
14360
+ const handle = h("div", { class: "ink-sidebar-handle" });
14361
+ let panelsEl = null;
12873
14362
  let shape = "";
12874
14363
  const triggers = /* @__PURE__ */ new Map();
12875
14364
  const panels = /* @__PURE__ */ new Map();
@@ -12905,6 +14394,7 @@ function createSidebar(opts) {
12905
14394
  for (const [p, panel] of built) panels.set(p.id, panel);
12906
14395
  if (s.layout === "stack") {
12907
14396
  list = null;
14397
+ panelsEl = null;
12908
14398
  root.replaceChildren(...built.map(([p, panel]) => {
12909
14399
  const header = makeTrigger(p, "ink-sidebar-header");
12910
14400
  triggers.set(p.id, header);
@@ -12917,15 +14407,20 @@ function createSidebar(opts) {
12917
14407
  triggers.set(p.id, t);
12918
14408
  list.append(t);
12919
14409
  }
12920
- root.replaceChildren(list, h("div", { class: "ink-sidebar-panels" }, ...built.map(([, panel]) => panel)));
14410
+ panelsEl = h("div", { class: "ink-sidebar-panels" }, ...built.map(([, panel]) => panel));
14411
+ root.replaceChildren(list, panelsEl);
12921
14412
  }
14413
+ root.append(handle);
12922
14414
  paint(s);
12923
14415
  if (focused !== void 0) triggers.get(focused)?.focus();
12924
14416
  }
12925
14417
  /** Attributes, labels and badges, in place: the focused trigger keeps focus. */
12926
14418
  function paint(s) {
12927
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);
12928
14421
  if (list) setAttrs(list, controller.tablistAttrs());
14422
+ if (panelsEl) setAttrs(panelsEl, controller.panelsAttrs());
14423
+ setAttrs(handle, controller.handleAttrs());
12929
14424
  for (const p of s.panels) {
12930
14425
  const t = triggers.get(p.id);
12931
14426
  const panel = panels.get(p.id);
@@ -12959,9 +14454,11 @@ function createSidebar(opts) {
12959
14454
  show(controller.state.get());
12960
14455
  const stopState = controller.state.subscribe(show);
12961
14456
  const unbind = controller.bindTriggers(root);
14457
+ const unbindHandle = controller.bindHandle(handle);
12962
14458
  onSidebarDestroy(controller, () => {
12963
14459
  stopState();
12964
14460
  unbind();
14461
+ unbindHandle();
12965
14462
  });
12966
14463
  return {
12967
14464
  element: root,
@@ -13805,6 +15302,581 @@ function createThumbnails(viewer, opts = {}) {
13805
15302
  return root;
13806
15303
  }
13807
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
13808
15880
  //#region src/dom/viewer/style-menu.ts
13809
15881
  /** A button that opens the line style menu: weight, solid or dashed, and arrowheads. */
13810
15882
  function createLineStyleMenu(viewer) {
@@ -14501,6 +16573,6 @@ function createHighlightMenu(viewer) {
14501
16573
  return root;
14502
16574
  }
14503
16575
  //#endregion
14504
- 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, 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, 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, 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, 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 };
14505
16577
 
14506
16578
  //# sourceMappingURL=dom.mjs.map