@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/README.md +155 -35
- package/dist/dom.d.mts +435 -16
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2216 -144
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{sign-DZVDJhqT.mjs → sign-BSP7H3b7.mjs} +1799 -1479
- package/dist/sign-BSP7H3b7.mjs.map +1 -0
- package/dist/{sign-BTbtPHbM.d.mts → sign-UdMGeqRr.d.mts} +161 -13
- package/dist/sign-UdMGeqRr.d.mts.map +1 -0
- package/package.json +3 -3
- package/dist/sign-BTbtPHbM.d.mts.map +0 -1
- package/dist/sign-DZVDJhqT.mjs.map +0 -1
package/dist/dom.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleDashed, Download, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, Printer, RectangleHorizontal, Redo2, Search, Shapes, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
1
|
+
import { $ as withBox, A as threadRoot, C as isPointerOnlyKind, Cn as rectsIntersect, D as isThreadRoot, G as isTextMarkup, Gt as calloutStroke, H as hasCornerHandles, J as movedTip, Jt as withCalloutBounds, K as keepsAspect, Kt as freeTextBox, N as createToolStyle, On as InkError, Q as translated, S as defaultEnds, T as isAbandonedNote, U as hasRectGeometry, V as hasCallout, W as isBubble, X as resizedRect, Y as resizedBox, Yt as cssVariableDefaults, Z as scaled, _ as DEFAULT_CORNER_RADIUS, a as createInkTemplate, an as mergeLineRects, b as SHAPE_KINDS, bn as rectFromPoints, c as createSignatureLibrary, cn as readonly, ct as sameValue, d as placeSignature, en as LINE_ENDINGS, et as openDocument, fn as invertMatrix, ft as importedId, g as recogniseStroke, hn as quadContainsPoint, i as createImageTemplate, j as withoutReplies, k as threadOf, kn as isInkError, l as createStorageSignatureStore, ln as writable, lt as safeUrl, m as scopeStorage, on as createRecordStore, p as createMemoryStorage, q as movedEndpoint, qt as isStandardCallout, rn as isKnown, rt as deriveRect, sn as derived, st as isProduction, t as signField, tt as isBlankQuery, un as applyMatrix, v as DEFAULT_SHAPE_OPTIONS, w as shapeInput, x as constrain, xn as rectFromQuad, y as DEFAULT_SHAPE_SIZE, yn as rectContainsPoint } from "./sign-BSP7H3b7.mjs";
|
|
2
|
+
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, ArrowLeft, ArrowRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleDashed, Download, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, Printer, RectangleHorizontal, Redo2, Search, Shapes, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
3
3
|
import { getStroke } from "perfect-freehand";
|
|
4
4
|
//#region src/dom/transform.ts
|
|
5
5
|
/** Folds any integer multiple of 90 onto 0, 90, 180 or 270. Throws `RangeError` otherwise. */
|
|
@@ -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
|
|
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 (!
|
|
447
|
-
const box = boxFor(
|
|
866
|
+
if (!at) return;
|
|
867
|
+
const box = boxFor(at.page);
|
|
448
868
|
if (!box) return;
|
|
449
|
-
const after = transformFor(
|
|
450
|
-
const
|
|
451
|
-
container.scrollLeft =
|
|
452
|
-
container.scrollTop =
|
|
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
|
|
708
|
-
* (it undid or
|
|
709
|
-
* reach of the
|
|
1237
|
+
* Keeps focus in the viewer when a focused Undo, Redo, Back or Forward button inside `el` turns
|
|
1238
|
+
* disabled (it undid, redid or went back over the last step), which would otherwise drop focus
|
|
1239
|
+
* to `<body>`, out of reach of the viewer's keys. Focus moves to `scroll()`, the viewer's scroll element,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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)
|
|
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
|
-
|
|
5794
|
-
|
|
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
|
|
7015
|
-
*
|
|
7016
|
-
*
|
|
7017
|
-
*
|
|
7018
|
-
*
|
|
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
|
|
7021
|
-
const listed =
|
|
7022
|
-
if (
|
|
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((
|
|
7025
|
-
id:
|
|
7772
|
+
return listed.map((item, i) => ({
|
|
7773
|
+
id: item.id,
|
|
7026
7774
|
i,
|
|
7027
|
-
at: at.get(
|
|
7028
|
-
})).sort((a, b) => a.at - b.at || a.i - b.i).map((
|
|
7775
|
+
at: at.get(item.id) ?? Infinity
|
|
7776
|
+
})).sort((a, b) => a.at - b.at || a.i - b.i).map((item) => item.id);
|
|
7029
7777
|
}
|
|
7030
|
-
if (
|
|
7778
|
+
if (tabs === "W") return [...listed.filter((i) => i.widget), ...listed.filter((i) => !i.widget)].map((i) => i.id);
|
|
7779
|
+
if (tabs !== "R" && tabs !== "C") return listed.map((i) => i.id);
|
|
7780
|
+
return (tabs === "R" ? banded(listed, (b) => b.top, (b) => b.height, (b) => b.left) : banded(listed, (b) => b.left, (b) => b.width, (b) => b.top)).map((p) => p.id);
|
|
7781
|
+
}
|
|
7782
|
+
/**
|
|
7783
|
+
* One page's widget and link ids in Tab order (`orderStops`), so fields and links share one
|
|
7784
|
+
* reading order. A link is placed by its first quad, the line it starts on. Positions are taken
|
|
7785
|
+
* as the page displays, after its `/Rotate`; without `info` the page is read unturned.
|
|
7786
|
+
* `structure` holds the page's annotation ids as PDF.js reports them, which link ids wrap.
|
|
7787
|
+
*/
|
|
7788
|
+
function pageStopOrder(widgets, links, order, info, page = 0, structure = []) {
|
|
7031
7789
|
const transform = createPageTransform(info ?? unturned(page), 1);
|
|
7032
|
-
const
|
|
7790
|
+
const listed = annotsOrder(widgets, (w) => w);
|
|
7791
|
+
const items = [...listed.map((w, i) => ({
|
|
7033
7792
|
id: w.id,
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
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
|
-
*
|
|
7041
|
-
*
|
|
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
|
|
7045
|
-
|
|
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/
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
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
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
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 ??
|
|
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
|
|
9465
|
-
*
|
|
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
|
|
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
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
return
|
|
10589
|
+
const shown = (widgets.get(page) ?? []).filter((w) => !(state[w.id]?.hidden ?? (w.hidden || w.printOnly)));
|
|
10590
|
+
const links = doc.links(page).filter((l) => l.target.kind !== "unsupported");
|
|
10591
|
+
if (shown.length === 0 && links.length === 0) return void 0;
|
|
10592
|
+
const rects = new Map([...shown.map((w) => [w.id, w.rect]), ...links.map((l) => [l.id, l.quads?.[0] ? rectFromQuad(l.quads[0]) : l.rect])]);
|
|
10593
|
+
const order = pageStopOrder(shown, links, doc.form._tabs.get(page), doc.pages[page], page, doc.form._structure.get(page));
|
|
10594
|
+
const id = last ? order.at(-1) : order[0];
|
|
10595
|
+
return id === void 0 ? void 0 : rects.get(id);
|
|
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
|
|
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)?.
|
|
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
|
|
9519
|
-
*
|
|
9520
|
-
*
|
|
9521
|
-
*
|
|
9522
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
9534
|
-
const
|
|
9535
|
-
|
|
9536
|
-
|
|
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.
|
|
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",
|
|
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",
|
|
10741
|
+
doc.form.on("action", reportAction),
|
|
9553
10742
|
doc.form.on("alert", (message) => opts.onFormAlert?.(message)),
|
|
9554
|
-
doc.form.on("goto", (page) => {
|
|
9555
|
-
|
|
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",
|
|
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 ??
|
|
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
|
-
|
|
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
|