@inkbar/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -7
- package/dist/dom.d.mts +610 -58
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2387 -929
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +17 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BHYKTwYE.d.mts → signatures-By4B40DP.d.mts} +706 -620
- package/dist/signatures-By4B40DP.d.mts.map +1 -0
- package/dist/{signatures-DF2VmNVa.mjs → signatures-D68fuLlt.mjs} +1333 -325
- package/dist/signatures-D68fuLlt.mjs.map +1 -0
- package/package.json +4 -7
- package/dist/signatures-BHYKTwYE.d.mts.map +0 -1
- package/dist/signatures-DF2VmNVa.mjs.map +0 -1
- package/schema.json +0 -1356
package/dist/dom.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $t as derived, B as isTextMarkup, Bt as withCalloutBounds, E as withoutReplies, G as resizedRect, Gt as isKnown, H as movedEndpoint, Ht as LINE_ENDINGS, I as hasCallout, J as withBox, Jt as cssVariableDefaults, K as scaled, L as hasCornerHandles, Lt as calloutStroke, O as createToolStyle, Qt as createRecordStore, R as hasRectGeometry, Rt as freeTextBox, S as isThreadRoot, T as threadRoot, U as movedTip, V as keepsAspect, W as resizedBox, Y as openDocument, Z as deriveRect, Zt as mergeLineRects, _ as defaultEnds, b as isAbandonedNote, bn as isInkError, d as recogniseStroke, dn as rectFromPoints, en as readonly, f as DEFAULT_CORNER_RADIUS, g as constrain, h as SHAPE_KINDS, i as createInkTemplate, in as invertMatrix, l as placeSignature, m as DEFAULT_SHAPE_SIZE, mn as rectsIntersect, nn as applyMatrix, o as createMemorySignatureStore, p as DEFAULT_SHAPE_OPTIONS, q as translated, r as createImageTemplate, s as createSignatureLibrary, tn as writable, tt as isProduction, v as isPointerOnlyKind, w as threadOf, y as shapeInput, yn as InkError, z as isBubble, zt as isStandardCallout } from "./signatures-D68fuLlt.mjs";
|
|
2
2
|
import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Check, ChevronDown, ChevronLeft, ChevronRight, Circle, CircleDashed, Hexagon, Highlighter, ICON_ATTRIBUTES, Italic, MessageCircle, MessageSquare, MessageSquareText, Minus, MoveUpRight, PenLine, PenTool, Plus, RectangleHorizontal, Redo2, Search, Signature, Slash, SpellCheck2, Square, SquareDashed, SquareDashedMousePointer, Star, Strikethrough, TextCursor, Trash2, Type, Underline, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
3
3
|
import { getStroke } from "perfect-freehand";
|
|
4
4
|
//#region src/dom/transform.ts
|
|
@@ -500,7 +500,7 @@ function createViewport(opts) {
|
|
|
500
500
|
});
|
|
501
501
|
sync();
|
|
502
502
|
},
|
|
503
|
-
scrollToRect(page, rect) {
|
|
503
|
+
scrollToRect(page, rect, o) {
|
|
504
504
|
if (mode.get() === "single" && currentPage.get() !== page) {
|
|
505
505
|
currentPage.set(page);
|
|
506
506
|
relayout();
|
|
@@ -508,11 +508,13 @@ function createViewport(opts) {
|
|
|
508
508
|
const box = boxFor(page);
|
|
509
509
|
if (!box) return;
|
|
510
510
|
const css = transformFor(page).toCssRect(rect);
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
511
|
+
const at = origin();
|
|
512
|
+
const to = { behavior: o?.behavior ?? "auto" };
|
|
513
|
+
const top = at.y + box.top + css.top;
|
|
514
|
+
if (top < container.scrollTop || top + css.height > container.scrollTop + container.clientHeight) to.top = Math.max(0, top - (container.clientHeight - css.height) / 2);
|
|
515
|
+
const left = at.x + box.left + css.left;
|
|
516
|
+
if (left < container.scrollLeft || left + css.width > container.scrollLeft + container.clientWidth) to.left = Math.max(0, left - (container.clientWidth - css.width) / 2);
|
|
517
|
+
if (to.top !== void 0 || to.left !== void 0) container.scrollTo(to);
|
|
516
518
|
sync();
|
|
517
519
|
},
|
|
518
520
|
transformFor,
|
|
@@ -629,7 +631,7 @@ function watchDevicePixelRatio(view, current, onChange) {
|
|
|
629
631
|
}
|
|
630
632
|
//#endregion
|
|
631
633
|
//#region src/dom/page-renderer.ts
|
|
632
|
-
const isCancellation$
|
|
634
|
+
const isCancellation$2 = (error) => error?.name === "RenderingCancelledException";
|
|
633
635
|
function createPageRenderer(opts) {
|
|
634
636
|
const { document: doc, page, host } = opts;
|
|
635
637
|
const view = host.ownerDocument.defaultView;
|
|
@@ -715,7 +717,7 @@ function createPageRenderer(opts) {
|
|
|
715
717
|
opts.onRendered?.();
|
|
716
718
|
} catch (error) {
|
|
717
719
|
if (canvas && canvas !== current) release(canvas);
|
|
718
|
-
if (!((destroyed || gen !== generation) && isCancellation$
|
|
720
|
+
if (!((destroyed || gen !== generation) && isCancellation$2(error)) && !destroyed) opts.onError?.(error);
|
|
719
721
|
} finally {
|
|
720
722
|
if (gen === generation) settle();
|
|
721
723
|
}
|
|
@@ -966,6 +968,8 @@ const CORNERS$1 = [
|
|
|
966
968
|
];
|
|
967
969
|
/** Width in CSS px of the band along a mask's edge that takes presses. */
|
|
968
970
|
const MASK_EDGE = 6;
|
|
971
|
+
/** Width in CSS px of the band straddling a text box's edge, which drags the box. */
|
|
972
|
+
const TEXT_EDGE = 6;
|
|
969
973
|
/**
|
|
970
974
|
* Syncs an element's edit handles (`.ink-handle[data-handle]`) with `spec`. Each is an 8px
|
|
971
975
|
* `position:absolute; pointer-events:auto` box centred on its point; its look is the theme's.
|
|
@@ -999,15 +1003,62 @@ function syncHandles(node, spec) {
|
|
|
999
1003
|
h.style.top = `${at.y - HANDLE_PX / 2}px`;
|
|
1000
1004
|
}
|
|
1001
1005
|
}
|
|
1006
|
+
/** Width in CSS px a thin stroke's invisible hit twin is widened to. */
|
|
1007
|
+
const HIT_STROKE = 8;
|
|
1008
|
+
/**
|
|
1009
|
+
* True when `a` is drawn as a stroke alone (a line, polyline, ink, or an unfilled square,
|
|
1010
|
+
* circle or polygon with a border), so a press beside the stroke would miss it. These get an
|
|
1011
|
+
* invisible, wider hit twin of their geometry.
|
|
1012
|
+
*/
|
|
1013
|
+
function hasHitTwin(a) {
|
|
1014
|
+
switch (a.type) {
|
|
1015
|
+
case "line":
|
|
1016
|
+
case "polyline":
|
|
1017
|
+
case "ink": return true;
|
|
1018
|
+
case "polygon":
|
|
1019
|
+
case "square":
|
|
1020
|
+
case "circle": return a.interiorColor === void 0 && a.width > 0;
|
|
1021
|
+
default: return false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/** The width in CSS px of `a`'s hit twin: 8px, or the drawn stroke if that is wider. */
|
|
1025
|
+
function hitWidth(a, t) {
|
|
1026
|
+
return Math.max(HIT_STROKE, "width" in a ? t.toCssLength(a.width) : 0);
|
|
1027
|
+
}
|
|
1002
1028
|
/**
|
|
1003
1029
|
* The CSS box an annotation's default visual occupies. Usually `toCssRect(a.rect)`. Stroked
|
|
1004
1030
|
* paths (lines, polylines, polygons, ink) use the bounds of their own points instead of
|
|
1005
1031
|
* `rect` (which may already carry stroke padding, as a derived ink rect does), padded once by
|
|
1006
1032
|
* half their stroke width on every side, at least 1px in all, so a horizontal or vertical
|
|
1007
|
-
* line still has a box to draw and hit-test in.
|
|
1008
|
-
*
|
|
1033
|
+
* line still has a box to draw and hit-test in. A shape with a hit twin (see `hasHitTwin`)
|
|
1034
|
+
* is padded by at least 4px more, so its wider hit area is not clipped. Custom elements do
|
|
1035
|
+
* not use this: they sit on `toCssRect(a.rect)`.
|
|
1009
1036
|
*/
|
|
1010
1037
|
function annotationBox(a, t) {
|
|
1038
|
+
const drawn = handleBox(a, t);
|
|
1039
|
+
if (!hasHitTwin(a)) return drawn;
|
|
1040
|
+
const pad = hitPad(a, t);
|
|
1041
|
+
return {
|
|
1042
|
+
left: drawn.left - pad,
|
|
1043
|
+
top: drawn.top - pad,
|
|
1044
|
+
width: drawn.width + pad * 2,
|
|
1045
|
+
height: drawn.height + pad * 2
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* How far past `handleBox` a shape with a hit twin extends its element: the twin's overhang
|
|
1050
|
+
* beyond the drawn stroke, and at least 4px.
|
|
1051
|
+
*/
|
|
1052
|
+
function hitPad(a, t) {
|
|
1053
|
+
const overhang = Math.max(0, hitWidth(a, t) - t.toCssLength("width" in a ? a.width : 0));
|
|
1054
|
+
return Math.max(HIT_STROKE / 2, overhang / 2);
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The CSS box an annotation's edit handles and resizing work on: `annotationBox` without the
|
|
1058
|
+
* padding its hit twin needs. `toCssRect(a.rect)` for a rect-shaped annotation; the padded
|
|
1059
|
+
* bounds of its points for a stroked path.
|
|
1060
|
+
*/
|
|
1061
|
+
function handleBox(a, t) {
|
|
1011
1062
|
const points = a.type === "line" ? [a.start, a.end] : a.type === "polyline" || a.type === "polygon" ? a.vertices : a.type === "ink" ? a.inklist.flat() : void 0;
|
|
1012
1063
|
if (!points || points.length === 0 || !("width" in a)) return t.toCssRect(a.rect);
|
|
1013
1064
|
const box = t.toCssRect(pointBounds(points));
|
|
@@ -1200,7 +1251,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1200
1251
|
text.style.height = `${inner.height}px`;
|
|
1201
1252
|
}
|
|
1202
1253
|
text.style.pointerEvents = "auto";
|
|
1203
|
-
el.append(text);
|
|
1254
|
+
el.append(text, edgeBand(doc, box, freeTextLocalBox(a, t)));
|
|
1204
1255
|
appendSelectionFrame(el, a, t);
|
|
1205
1256
|
return;
|
|
1206
1257
|
}
|
|
@@ -1210,6 +1261,8 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1210
1261
|
}
|
|
1211
1262
|
const svg = boxSvg(doc, box.width, box.height);
|
|
1212
1263
|
svg.classList.add("ink-annotation-shape");
|
|
1264
|
+
/** Invisible wider copies of stroke-only geometry, added once the drawn shapes are. */
|
|
1265
|
+
const twins = [];
|
|
1213
1266
|
switch (a.type) {
|
|
1214
1267
|
case "highlight":
|
|
1215
1268
|
for (const q of a.quads) svg.append(node("polygon", {
|
|
@@ -1252,23 +1305,34 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1252
1305
|
case "square":
|
|
1253
1306
|
case "circle": {
|
|
1254
1307
|
const sw = t.toCssLength(a.width);
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1308
|
+
const pad = hasHitTwin(a) ? hitPad(a, t) : 0;
|
|
1309
|
+
const w = box.width - pad * 2;
|
|
1310
|
+
const h = box.height - pad * 2;
|
|
1311
|
+
const geometry = a.type === "square" ? {
|
|
1312
|
+
name: "rect",
|
|
1313
|
+
attrs: {
|
|
1314
|
+
x: pad + sw / 2,
|
|
1315
|
+
y: pad + sw / 2,
|
|
1316
|
+
width: Math.max(0, w - sw),
|
|
1317
|
+
height: Math.max(0, h - sw),
|
|
1318
|
+
...a.cornerRadius ? { rx: t.toCssLength(a.cornerRadius) } : {}
|
|
1319
|
+
}
|
|
1320
|
+
} : {
|
|
1321
|
+
name: "ellipse",
|
|
1322
|
+
attrs: {
|
|
1323
|
+
cx: box.width / 2,
|
|
1324
|
+
cy: box.height / 2,
|
|
1325
|
+
rx: Math.max(0, w / 2 - sw / 2),
|
|
1326
|
+
ry: Math.max(0, h / 2 - sw / 2)
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
const shape = node(geometry.name, geometry.attrs);
|
|
1267
1330
|
shape.setAttribute("fill", a.interiorColor ?? "none");
|
|
1268
1331
|
shape.setAttribute("stroke", a.color);
|
|
1269
1332
|
shape.setAttribute("stroke-width", String(sw));
|
|
1270
1333
|
dash(shape, a.dashes, t);
|
|
1271
1334
|
svg.append(shape);
|
|
1335
|
+
if (hasHitTwin(a)) twins.push(hitTwin(node, geometry.name, geometry.attrs, hitWidth(a, t)));
|
|
1272
1336
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1273
1337
|
break;
|
|
1274
1338
|
}
|
|
@@ -1320,6 +1384,10 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1320
1384
|
});
|
|
1321
1385
|
dash(stroked, a.dashes, t);
|
|
1322
1386
|
svg.append(stroked);
|
|
1387
|
+
if (hasHitTwin(a)) twins.push(hitTwin(node, stroked.tagName, {
|
|
1388
|
+
points: stroked.getAttribute("points"),
|
|
1389
|
+
"stroke-linejoin": "round"
|
|
1390
|
+
}, hitWidth(a, t)));
|
|
1323
1391
|
const ends = endingKinds(a);
|
|
1324
1392
|
if (ends && pts.length >= 2) for (const [name, kind, tip, other] of [[
|
|
1325
1393
|
"head",
|
|
@@ -1344,6 +1412,11 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1344
1412
|
});
|
|
1345
1413
|
ending.setAttribute("data-ending", name);
|
|
1346
1414
|
svg.append(ending);
|
|
1415
|
+
twins.push(hitTwin(node, "path", {
|
|
1416
|
+
d,
|
|
1417
|
+
"stroke-linejoin": "round",
|
|
1418
|
+
"stroke-linecap": "round"
|
|
1419
|
+
}, hitWidth(a, t)));
|
|
1347
1420
|
}
|
|
1348
1421
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1349
1422
|
break;
|
|
@@ -1356,14 +1429,54 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1356
1429
|
d: outlinePath(outline),
|
|
1357
1430
|
fill: a.color
|
|
1358
1431
|
}));
|
|
1432
|
+
twins.push(hitTwin(node, "polyline", {
|
|
1433
|
+
points: path.map(local).join(" "),
|
|
1434
|
+
"stroke-linejoin": "round",
|
|
1435
|
+
"stroke-linecap": "round"
|
|
1436
|
+
}, hitWidth(a, t)));
|
|
1359
1437
|
});
|
|
1360
1438
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1361
1439
|
}
|
|
1362
1440
|
for (const shape of svg.children) shape.setAttribute("pointer-events", "visiblePainted");
|
|
1441
|
+
svg.append(...twins);
|
|
1363
1442
|
el.append(svg);
|
|
1364
1443
|
appendCommentMarker(el, a, t);
|
|
1365
1444
|
}
|
|
1366
1445
|
/**
|
|
1446
|
+
* An invisible copy of a stroke-only shape's geometry, `width` CSS px wide, that takes
|
|
1447
|
+
* presses on the stroke alone (`[data-ink-hit-stroke]`), so a thin line is easy to hit.
|
|
1448
|
+
*/
|
|
1449
|
+
function hitTwin(node, name, attrs, width) {
|
|
1450
|
+
const twin = node(name, attrs);
|
|
1451
|
+
twin.setAttribute("data-ink-hit-stroke", "");
|
|
1452
|
+
twin.setAttribute("fill", "none");
|
|
1453
|
+
twin.setAttribute("stroke", "transparent");
|
|
1454
|
+
twin.setAttribute("stroke-width", String(width));
|
|
1455
|
+
twin.setAttribute("pointer-events", "stroke");
|
|
1456
|
+
return twin;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* The band a press on a free text's edge lands on: an invisible `rect[data-ink-edge]` stroke
|
|
1460
|
+
* `TEXT_EDGE` px wide along the text box's edge, half inside it and half out, in its own SVG
|
|
1461
|
+
* over the element. A press inside the box edits it; one on the band selects and drags it.
|
|
1462
|
+
* It is there whether or not the box draws a border. Geometry only.
|
|
1463
|
+
*/
|
|
1464
|
+
function edgeBand(doc, el, box) {
|
|
1465
|
+
const svg = boxSvg(doc, el.width, el.height);
|
|
1466
|
+
const band = doc.createElementNS(SVG$2, "rect");
|
|
1467
|
+
band.setAttribute("x", String(box.left));
|
|
1468
|
+
band.setAttribute("y", String(box.top));
|
|
1469
|
+
band.setAttribute("width", String(box.width));
|
|
1470
|
+
band.setAttribute("height", String(box.height));
|
|
1471
|
+
band.setAttribute("fill", "none");
|
|
1472
|
+
band.setAttribute("stroke", "transparent");
|
|
1473
|
+
band.setAttribute("stroke-width", String(TEXT_EDGE));
|
|
1474
|
+
band.setAttribute("pointer-events", "stroke");
|
|
1475
|
+
band.setAttribute("data-ink-edge", "");
|
|
1476
|
+
svg.append(band);
|
|
1477
|
+
return svg;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1367
1480
|
* A stamp's `img.ink-stamp`, filling the element's box. The image is upright in user space
|
|
1368
1481
|
* turned by the stamp's `rotation` (anticlockwise), and the page turns it again on screen
|
|
1369
1482
|
* (clockwise), so it is drawn turned by the difference: a signature placed against the page's
|
|
@@ -1660,7 +1773,8 @@ const SAME_PX = .5;
|
|
|
1660
1773
|
const MIN_WIDTH_EM = 2;
|
|
1661
1774
|
/**
|
|
1662
1775
|
* Opens an editor on `el`, the overlay element showing `a` (or a stand-in for a box not in the
|
|
1663
|
-
* store yet)
|
|
1776
|
+
* store yet), with the caret at `opts.caret` or after the text. `Escape` commits; a press
|
|
1777
|
+
* outside the editor, or focus leaving it, commits too.
|
|
1664
1778
|
* Closing while the editor has focus hands focus to `el`. Keys typed in the
|
|
1665
1779
|
* editor stop there, so no viewport or tool shortcut sees them. Opening a second editor on the
|
|
1666
1780
|
* same element commits the first.
|
|
@@ -1716,7 +1830,7 @@ function editFreeText(el, a, opts) {
|
|
|
1716
1830
|
return Math.abs(grown) < SAME_PX ? 0 : grown;
|
|
1717
1831
|
};
|
|
1718
1832
|
let closed = false;
|
|
1719
|
-
const finish = (commit) => {
|
|
1833
|
+
const finish = (commit, how) => {
|
|
1720
1834
|
if (closed) return;
|
|
1721
1835
|
closed = true;
|
|
1722
1836
|
const contents = read();
|
|
@@ -1730,27 +1844,27 @@ function editFreeText(el, a, opts) {
|
|
|
1730
1844
|
box?.style.removeProperty("visibility");
|
|
1731
1845
|
el.style.height = `${base.height}px`;
|
|
1732
1846
|
refitBubble(el, a, opts.transform, base);
|
|
1733
|
-
if (commit) opts.commit(contents, next, grown);
|
|
1847
|
+
if (commit) opts.commit(contents, next, grown, how);
|
|
1734
1848
|
else opts.cancel();
|
|
1735
1849
|
};
|
|
1736
1850
|
const onOutside = (e) => {
|
|
1737
1851
|
if (e.target instanceof Node && field.contains(e.target)) return;
|
|
1738
1852
|
closingPresses.add(e);
|
|
1739
|
-
finish(true);
|
|
1853
|
+
finish(true, "press");
|
|
1740
1854
|
};
|
|
1741
1855
|
view?.addEventListener("pointerdown", onOutside, true);
|
|
1742
1856
|
field.addEventListener("keydown", (e) => {
|
|
1743
1857
|
e.stopPropagation();
|
|
1744
1858
|
if (e.key === "Escape" && !e.isComposing) {
|
|
1745
1859
|
e.preventDefault();
|
|
1746
|
-
finish(true);
|
|
1860
|
+
finish(true, "escape");
|
|
1747
1861
|
}
|
|
1748
1862
|
});
|
|
1749
1863
|
field.addEventListener("focusout", (e) => {
|
|
1750
1864
|
if (!doc.hasFocus()) return;
|
|
1751
1865
|
const to = e.relatedTarget;
|
|
1752
1866
|
if (to instanceof Node && field.contains(to)) return;
|
|
1753
|
-
finish(true);
|
|
1867
|
+
finish(true, "focus");
|
|
1754
1868
|
});
|
|
1755
1869
|
field.addEventListener("paste", (e) => {
|
|
1756
1870
|
e.preventDefault();
|
|
@@ -1762,14 +1876,17 @@ function editFreeText(el, a, opts) {
|
|
|
1762
1876
|
field.focus({ preventScroll: true });
|
|
1763
1877
|
const selection = doc.getSelection();
|
|
1764
1878
|
if (selection) {
|
|
1765
|
-
|
|
1766
|
-
range
|
|
1767
|
-
|
|
1879
|
+
let range = opts.caret && caretIn(field, opts.caret);
|
|
1880
|
+
if (!range) {
|
|
1881
|
+
range = doc.createRange();
|
|
1882
|
+
range.selectNodeContents(field);
|
|
1883
|
+
range.collapse(false);
|
|
1884
|
+
}
|
|
1768
1885
|
selection.removeAllRanges();
|
|
1769
1886
|
selection.addRange(range);
|
|
1770
1887
|
}
|
|
1771
1888
|
grow();
|
|
1772
|
-
const editor = { close: (commit) => finish(commit) };
|
|
1889
|
+
const editor = { close: (commit) => finish(commit, "api") };
|
|
1773
1890
|
editors.set(el, editor);
|
|
1774
1891
|
return editor;
|
|
1775
1892
|
}
|
|
@@ -1785,37 +1902,44 @@ function canEditFreeText(a, el) {
|
|
|
1785
1902
|
* Opens an editor on the overlay element `el` for the free-text box `id`, wired to the store: a
|
|
1786
1903
|
* commit is one `update` with the text and the grown rect (none when neither changed), and an
|
|
1787
1904
|
* empty box is removed, as an abandoned note is. A speech bubble is the exception: its outline
|
|
1788
|
-
* shows without text, so it is kept.
|
|
1789
|
-
*
|
|
1905
|
+
* shows without text, so it is kept. `onClose` runs after the store change, with what closed
|
|
1906
|
+
* the editor; `caret` is a client point to put the caret at (`editFreeText`). Returns
|
|
1907
|
+
* undefined when the box cannot be edited (`canEditFreeText`).
|
|
1790
1908
|
*/
|
|
1791
|
-
function editFreeTextIn(store, el, id, transform) {
|
|
1909
|
+
function editFreeTextIn(store, el, id, transform, opts = {}) {
|
|
1792
1910
|
const a = store._getProposed(id) ?? store.get(id);
|
|
1793
1911
|
if (!canEditFreeText(a, el)) return void 0;
|
|
1794
1912
|
const current = () => {
|
|
1795
1913
|
const now = store._getProposed(id) ?? store.get(id);
|
|
1796
1914
|
return now?.type === "freetext" ? now : void 0;
|
|
1797
1915
|
};
|
|
1916
|
+
const write = (contents, grown) => {
|
|
1917
|
+
const now = current();
|
|
1918
|
+
if (!now) return;
|
|
1919
|
+
if (contents.trim() === "" && !isBubble(now)) {
|
|
1920
|
+
store.remove(id);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (grown === 0) {
|
|
1924
|
+
if (contents !== now.contents) store.update(id, { contents });
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
store.update(id, {
|
|
1928
|
+
contents,
|
|
1929
|
+
...grownFreeText(now, grown, transform)
|
|
1930
|
+
});
|
|
1931
|
+
};
|
|
1798
1932
|
return editFreeText(el, a, {
|
|
1799
1933
|
transform,
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
store.remove(id);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
if (grown === 0) {
|
|
1808
|
-
if (contents !== now.contents) store.update(id, { contents });
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
store.update(id, {
|
|
1812
|
-
contents,
|
|
1813
|
-
...grownFreeText(now, grown, transform)
|
|
1814
|
-
});
|
|
1934
|
+
caret: opts.caret,
|
|
1935
|
+
commit(contents, _rect, grown, how) {
|
|
1936
|
+
write(contents, grown);
|
|
1937
|
+
opts.onClose?.(how);
|
|
1815
1938
|
},
|
|
1816
1939
|
cancel() {
|
|
1817
1940
|
const now = current();
|
|
1818
1941
|
if (now && now.contents.trim() === "" && !isBubble(now)) store.remove(id);
|
|
1942
|
+
opts.onClose?.("api");
|
|
1819
1943
|
}
|
|
1820
1944
|
});
|
|
1821
1945
|
}
|
|
@@ -1844,6 +1968,28 @@ function grownFreeText(a, grown, t) {
|
|
|
1844
1968
|
};
|
|
1845
1969
|
return withBox(a, box, resized.callout.map((p, i) => i === last ? p : down(p)));
|
|
1846
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* A collapsed range at the text position under a client point, if that position is in `field`.
|
|
1973
|
+
* The editor lies over the box with the same look, so a point on the box lands on the same
|
|
1974
|
+
* character in the editor.
|
|
1975
|
+
*/
|
|
1976
|
+
function caretIn(field, at) {
|
|
1977
|
+
const doc = field.ownerDocument;
|
|
1978
|
+
let node;
|
|
1979
|
+
let offset = 0;
|
|
1980
|
+
if (typeof doc.caretPositionFromPoint === "function") {
|
|
1981
|
+
const p = doc.caretPositionFromPoint(at.clientX, at.clientY);
|
|
1982
|
+
if (p) ({offsetNode: node, offset} = p);
|
|
1983
|
+
} else if (typeof doc.caretRangeFromPoint === "function") {
|
|
1984
|
+
const r = doc.caretRangeFromPoint(at.clientX, at.clientY);
|
|
1985
|
+
if (r) ({startContainer: node, startOffset: offset} = r);
|
|
1986
|
+
}
|
|
1987
|
+
if (!node || !field.contains(node)) return void 0;
|
|
1988
|
+
const range = doc.createRange();
|
|
1989
|
+
range.setStart(node, offset);
|
|
1990
|
+
range.collapse(true);
|
|
1991
|
+
return range;
|
|
1992
|
+
}
|
|
1847
1993
|
/** `plaintext-only` where the browser has it, else `true` (paste and formatting are then guarded). */
|
|
1848
1994
|
function makeEditable(field) {
|
|
1849
1995
|
try {
|
|
@@ -1874,7 +2020,7 @@ function insertText(field, text) {
|
|
|
1874
2020
|
}
|
|
1875
2021
|
//#endregion
|
|
1876
2022
|
//#region src/pdfjs/render-region.ts
|
|
1877
|
-
const isCancellation = (error) => error?.name === "RenderingCancelledException";
|
|
2023
|
+
const isCancellation$1 = (error) => error?.name === "RenderingCancelledException";
|
|
1878
2024
|
/**
|
|
1879
2025
|
* Draws `userRect` of `page` (0-based) into `canvas` at `scale` device pixels per user-space
|
|
1880
2026
|
* unit (times `/UserUnit`, as PDF.js applies it), turned by `rotation`: the page's /Rotate plus
|
|
@@ -1917,7 +2063,7 @@ async function renderRegion(doc, page, userRect, scale, canvas, signal, rotation
|
|
|
1917
2063
|
try {
|
|
1918
2064
|
await task.promise;
|
|
1919
2065
|
} catch (error) {
|
|
1920
|
-
if (signal?.aborted && isCancellation(error)) throw signal.reason;
|
|
2066
|
+
if (signal?.aborted && isCancellation$1(error)) throw signal.reason;
|
|
1921
2067
|
throw error;
|
|
1922
2068
|
} finally {
|
|
1923
2069
|
signal?.removeEventListener("abort", cancel);
|
|
@@ -2160,27 +2306,69 @@ const LABELS = {
|
|
|
2160
2306
|
mask: "Mask",
|
|
2161
2307
|
redact: "Redaction"
|
|
2162
2308
|
};
|
|
2163
|
-
/** The `data-confidence` bucket for a suggested redaction; undefined for everything else. */
|
|
2164
|
-
function confidenceAttr(a) {
|
|
2165
|
-
return a.type === "redact" && a.suggestion ? confidenceBucket(a.suggestion.confidence) : void 0;
|
|
2166
|
-
}
|
|
2167
2309
|
function ariaLabel(a) {
|
|
2168
|
-
const base = a.
|
|
2310
|
+
const base = a.subject === "Signature" && (a.type === "ink" || a.type === "stamp") ? "Signature" : LABELS[a.type];
|
|
2169
2311
|
const named = hasComment(a) ? `${base} with comment` : base;
|
|
2170
2312
|
return a.author ? `${named} by ${a.author}` : named;
|
|
2171
2313
|
}
|
|
2172
2314
|
/** Reading order in user space: top edge first (y grows upwards), then left edge. */
|
|
2173
|
-
function readingOrder(a, b) {
|
|
2315
|
+
function readingOrder$1(a, b) {
|
|
2174
2316
|
return b.rect.y + b.rect.height - (a.rect.y + a.rect.height) || a.rect.x - b.rect.x;
|
|
2175
2317
|
}
|
|
2318
|
+
/** A decoration's data key: lower-case kebab, as written after `data-`. */
|
|
2319
|
+
const DECORATION_KEY = /^[a-z][a-z0-9-]*$/;
|
|
2320
|
+
/**
|
|
2321
|
+
* `data-*` keys core writes on an annotation element or inside it; a decoration cannot set
|
|
2322
|
+
* them. `data-ink-*` is core's too.
|
|
2323
|
+
*/
|
|
2324
|
+
const RESERVED_DATA = /* @__PURE__ */ new Set([
|
|
2325
|
+
"annotation",
|
|
2326
|
+
"type",
|
|
2327
|
+
"source",
|
|
2328
|
+
"locked",
|
|
2329
|
+
"state",
|
|
2330
|
+
"custom",
|
|
2331
|
+
"handle",
|
|
2332
|
+
"framed",
|
|
2333
|
+
"align",
|
|
2334
|
+
"bubble",
|
|
2335
|
+
"border",
|
|
2336
|
+
"icon",
|
|
2337
|
+
"ending"
|
|
2338
|
+
]);
|
|
2339
|
+
/** Skipped decoration keys and classes already warned about. */
|
|
2340
|
+
const warnedDecorations = /* @__PURE__ */ new Set();
|
|
2341
|
+
function warnDecoration(what, why) {
|
|
2342
|
+
if (isProduction() || warnedDecorations.has(what)) return;
|
|
2343
|
+
warnedDecorations.add(what);
|
|
2344
|
+
console.warn(`[inkbar] annotation decoration: skipped ${what}: ${why}`);
|
|
2345
|
+
}
|
|
2346
|
+
function usableData(key) {
|
|
2347
|
+
if (!DECORATION_KEY.test(key)) {
|
|
2348
|
+
warnDecoration(`data key "${key}"`, "keys are lower-case kebab");
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2351
|
+
if (RESERVED_DATA.has(key) || key === "ink" || key.startsWith("ink-")) {
|
|
2352
|
+
warnDecoration(`data key "${key}"`, "core sets it");
|
|
2353
|
+
return false;
|
|
2354
|
+
}
|
|
2355
|
+
return true;
|
|
2356
|
+
}
|
|
2357
|
+
function usableClass(name) {
|
|
2358
|
+
if (name.startsWith("ink-")) {
|
|
2359
|
+
warnDecoration(`class "${name}"`, "`ink-` classes are core's");
|
|
2360
|
+
return false;
|
|
2361
|
+
}
|
|
2362
|
+
if (name === "" || /\s/.test(name)) {
|
|
2363
|
+
warnDecoration(`class "${name}"`, "not a single class name");
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
return true;
|
|
2367
|
+
}
|
|
2176
2368
|
function setFlag(node, key, on) {
|
|
2177
2369
|
if (on) node.dataset[key] = "";
|
|
2178
2370
|
else delete node.dataset[key];
|
|
2179
2371
|
}
|
|
2180
|
-
function setData(node, key, value) {
|
|
2181
|
-
if (value === void 0) delete node.dataset[key];
|
|
2182
|
-
else node.dataset[key] = value;
|
|
2183
|
-
}
|
|
2184
2372
|
function createOverlay(opts) {
|
|
2185
2373
|
const { store, page } = opts;
|
|
2186
2374
|
const doc = opts.host.ownerDocument;
|
|
@@ -2199,6 +2387,8 @@ function createOverlay(opts) {
|
|
|
2199
2387
|
/** What each element's content was last drawn from; absent when it has none of ours. */
|
|
2200
2388
|
const drawn = /* @__PURE__ */ new Map();
|
|
2201
2389
|
const hovered = /* @__PURE__ */ new Set();
|
|
2390
|
+
/** The decoration each element last got, so a later one can drop what it no longer has. */
|
|
2391
|
+
const decorated = /* @__PURE__ */ new WeakMap();
|
|
2202
2392
|
let order = [];
|
|
2203
2393
|
let destroyed = false;
|
|
2204
2394
|
function stateOf(id, selected) {
|
|
@@ -2213,7 +2403,17 @@ function createOverlay(opts) {
|
|
|
2213
2403
|
node.setAttribute("aria-pressed", String(state === "selected"));
|
|
2214
2404
|
node.tabIndex = !anyHere || state === "selected" ? 0 : -1;
|
|
2215
2405
|
const a = store.get(id);
|
|
2216
|
-
syncHandles(node, a && !a.locked && state === "selected" &&
|
|
2406
|
+
syncHandles(node, a && !a.locked && state === "selected" && editsSelection() ? handleSpec(node, a) : { kind: "none" });
|
|
2407
|
+
}
|
|
2408
|
+
/** `handleBox` relative to an element at `left`,`top`. */
|
|
2409
|
+
function handleLocalBox(a, left, top) {
|
|
2410
|
+
const box = handleBox(a, transform);
|
|
2411
|
+
return {
|
|
2412
|
+
left: box.left - left,
|
|
2413
|
+
top: box.top - top,
|
|
2414
|
+
width: box.width,
|
|
2415
|
+
height: box.height
|
|
2416
|
+
};
|
|
2217
2417
|
}
|
|
2218
2418
|
/**
|
|
2219
2419
|
* Line ends for a line, corners for what a corner drag resizes (a free text's on its text
|
|
@@ -2242,8 +2442,7 @@ function createOverlay(opts) {
|
|
|
2242
2442
|
...freeTextLocalBox(a, transform)
|
|
2243
2443
|
} : {
|
|
2244
2444
|
kind: "corners",
|
|
2245
|
-
|
|
2246
|
-
height: parseFloat(node.style.height) || 0
|
|
2445
|
+
...handleLocalBox(a, left, top)
|
|
2247
2446
|
};
|
|
2248
2447
|
if (!hasCallout(a)) return corners;
|
|
2249
2448
|
const tip = transform.toCss(a.callout[0]);
|
|
@@ -2257,11 +2456,13 @@ function createOverlay(opts) {
|
|
|
2257
2456
|
}
|
|
2258
2457
|
/**
|
|
2259
2458
|
* True while the tool controller marks the tool root (an ancestor) with a select tool,
|
|
2260
|
-
* `data-ink-tool="select"` or `"select-text"
|
|
2459
|
+
* `data-ink-tool="select"` or `"select-text"`, or with `data-ink-made`: what a creation tool
|
|
2460
|
+
* just made can be moved and resized too.
|
|
2261
2461
|
*/
|
|
2262
|
-
function
|
|
2263
|
-
const
|
|
2264
|
-
|
|
2462
|
+
function editsSelection() {
|
|
2463
|
+
const root = opts.host.closest("[data-ink-tool]");
|
|
2464
|
+
const tool = root?.getAttribute("data-ink-tool");
|
|
2465
|
+
return tool === "select" || tool === "select-text" || root?.hasAttribute("data-ink-made") === true;
|
|
2265
2466
|
}
|
|
2266
2467
|
const toolRoot = opts.host.closest("[data-ink-tool]") ?? opts.host.closest("[data-ink-viewport]");
|
|
2267
2468
|
const Observer = doc.defaultView?.MutationObserver;
|
|
@@ -2273,16 +2474,48 @@ function createOverlay(opts) {
|
|
|
2273
2474
|
});
|
|
2274
2475
|
toolObserver.observe(toolRoot, {
|
|
2275
2476
|
attributes: true,
|
|
2276
|
-
attributeFilter: ["data-ink-tool"]
|
|
2477
|
+
attributeFilter: ["data-ink-tool", "data-ink-made"]
|
|
2277
2478
|
});
|
|
2278
2479
|
}
|
|
2279
2480
|
function applyAttributes(node, a) {
|
|
2280
2481
|
node.dataset.type = a.type;
|
|
2281
2482
|
node.dataset.source = a.source;
|
|
2282
2483
|
setFlag(node, "locked", a.locked === true);
|
|
2283
|
-
setData(node, "origin", a.type === "redact" ? a.origin : void 0);
|
|
2284
|
-
setData(node, "confidence", confidenceAttr(a));
|
|
2285
2484
|
node.setAttribute("aria-label", ariaLabel(a));
|
|
2485
|
+
applyDecoration(node, a);
|
|
2486
|
+
}
|
|
2487
|
+
/** The app's data attributes, classes and description, from `decorate`; a throw adds none. */
|
|
2488
|
+
function applyDecoration(node, a) {
|
|
2489
|
+
let decoration;
|
|
2490
|
+
try {
|
|
2491
|
+
decoration = opts.decorate?.(a);
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
console.error(error);
|
|
2494
|
+
}
|
|
2495
|
+
const prev = decorated.get(node);
|
|
2496
|
+
if (!decoration && !prev) return;
|
|
2497
|
+
const next = {
|
|
2498
|
+
data: /* @__PURE__ */ new Set(),
|
|
2499
|
+
classes: /* @__PURE__ */ new Set(),
|
|
2500
|
+
description: false
|
|
2501
|
+
};
|
|
2502
|
+
for (const [key, value] of Object.entries(decoration?.data ?? {})) {
|
|
2503
|
+
if (!usableData(key)) continue;
|
|
2504
|
+
node.setAttribute(`data-${key}`, value);
|
|
2505
|
+
next.data.add(key);
|
|
2506
|
+
}
|
|
2507
|
+
for (const name of decoration?.classes ?? []) {
|
|
2508
|
+
if (!usableClass(name)) continue;
|
|
2509
|
+
node.classList.add(name);
|
|
2510
|
+
next.classes.add(name);
|
|
2511
|
+
}
|
|
2512
|
+
if (decoration?.description) {
|
|
2513
|
+
node.setAttribute("aria-description", decoration.description);
|
|
2514
|
+
next.description = true;
|
|
2515
|
+
} else if (prev?.description) node.removeAttribute("aria-description");
|
|
2516
|
+
for (const key of prev?.data ?? []) if (!next.data.has(key)) node.removeAttribute(`data-${key}`);
|
|
2517
|
+
for (const name of prev?.classes ?? []) if (!next.classes.has(name)) node.classList.remove(name);
|
|
2518
|
+
decorated.set(node, next);
|
|
2286
2519
|
}
|
|
2287
2520
|
function applyGeometry(node, a) {
|
|
2288
2521
|
const box = opts.isCustom?.(a) === true ? transform.toCssRect(a.rect) : annotationBox(a, transform);
|
|
@@ -2496,7 +2729,7 @@ function createOverlay(opts) {
|
|
|
2496
2729
|
}
|
|
2497
2730
|
function render() {
|
|
2498
2731
|
if (destroyed) return;
|
|
2499
|
-
const current = withoutReplies(store.list.get()).filter((a) => a.page === page).sort(readingOrder);
|
|
2732
|
+
const current = withoutReplies(store.list.get()).filter((a) => a.page === page).sort(readingOrder$1);
|
|
2500
2733
|
const ids = new Set(current.map((a) => a.id));
|
|
2501
2734
|
const active = doc.activeElement;
|
|
2502
2735
|
let focusId = [...elements].find(([, n]) => n === active)?.[0];
|
|
@@ -2561,6 +2794,13 @@ function createOverlay(opts) {
|
|
|
2561
2794
|
render();
|
|
2562
2795
|
},
|
|
2563
2796
|
elementFor: (id) => elements.get(id),
|
|
2797
|
+
redecorate() {
|
|
2798
|
+
if (destroyed) return;
|
|
2799
|
+
for (const [id, node] of elements) {
|
|
2800
|
+
const a = store.get(id);
|
|
2801
|
+
if (a) applyDecoration(node, a);
|
|
2802
|
+
}
|
|
2803
|
+
},
|
|
2564
2804
|
destroy() {
|
|
2565
2805
|
if (destroyed) return;
|
|
2566
2806
|
closeEditors();
|
|
@@ -2580,114 +2820,1730 @@ function createOverlay(opts) {
|
|
|
2580
2820
|
};
|
|
2581
2821
|
}
|
|
2582
2822
|
//#endregion
|
|
2583
|
-
//#region src/dom/
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
2597
|
-
...callbacks("canvas")
|
|
2598
|
-
});
|
|
2599
|
-
const textLayer = createTextLayer({
|
|
2600
|
-
document,
|
|
2601
|
-
page,
|
|
2602
|
-
host,
|
|
2603
|
-
transform,
|
|
2604
|
-
...callbacks("text")
|
|
2605
|
-
});
|
|
2606
|
-
const overlay = createOverlay({
|
|
2607
|
-
host,
|
|
2608
|
-
page,
|
|
2609
|
-
store: opts.store,
|
|
2610
|
-
transform,
|
|
2611
|
-
document,
|
|
2612
|
-
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
2613
|
-
...opts.onActivate ? { onActivate: (id) => opts.onActivate?.(id) } : {},
|
|
2614
|
-
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {}
|
|
2823
|
+
//#region src/dom/plugins/api/command.ts
|
|
2824
|
+
const DEFAULT_ERROR_STATUS = "Something went wrong.";
|
|
2825
|
+
/** A failure that is the run being cancelled, not a bug: not logged, no error status. */
|
|
2826
|
+
function isCancellation(error) {
|
|
2827
|
+
if (isInkError(error) && error.code === "DocumentDestroyed") return true;
|
|
2828
|
+
return error instanceof Error && error.name === "AbortError";
|
|
2829
|
+
}
|
|
2830
|
+
function createCommand(ctx, id, label, run, opts = {}) {
|
|
2831
|
+
const status = writable("");
|
|
2832
|
+
let controller = null;
|
|
2833
|
+
ctx.onDocument(() => () => {
|
|
2834
|
+
controller?.abort();
|
|
2835
|
+
status.set("");
|
|
2615
2836
|
});
|
|
2616
|
-
return {
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2837
|
+
return ctx.commands.add({
|
|
2838
|
+
id,
|
|
2839
|
+
label,
|
|
2840
|
+
...opts.icon && { icon: opts.icon },
|
|
2841
|
+
...opts.enabled && { enabled: opts.enabled },
|
|
2842
|
+
status: readonly(status),
|
|
2843
|
+
async run() {
|
|
2844
|
+
const doc = ctx.document.get();
|
|
2845
|
+
if (!doc) return;
|
|
2846
|
+
const own = new AbortController();
|
|
2847
|
+
controller = own;
|
|
2848
|
+
status.set("");
|
|
2849
|
+
try {
|
|
2850
|
+
const result = await run({
|
|
2851
|
+
doc,
|
|
2852
|
+
signal: own.signal
|
|
2853
|
+
});
|
|
2854
|
+
if (!own.signal.aborted) status.set(result ?? "");
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
if (own.signal.aborted || isCancellation(error)) return;
|
|
2857
|
+
console.error(error);
|
|
2858
|
+
status.set(opts.errorStatus ?? DEFAULT_ERROR_STATUS);
|
|
2859
|
+
} finally {
|
|
2860
|
+
if (controller === own) controller = null;
|
|
2861
|
+
}
|
|
2629
2862
|
}
|
|
2630
|
-
};
|
|
2863
|
+
});
|
|
2631
2864
|
}
|
|
2632
2865
|
//#endregion
|
|
2633
|
-
//#region src/dom/
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
return {
|
|
2652
|
-
readable(id) {
|
|
2653
|
-
entry(id);
|
|
2654
|
-
return views.get(id);
|
|
2655
|
-
},
|
|
2656
|
-
get: (id) => entry(id).get(),
|
|
2657
|
-
merge(id, patch) {
|
|
2658
|
-
const w = entry(id);
|
|
2659
|
-
w.set({
|
|
2660
|
-
...w.get(),
|
|
2661
|
-
...patch
|
|
2662
|
-
});
|
|
2663
|
-
},
|
|
2664
|
-
register
|
|
2866
|
+
//#region src/dom/annotation-element.ts
|
|
2867
|
+
/** The overlay element drawn for annotation `id` under `root`, if it is in the DOM. */
|
|
2868
|
+
function annotationElementFor(root, id) {
|
|
2869
|
+
return root.querySelector(`[data-annotation="${CSS.escape(id)}"]`);
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Resolves the overlay element for `id` and keeps resolving while `root`'s subtree changes:
|
|
2873
|
+
* the overlay creates and removes these elements outside any framework (a page mounts as it
|
|
2874
|
+
* scrolls into view), so a popover anchored to one has to follow. Calls `onChange` once now
|
|
2875
|
+
* and again whenever the answer changes. The returned function stops observing.
|
|
2876
|
+
*/
|
|
2877
|
+
function observeAnnotationElement(root, id, onChange) {
|
|
2878
|
+
let last;
|
|
2879
|
+
const resolve = () => {
|
|
2880
|
+
const found = annotationElementFor(root, id);
|
|
2881
|
+
if (found === last) return;
|
|
2882
|
+
last = found;
|
|
2883
|
+
onChange(found);
|
|
2665
2884
|
};
|
|
2885
|
+
resolve();
|
|
2886
|
+
const observer = new MutationObserver(resolve);
|
|
2887
|
+
observer.observe(root, {
|
|
2888
|
+
childList: true,
|
|
2889
|
+
subtree: true
|
|
2890
|
+
});
|
|
2891
|
+
return () => observer.disconnect();
|
|
2666
2892
|
}
|
|
2667
2893
|
//#endregion
|
|
2668
|
-
//#region src/dom/
|
|
2669
|
-
/**
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2894
|
+
//#region src/dom/popover-focus.ts
|
|
2895
|
+
/**
|
|
2896
|
+
* A press that will activate an annotation is not a dismissal: the popover follows the
|
|
2897
|
+
* activation. Mirrors the overlay's rule: a press on the annotation element or on its drawn
|
|
2898
|
+
* shape activates; a press on a custom element's own content belongs to that content.
|
|
2899
|
+
*/
|
|
2900
|
+
function shouldKeepOpenOnAnnotation(target) {
|
|
2901
|
+
if (!(target instanceof Element)) return false;
|
|
2902
|
+
const annotation = target.closest("[data-annotation]");
|
|
2903
|
+
if (!annotation) return false;
|
|
2904
|
+
return target === annotation || !annotation.hasAttribute("data-custom");
|
|
2676
2905
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2906
|
+
function createFocusReturn(opts) {
|
|
2907
|
+
let fallbacks = [];
|
|
2908
|
+
/** Set by `beforeRemove`: the close comes from removing the anchor's annotation. */
|
|
2909
|
+
let removing = false;
|
|
2680
2910
|
return {
|
|
2681
|
-
|
|
2682
|
-
|
|
2911
|
+
beforeRemove(anchor, prefer) {
|
|
2912
|
+
fallbacks = [];
|
|
2913
|
+
removing = true;
|
|
2914
|
+
const siblings = anchor?.parentElement ? [...anchor.parentElement.children].filter((el) => el instanceof HTMLElement && el.hasAttribute("data-annotation")) : [];
|
|
2915
|
+
const i = anchor ? siblings.indexOf(anchor) : -1;
|
|
2916
|
+
if (i >= 0) {
|
|
2917
|
+
const after = siblings.slice(i + 1);
|
|
2918
|
+
const preferred = prefer ? after.filter((el) => el.matches(prefer)) : [];
|
|
2919
|
+
fallbacks.push(...preferred, ...after, ...siblings.slice(0, i).reverse());
|
|
2920
|
+
}
|
|
2921
|
+
},
|
|
2922
|
+
onCloseAutoFocus(event, anchor) {
|
|
2923
|
+
event.preventDefault();
|
|
2924
|
+
const plan = fallbacks;
|
|
2925
|
+
const removed = removing;
|
|
2926
|
+
fallbacks = [];
|
|
2927
|
+
removing = false;
|
|
2928
|
+
const root = opts.root();
|
|
2929
|
+
if (!root) return;
|
|
2930
|
+
const activated = opts.activated();
|
|
2931
|
+
if (activated !== null && annotationElementFor(root, activated)) return;
|
|
2932
|
+
const active = root.ownerDocument.activeElement;
|
|
2933
|
+
if (active && active !== root && root.contains(active)) return;
|
|
2934
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
2935
|
+
(removed ? [...plan, scroll] : [
|
|
2936
|
+
anchor,
|
|
2937
|
+
...plan,
|
|
2938
|
+
scroll
|
|
2939
|
+
]).find((el) => el?.isConnected)?.focus({ preventScroll: true });
|
|
2940
|
+
}
|
|
2683
2941
|
};
|
|
2684
2942
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2943
|
+
//#endregion
|
|
2944
|
+
//#region src/dom/viewer/dom.ts
|
|
2945
|
+
/** `document.createElement` with attributes and children. */
|
|
2946
|
+
function h(tag, attrs = {}, ...children) {
|
|
2947
|
+
const el = document.createElement(tag);
|
|
2948
|
+
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
2949
|
+
el.append(...children);
|
|
2950
|
+
return el;
|
|
2951
|
+
}
|
|
2952
|
+
function button$1(label, onClick) {
|
|
2953
|
+
const b = h("button", { type: "button" }, label);
|
|
2954
|
+
b.addEventListener("click", onClick);
|
|
2955
|
+
return b;
|
|
2956
|
+
}
|
|
2957
|
+
//#endregion
|
|
2958
|
+
//#region src/dom/viewer/popover.ts
|
|
2959
|
+
/** Space between the anchor and the panel, and between the panel and the window edge. */
|
|
2960
|
+
const GAP = 6;
|
|
2961
|
+
const MARGIN = 8;
|
|
2962
|
+
const TABBABLE$1 = "button, input, select, textarea, a[href], [tabindex]";
|
|
2963
|
+
/**
|
|
2964
|
+
* Positions `panel` (fixed) below the `anchor` rectangle, or above it when only that fits,
|
|
2965
|
+
* kept inside the window. For panels anchored to something other than an annotation.
|
|
2966
|
+
*/
|
|
2967
|
+
function placePanel(panel, anchor) {
|
|
2968
|
+
const { innerWidth: vw, innerHeight: vh } = window;
|
|
2969
|
+
panel.style.position = "fixed";
|
|
2970
|
+
panel.style.left = "0px";
|
|
2971
|
+
panel.style.top = "0px";
|
|
2972
|
+
const w = panel.offsetWidth;
|
|
2973
|
+
const ht = panel.offsetHeight;
|
|
2974
|
+
let top = anchor.bottom + GAP;
|
|
2975
|
+
if (top + ht > vh - MARGIN && anchor.top - GAP - ht >= MARGIN) top = anchor.top - GAP - ht;
|
|
2976
|
+
top = Math.max(MARGIN, Math.min(top, vh - ht - MARGIN));
|
|
2977
|
+
const left = Math.max(MARGIN, Math.min(anchor.left, vw - w - MARGIN));
|
|
2978
|
+
panel.style.left = `${left}px`;
|
|
2979
|
+
panel.style.top = `${top}px`;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Calls `onChange` with `el` while it is under `root` and null while it is not: once now, then
|
|
2983
|
+
* whenever that changes. Returns a function that stops observing.
|
|
2984
|
+
*/
|
|
2985
|
+
function observeElement(root, el, onChange) {
|
|
2986
|
+
let last;
|
|
2987
|
+
const resolve = () => {
|
|
2988
|
+
const found = root.contains(el) ? el : null;
|
|
2989
|
+
if (found === last) return;
|
|
2990
|
+
last = found;
|
|
2991
|
+
onChange(found);
|
|
2992
|
+
};
|
|
2993
|
+
resolve();
|
|
2994
|
+
const observer = new MutationObserver(resolve);
|
|
2995
|
+
observer.observe(root, {
|
|
2996
|
+
childList: true,
|
|
2997
|
+
subtree: true
|
|
2998
|
+
});
|
|
2999
|
+
return () => observer.disconnect();
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Creates a non-modal `role="dialog"` panel on `<body>`, marked `data-ink-popover` so the theme
|
|
3003
|
+
* gives it its tokens, that an app anchors to an annotation with `show(id)` (for instance on
|
|
3004
|
+
* `viewer.activated`), or to any element in the viewer with `showAt(el)`. The panel follows the annotation's element as pages mount and unmount:
|
|
3005
|
+
* it hides while the element is gone and comes back with it. Tab loops inside the panel, and
|
|
3006
|
+
* Escape or a press outside calls `onDismiss`. Call `destroy` when done.
|
|
3007
|
+
*/
|
|
3008
|
+
function createPopover(opts) {
|
|
3009
|
+
const { root, focusReturn } = opts;
|
|
3010
|
+
const panel = h("div", {
|
|
3011
|
+
...opts.attrs,
|
|
3012
|
+
role: "dialog",
|
|
3013
|
+
"data-ink-popover": "",
|
|
3014
|
+
hidden: ""
|
|
3015
|
+
});
|
|
3016
|
+
document.body.append(panel);
|
|
3017
|
+
/** What the panel is anchored to: an annotation id, or an element from `showAt`. */
|
|
3018
|
+
let anchored = null;
|
|
3019
|
+
let anchor = null;
|
|
3020
|
+
let stopObserving = () => void 0;
|
|
3021
|
+
/** The element that had focus in the panel when the anchor went away. */
|
|
3022
|
+
let stranded = null;
|
|
3023
|
+
const place = () => {
|
|
3024
|
+
if (!anchor || panel.hidden) return;
|
|
3025
|
+
placePanel(panel, anchor.getBoundingClientRect());
|
|
3026
|
+
};
|
|
3027
|
+
const resizes = new ResizeObserver(place);
|
|
3028
|
+
resizes.observe(panel);
|
|
3029
|
+
const onAnchor = (el, closing = false) => {
|
|
3030
|
+
if (anchor) resizes.unobserve(anchor);
|
|
3031
|
+
anchor = el;
|
|
3032
|
+
if (!el) {
|
|
3033
|
+
const active = document.activeElement;
|
|
3034
|
+
if (!closing && !panel.hidden && active instanceof HTMLElement && panel.contains(active)) {
|
|
3035
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
3036
|
+
if (scroll) {
|
|
3037
|
+
stranded = active;
|
|
3038
|
+
scroll.focus({ preventScroll: true });
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
panel.hidden = true;
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
resizes.observe(el);
|
|
3045
|
+
const wasHidden = panel.hidden;
|
|
3046
|
+
panel.hidden = false;
|
|
3047
|
+
place();
|
|
3048
|
+
if (wasHidden && stranded) {
|
|
3049
|
+
const back = stranded;
|
|
3050
|
+
stranded = null;
|
|
3051
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
3052
|
+
if (panel.contains(back) && document.activeElement === scroll) back.focus({ preventScroll: true });
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
const onKey = (e) => {
|
|
3056
|
+
if (e.key !== "Escape" || panel.hidden || e.defaultPrevented) return;
|
|
3057
|
+
const target = e.target;
|
|
3058
|
+
if (!(target instanceof Node) || !(panel.contains(target) || root.contains(target))) return;
|
|
3059
|
+
e.preventDefault();
|
|
3060
|
+
opts.onDismiss();
|
|
3061
|
+
};
|
|
3062
|
+
const onPress = (e) => {
|
|
3063
|
+
if (panel.hidden || panel.contains(e.target)) return;
|
|
3064
|
+
if (shouldKeepOpenOnAnnotation(e.target)) return;
|
|
3065
|
+
opts.onDismiss();
|
|
3066
|
+
};
|
|
3067
|
+
const onTab = (e) => {
|
|
3068
|
+
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3069
|
+
const tabbable = [...panel.querySelectorAll(TABBABLE$1)].filter((el) => !el.disabled && el.tabIndex >= 0);
|
|
3070
|
+
const first = tabbable[0];
|
|
3071
|
+
const last = tabbable.at(-1);
|
|
3072
|
+
if (!first || !last) return;
|
|
3073
|
+
const active = document.activeElement;
|
|
3074
|
+
if (e.shiftKey ? active !== first : active !== last) return;
|
|
3075
|
+
e.preventDefault();
|
|
3076
|
+
(e.shiftKey ? last : first).focus();
|
|
3077
|
+
};
|
|
3078
|
+
panel.addEventListener("keydown", onTab);
|
|
3079
|
+
document.addEventListener("keydown", onKey);
|
|
3080
|
+
document.addEventListener("pointerdown", onPress, true);
|
|
3081
|
+
document.addEventListener("scroll", place, true);
|
|
3082
|
+
window.addEventListener("resize", place);
|
|
3083
|
+
return {
|
|
3084
|
+
panel,
|
|
3085
|
+
get anchor() {
|
|
3086
|
+
return anchor;
|
|
3087
|
+
},
|
|
3088
|
+
show(id) {
|
|
3089
|
+
if (id === anchored) return;
|
|
3090
|
+
stopObserving();
|
|
3091
|
+
anchored = id;
|
|
3092
|
+
stopObserving = observeAnnotationElement(root, id, onAnchor);
|
|
3093
|
+
},
|
|
3094
|
+
showAt(el) {
|
|
3095
|
+
if (el === anchored) return;
|
|
3096
|
+
stopObserving();
|
|
3097
|
+
anchored = el;
|
|
3098
|
+
stopObserving = observeElement(root, el, onAnchor);
|
|
3099
|
+
},
|
|
3100
|
+
hide() {
|
|
3101
|
+
if (anchored === null) return;
|
|
3102
|
+
stopObserving();
|
|
3103
|
+
stopObserving = () => void 0;
|
|
3104
|
+
const element = typeof anchored === "string" ? null : anchored;
|
|
3105
|
+
anchored = null;
|
|
3106
|
+
const back = anchor;
|
|
3107
|
+
stranded = null;
|
|
3108
|
+
onAnchor(null, true);
|
|
3109
|
+
queueMicrotask(() => {
|
|
3110
|
+
if (anchored !== null || !panel.isConnected) return;
|
|
3111
|
+
if (element?.isConnected) {
|
|
3112
|
+
const active = document.activeElement;
|
|
3113
|
+
if (!(active && active !== root && root.contains(active))) element.focus({ preventScroll: true });
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
focusReturn.onCloseAutoFocus(new Event("closeautofocus", { cancelable: true }), back);
|
|
3117
|
+
});
|
|
3118
|
+
},
|
|
3119
|
+
destroy() {
|
|
3120
|
+
stopObserving();
|
|
3121
|
+
resizes.disconnect();
|
|
3122
|
+
document.removeEventListener("keydown", onKey);
|
|
3123
|
+
document.removeEventListener("pointerdown", onPress, true);
|
|
3124
|
+
panel.removeEventListener("keydown", onTab);
|
|
3125
|
+
document.removeEventListener("scroll", place, true);
|
|
3126
|
+
window.removeEventListener("resize", place);
|
|
3127
|
+
panel.remove();
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
//#endregion
|
|
3132
|
+
//#region src/dom/plugins/api/card.ts
|
|
3133
|
+
function el(tag, className, ...children) {
|
|
3134
|
+
const e = document.createElement(tag);
|
|
3135
|
+
e.className = className;
|
|
3136
|
+
e.append(...children);
|
|
3137
|
+
return e;
|
|
3138
|
+
}
|
|
3139
|
+
function createCard(ctx, records, opts) {
|
|
3140
|
+
/** Made on first open, in the viewer root the mark is in. */
|
|
3141
|
+
let popover = null;
|
|
3142
|
+
let root = null;
|
|
3143
|
+
/** The record on show. */
|
|
3144
|
+
let shown = null;
|
|
3145
|
+
/** Set while an action runs: the action decides what happens after it. */
|
|
3146
|
+
let acting = false;
|
|
3147
|
+
/** The mark whose card is open: its `aria-expanded` is true. */
|
|
3148
|
+
let expanded = null;
|
|
3149
|
+
const expand = (mark) => {
|
|
3150
|
+
expanded?.setAttribute("aria-expanded", "false");
|
|
3151
|
+
expanded = mark;
|
|
3152
|
+
mark?.setAttribute("aria-expanded", "true");
|
|
3153
|
+
};
|
|
3154
|
+
/** The shown record as it stands now, while it still has a mark. */
|
|
3155
|
+
const current = () => {
|
|
3156
|
+
const record = shown === null ? void 0 : records.find(shown);
|
|
3157
|
+
return record && opts.marked(record) ? record : void 0;
|
|
3158
|
+
};
|
|
3159
|
+
const close = () => {
|
|
3160
|
+
shown = null;
|
|
3161
|
+
expand(null);
|
|
3162
|
+
popover?.hide();
|
|
3163
|
+
};
|
|
3164
|
+
/** Runs `fn` as one undo step, then re-renders, or closes and moves focus on. */
|
|
3165
|
+
const act = (fn) => {
|
|
3166
|
+
if (!popover || !root || shown === null) return;
|
|
3167
|
+
const marks = opts.marks(root);
|
|
3168
|
+
const i = popover.anchor ? marks.indexOf(popover.anchor) : -1;
|
|
3169
|
+
const after = i < 0 ? [] : [...marks.slice(i + 1), ...marks.slice(0, i).reverse()];
|
|
3170
|
+
acting = true;
|
|
3171
|
+
try {
|
|
3172
|
+
ctx.history.batch(fn);
|
|
3173
|
+
} finally {
|
|
3174
|
+
acting = false;
|
|
3175
|
+
}
|
|
3176
|
+
const record = current();
|
|
3177
|
+
if (record) return render(record);
|
|
3178
|
+
after.find((m) => m.isConnected)?.focus({ preventScroll: true });
|
|
3179
|
+
close();
|
|
3180
|
+
};
|
|
3181
|
+
/** Fills the panel from `record`, keeping focus on the same action. */
|
|
3182
|
+
const render = (record) => {
|
|
3183
|
+
const spec = opts.spec(record);
|
|
3184
|
+
const panel = popover.panel;
|
|
3185
|
+
const buttons = () => [...panel.querySelectorAll(".ink-card-actions > button")];
|
|
3186
|
+
const focused = buttons().indexOf(document.activeElement);
|
|
3187
|
+
panel.className = spec.className ? `ink-card ${spec.className}` : "ink-card";
|
|
3188
|
+
panel.setAttribute("aria-label", spec.label ?? spec.title);
|
|
3189
|
+
const parts = [el("p", "ink-card-title", spec.title)];
|
|
3190
|
+
if (spec.meta !== void 0) {
|
|
3191
|
+
const meta = el("p", "ink-card-meta", spec.meta);
|
|
3192
|
+
if (spec.swatch !== void 0) {
|
|
3193
|
+
const swatch = el("span", "ink-card-swatch");
|
|
3194
|
+
swatch.style.background = spec.swatch;
|
|
3195
|
+
meta.prepend(swatch);
|
|
3196
|
+
}
|
|
3197
|
+
parts.push(meta);
|
|
3198
|
+
}
|
|
3199
|
+
if (spec.body !== void 0) parts.push(el("p", "ink-card-body", spec.body));
|
|
3200
|
+
const actions = el("div", "ink-card-actions");
|
|
3201
|
+
for (const [label, fn] of Object.entries(spec.actions)) {
|
|
3202
|
+
const button = el("button", "", label);
|
|
3203
|
+
button.type = "button";
|
|
3204
|
+
button.addEventListener("click", () => act(fn));
|
|
3205
|
+
actions.append(button);
|
|
3206
|
+
}
|
|
3207
|
+
parts.push(actions);
|
|
3208
|
+
panel.replaceChildren(...parts);
|
|
3209
|
+
if (focused >= 0) {
|
|
3210
|
+
const now = buttons();
|
|
3211
|
+
(now[focused] ?? now[0])?.focus({ preventScroll: true });
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
ctx.onDocument(() => {
|
|
3215
|
+
const stop = records.list.subscribe(() => {
|
|
3216
|
+
if (acting || shown === null) return;
|
|
3217
|
+
const record = current();
|
|
3218
|
+
if (record) render(record);
|
|
3219
|
+
else close();
|
|
3220
|
+
});
|
|
3221
|
+
return () => {
|
|
3222
|
+
stop();
|
|
3223
|
+
shown = null;
|
|
3224
|
+
expand(null);
|
|
3225
|
+
popover?.destroy();
|
|
3226
|
+
popover = null;
|
|
3227
|
+
root = null;
|
|
3228
|
+
};
|
|
3229
|
+
});
|
|
3230
|
+
return {
|
|
3231
|
+
open(record, mark) {
|
|
3232
|
+
const viewer = mark.closest("[data-ink-root]");
|
|
3233
|
+
if (!viewer) return;
|
|
3234
|
+
if (viewer !== root) {
|
|
3235
|
+
popover?.destroy();
|
|
3236
|
+
root = viewer;
|
|
3237
|
+
popover = createPopover({
|
|
3238
|
+
root: viewer,
|
|
3239
|
+
attrs: { class: "ink-card" },
|
|
3240
|
+
focusReturn: createFocusReturn({
|
|
3241
|
+
root: () => viewer,
|
|
3242
|
+
activated: () => null
|
|
3243
|
+
}),
|
|
3244
|
+
onDismiss: close
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
shown = record.id;
|
|
3248
|
+
render(record);
|
|
3249
|
+
popover.showAt(mark);
|
|
3250
|
+
expand(mark);
|
|
3251
|
+
popover.panel.querySelector(".ink-card-actions > button")?.focus();
|
|
3252
|
+
},
|
|
3253
|
+
expanded: (mark) => mark === expanded
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
//#endregion
|
|
3257
|
+
//#region src/dom/plugins/api/marks.ts
|
|
3258
|
+
/** Events stopped on a mark, so the viewer's root never sees a press on it. */
|
|
3259
|
+
const STOPPED = [
|
|
3260
|
+
"pointerdown",
|
|
3261
|
+
"pointerup",
|
|
3262
|
+
"mousedown",
|
|
3263
|
+
"mouseup",
|
|
3264
|
+
"click"
|
|
3265
|
+
];
|
|
3266
|
+
/** Attributes the plugin's `attrs` cannot set: core sets them, or they have options of their own. */
|
|
3267
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
3268
|
+
"style",
|
|
3269
|
+
"class",
|
|
3270
|
+
"type"
|
|
3271
|
+
]);
|
|
3272
|
+
function createMarks(ctx, records, opts) {
|
|
3273
|
+
const applied = /* @__PURE__ */ new WeakMap();
|
|
3274
|
+
const marked = (record) => opts.where?.(record) ?? true;
|
|
3275
|
+
const spec = opts.card?.bind(opts);
|
|
3276
|
+
const card = spec && createCard(ctx, records, {
|
|
3277
|
+
spec,
|
|
3278
|
+
marked,
|
|
3279
|
+
marks: (root) => [...root.querySelectorAll(`.ink-plugin-layer[data-plugin="${ctx.id}"] > button`)].filter((mark) => applied.has(mark))
|
|
3280
|
+
});
|
|
3281
|
+
/** What activating a mark does. */
|
|
3282
|
+
const activate = (record, mark) => {
|
|
3283
|
+
card?.open(record, mark);
|
|
3284
|
+
opts.onActivate?.(record, mark);
|
|
3285
|
+
};
|
|
3286
|
+
const create = (doc) => {
|
|
3287
|
+
const mark = doc.createElement("button");
|
|
3288
|
+
if (opts.className) mark.className = opts.className;
|
|
3289
|
+
for (const type of STOPPED) mark.addEventListener(type, (e) => e.stopPropagation());
|
|
3290
|
+
mark.addEventListener("click", () => {
|
|
3291
|
+
const state = applied.get(mark);
|
|
3292
|
+
if (state) activate(state.record, mark);
|
|
3293
|
+
});
|
|
3294
|
+
return mark;
|
|
3295
|
+
};
|
|
3296
|
+
/** Brings `mark` up to date with `record`, placed through `transform`. */
|
|
3297
|
+
const apply = (mark, record, transform) => {
|
|
3298
|
+
const prev = applied.get(mark);
|
|
3299
|
+
const style = opts.style?.(record) ?? {};
|
|
3300
|
+
const styleNames = Object.keys(style).map(toCssName);
|
|
3301
|
+
const attrs = Object.fromEntries(Object.entries(opts.attrs?.(record) ?? {}).filter(([name]) => !RESERVED.has(name.toLowerCase())));
|
|
3302
|
+
for (const name of prev?.style ?? []) if (!styleNames.includes(name)) mark.style.removeProperty(name);
|
|
3303
|
+
for (const key of prev?.attrs ?? []) if (!(key in attrs)) mark.removeAttribute(key);
|
|
3304
|
+
for (const [key, value] of Object.entries(style)) mark.style.setProperty(toCssName(key), value);
|
|
3305
|
+
for (const [name, value] of Object.entries(attrs)) mark.setAttribute(name, value);
|
|
3306
|
+
mark.type = "button";
|
|
3307
|
+
mark.style.position = "absolute";
|
|
3308
|
+
mark.dataset.record = record.id;
|
|
3309
|
+
mark.dataset.plugin = ctx.id;
|
|
3310
|
+
mark.setAttribute("aria-label", opts.label(record));
|
|
3311
|
+
if (card) {
|
|
3312
|
+
mark.setAttribute("aria-haspopup", "dialog");
|
|
3313
|
+
mark.setAttribute("aria-expanded", String(card.expanded(mark)));
|
|
3314
|
+
}
|
|
3315
|
+
const r = transform.toCssRect(opts.rect(record));
|
|
3316
|
+
mark.style.left = `${r.left}px`;
|
|
3317
|
+
mark.style.top = `${r.top}px`;
|
|
3318
|
+
mark.style.width = `${r.width}px`;
|
|
3319
|
+
mark.style.height = `${r.height}px`;
|
|
3320
|
+
applied.set(mark, {
|
|
3321
|
+
record,
|
|
3322
|
+
style: styleNames,
|
|
3323
|
+
attrs: Object.keys(attrs)
|
|
3324
|
+
});
|
|
3325
|
+
};
|
|
3326
|
+
/**
|
|
3327
|
+
* Makes `el`'s marks match the page's records, in list order: marks that stay are moved,
|
|
3328
|
+
* new ones inserted, the rest removed.
|
|
3329
|
+
*/
|
|
3330
|
+
const draw = (el, page, transform) => {
|
|
3331
|
+
const list = records.list.get().filter((r) => r.page === page && marked(r));
|
|
3332
|
+
const ids = new Set(list.map((r) => r.id));
|
|
3333
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3334
|
+
for (const mark of [...el.children]) {
|
|
3335
|
+
const id = mark.dataset.record;
|
|
3336
|
+
if (id !== void 0 && ids.has(id) && !kept.has(id)) kept.set(id, mark);
|
|
3337
|
+
else mark.remove();
|
|
3338
|
+
}
|
|
3339
|
+
let next = el.firstElementChild;
|
|
3340
|
+
for (const record of list) {
|
|
3341
|
+
const mark = kept.get(record.id) ?? create(el.ownerDocument);
|
|
3342
|
+
apply(mark, record, transform);
|
|
3343
|
+
if (mark === next) next = mark.nextElementSibling;
|
|
3344
|
+
else el.insertBefore(mark, next);
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
let scrolling = false;
|
|
3348
|
+
/** Ids that started to get a mark in the change under way, read once it settles. */
|
|
3349
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
3350
|
+
/** Scrolls to the first fresh record still marked, so a batch of adds scrolls once. */
|
|
3351
|
+
const reveal = () => {
|
|
3352
|
+
const [first] = [...fresh].map((id) => records.find(id)).filter((r) => r !== void 0 && marked(r)).sort(readingOrder((r) => opts.rect(r)));
|
|
3353
|
+
fresh.clear();
|
|
3354
|
+
if (first) ctx.scrollTo(first.page, opts.rect(first), { behavior: reducedMotion() ? "instant" : "smooth" });
|
|
3355
|
+
};
|
|
3356
|
+
/** Notes a record that starts to get a mark; replaces (undo, redo, reset) never count. */
|
|
3357
|
+
const watch = (change) => {
|
|
3358
|
+
const record = change.kind === "add" ? change.record : change.kind === "update" && !marked(change.before) ? change.after : void 0;
|
|
3359
|
+
if (!record || !marked(record)) return;
|
|
3360
|
+
if (fresh.size === 0) queueMicrotask(reveal);
|
|
3361
|
+
fresh.add(record.id);
|
|
3362
|
+
};
|
|
3363
|
+
const layer = ctx.layer({ render: draw });
|
|
3364
|
+
ctx.onDocument(() => {
|
|
3365
|
+
const stopDraw = records.list.subscribe(() => layer.invalidate());
|
|
3366
|
+
const stopWatch = records.store()?.on("change", (change) => {
|
|
3367
|
+
if (scrolling) watch(change);
|
|
3368
|
+
});
|
|
3369
|
+
return () => {
|
|
3370
|
+
stopDraw();
|
|
3371
|
+
stopWatch?.();
|
|
3372
|
+
fresh.clear();
|
|
3373
|
+
};
|
|
3374
|
+
});
|
|
3375
|
+
const handle = {
|
|
3376
|
+
refresh: () => layer.invalidate(),
|
|
3377
|
+
scrollToFirst() {
|
|
3378
|
+
scrolling = true;
|
|
3379
|
+
return handle;
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
return handle;
|
|
3383
|
+
}
|
|
3384
|
+
/** Page first, then top to bottom and left to right, by each record's rect in user space. */
|
|
3385
|
+
function readingOrder(rect) {
|
|
3386
|
+
return (a, b) => {
|
|
3387
|
+
const ra = rect(a);
|
|
3388
|
+
const rb = rect(b);
|
|
3389
|
+
return a.page - b.page || rb.y + rb.height - (ra.y + ra.height) || ra.x - rb.x;
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
function reducedMotion() {
|
|
3393
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
3394
|
+
}
|
|
3395
|
+
/** `borderColor` → `border-color`, as `setProperty` takes it; `--custom` names stay as given. */
|
|
3396
|
+
function toCssName(key) {
|
|
3397
|
+
return key.startsWith("--") ? key : key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
3398
|
+
}
|
|
3399
|
+
//#endregion
|
|
3400
|
+
//#region src/dom/plugins/api/links.ts
|
|
3401
|
+
function isObject(value) {
|
|
3402
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3403
|
+
}
|
|
3404
|
+
/** Links `type` annotations to records; registers one `onAnnotationEdit` listener. */
|
|
3405
|
+
function createLink(deps, type, spec) {
|
|
3406
|
+
const linkOf = (a) => {
|
|
3407
|
+
if (a.type !== type) return void 0;
|
|
3408
|
+
const own = a.custom?.[deps.plugin];
|
|
3409
|
+
const id = isObject(own) ? own[deps.name] : void 0;
|
|
3410
|
+
return typeof id === "string" ? id : void 0;
|
|
3411
|
+
};
|
|
3412
|
+
deps.onAnnotationEdit((edit) => {
|
|
3413
|
+
if (edit.kind === "add") return;
|
|
3414
|
+
const annotation = edit.kind === "remove" ? edit.annotation : edit.after;
|
|
3415
|
+
const id = linkOf(annotation);
|
|
3416
|
+
const record = id === void 0 ? void 0 : deps.find(id);
|
|
3417
|
+
if (!record) return;
|
|
3418
|
+
if (edit.kind === "remove") spec.onRemoved?.(record, annotation);
|
|
3419
|
+
else spec.onChanged?.(record, annotation);
|
|
3420
|
+
});
|
|
3421
|
+
return {
|
|
3422
|
+
add(record) {
|
|
3423
|
+
const doc = deps.document();
|
|
3424
|
+
if (!doc) return void 0;
|
|
3425
|
+
const { color, custom, ...fields } = spec.from(record);
|
|
3426
|
+
const own = custom?.[deps.plugin];
|
|
3427
|
+
return doc.annotations.add({
|
|
3428
|
+
...fields,
|
|
3429
|
+
type,
|
|
3430
|
+
color: color ?? doc.annotations.defaults.color(type),
|
|
3431
|
+
custom: {
|
|
3432
|
+
...custom,
|
|
3433
|
+
[deps.plugin]: {
|
|
3434
|
+
...isObject(own) ? own : {},
|
|
3435
|
+
[deps.name]: record.id
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
},
|
|
3440
|
+
of(record) {
|
|
3441
|
+
const doc = deps.document();
|
|
3442
|
+
return doc ? doc.annotations.list.get().filter((a) => linkOf(a) === record.id) : [];
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3447
|
+
//#region src/dom/plugins/api/records.ts
|
|
3448
|
+
const EMPTY$1 = Object.freeze([]);
|
|
3449
|
+
/** The list of whichever store `source` holds, `[]` for none; emits on swaps and changes. */
|
|
3450
|
+
function listOf(source) {
|
|
3451
|
+
const read = (store) => store?.list.get() ?? EMPTY$1;
|
|
3452
|
+
return {
|
|
3453
|
+
get: () => read(source.get()),
|
|
3454
|
+
subscribe(fn) {
|
|
3455
|
+
let last = read(source.get());
|
|
3456
|
+
let stopList;
|
|
3457
|
+
const emit = (next) => {
|
|
3458
|
+
if (Object.is(next, last) || next.length === 0 && last.length === 0) return;
|
|
3459
|
+
last = next;
|
|
3460
|
+
fn(next);
|
|
3461
|
+
};
|
|
3462
|
+
const follow = (store) => {
|
|
3463
|
+
stopList?.();
|
|
3464
|
+
stopList = store?.list.subscribe(emit);
|
|
3465
|
+
emit(read(store));
|
|
3466
|
+
};
|
|
3467
|
+
follow(source.get());
|
|
3468
|
+
const stopSource = source.subscribe(follow);
|
|
3469
|
+
return () => {
|
|
3470
|
+
stopSource();
|
|
3471
|
+
stopList?.();
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
function matches(record, match) {
|
|
3477
|
+
return Object.entries(match).every(([k, v]) => record[k] === v);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Builds a handle over `source`, a plugin store from `ctx.store`. Every call reads the store
|
|
3481
|
+
* `source` holds then.
|
|
3482
|
+
*/
|
|
3483
|
+
function createRecords(deps, source, base = source) {
|
|
3484
|
+
const current = () => source.get();
|
|
3485
|
+
const all = () => current()?.list.get() ?? EMPTY$1;
|
|
3486
|
+
return {
|
|
3487
|
+
list: listOf(source),
|
|
3488
|
+
all,
|
|
3489
|
+
find: (id) => current()?.get(id),
|
|
3490
|
+
has: (id) => current()?.get(id) !== void 0,
|
|
3491
|
+
where: (match) => all().filter(typeof match === "function" ? match : (r) => matches(r, match)),
|
|
3492
|
+
add: (record) => current()?.add(record),
|
|
3493
|
+
addMissing(records) {
|
|
3494
|
+
const store = current();
|
|
3495
|
+
if (!store) return [];
|
|
3496
|
+
const added = [];
|
|
3497
|
+
deps.batch(() => {
|
|
3498
|
+
for (const record of records) if (!store.get(record.id)) added.push(store.add(record));
|
|
3499
|
+
});
|
|
3500
|
+
return added;
|
|
3501
|
+
},
|
|
3502
|
+
update: (id, patch) => current()?.update(id, patch),
|
|
3503
|
+
remove: (id) => current()?.remove(id),
|
|
3504
|
+
in(doc) {
|
|
3505
|
+
const captured = deps.storesDocument() === doc ? base.get() : null;
|
|
3506
|
+
return createRecords(deps, derived(base, (store) => store && store === captured ? store : null), base);
|
|
3507
|
+
},
|
|
3508
|
+
store: current,
|
|
3509
|
+
annotates: (type, spec) => createLink({
|
|
3510
|
+
plugin: deps.plugin,
|
|
3511
|
+
name: deps.name,
|
|
3512
|
+
onAnnotationEdit: (fn) => deps.onAnnotationEdit(fn),
|
|
3513
|
+
document: () => current() ? deps.storesDocument() : null,
|
|
3514
|
+
find: (id) => current()?.get(id)
|
|
3515
|
+
}, type, spec)
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
//#endregion
|
|
3519
|
+
//#region src/dom/plugins/api/api.ts
|
|
3520
|
+
function createPluginApi(ctx, options) {
|
|
3521
|
+
let storesDocument = null;
|
|
3522
|
+
ctx.onDocument((doc) => {
|
|
3523
|
+
storesDocument = doc;
|
|
3524
|
+
return () => {
|
|
3525
|
+
storesDocument = null;
|
|
3526
|
+
};
|
|
3527
|
+
});
|
|
3528
|
+
const batch = (fn) => ctx.history.batch(fn);
|
|
3529
|
+
return {
|
|
3530
|
+
...ctx,
|
|
3531
|
+
records(name, opts) {
|
|
3532
|
+
const source = ctx.store(name, opts ?? options.records?.[name]);
|
|
3533
|
+
return createRecords({
|
|
3534
|
+
batch,
|
|
3535
|
+
storesDocument: () => storesDocument,
|
|
3536
|
+
plugin: ctx.id,
|
|
3537
|
+
name,
|
|
3538
|
+
onAnnotationEdit: (fn) => ctx.onAnnotationEdit(fn)
|
|
3539
|
+
}, source);
|
|
3540
|
+
},
|
|
3541
|
+
batch,
|
|
3542
|
+
command: (id, label, run, opts) => createCommand(ctx, id, label, run, opts),
|
|
3543
|
+
marks: (records, opts) => createMarks(ctx, records, opts)
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
//#endregion
|
|
3547
|
+
//#region src/dom/plugins/define.ts
|
|
3548
|
+
const PLUGIN_ID = /^[a-z][a-z0-9-]*$/;
|
|
3549
|
+
function checkId(id) {
|
|
3550
|
+
if (!PLUGIN_ID.test(id)) throw new InkError("InvalidPluginId", `Plugin id ${JSON.stringify(id)} must be lower-case kebab`, { id });
|
|
3551
|
+
}
|
|
3552
|
+
function definePlugin(plugin, setup) {
|
|
3553
|
+
if (typeof plugin !== "string") {
|
|
3554
|
+
checkId(plugin.id);
|
|
3555
|
+
return plugin;
|
|
3556
|
+
}
|
|
3557
|
+
const id = plugin;
|
|
3558
|
+
checkId(id);
|
|
3559
|
+
return (options = {}) => ({
|
|
3560
|
+
id,
|
|
3561
|
+
setup: (ctx) => setup?.(createPluginApi(ctx, options), options)
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
//#endregion
|
|
3565
|
+
//#region src/dom/plugins/commands.ts
|
|
3566
|
+
/**
|
|
3567
|
+
* The error for a command id no plugin registered, naming the ids that were. Bindings log it
|
|
3568
|
+
* rather than throw, and show the control as it is before the plugins are set up.
|
|
3569
|
+
*/
|
|
3570
|
+
function unknownCommand(id, registered) {
|
|
3571
|
+
const known = registered.length > 0 ? registered.join(", ") : "none";
|
|
3572
|
+
return new InkError("UnknownCommand", `No plugin registered a command ${id} (registered: ${known})`, {
|
|
3573
|
+
id,
|
|
3574
|
+
registered
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
const EMPTY = {
|
|
3578
|
+
get: () => "",
|
|
3579
|
+
subscribe: () => () => {}
|
|
3580
|
+
};
|
|
3581
|
+
/**
|
|
3582
|
+
* A boolean readable computed from `sources`. Like `derived`, it subscribes to the sources only
|
|
3583
|
+
* while it has subscribers of its own; `watch` re-reads them, so a source can be swapped while
|
|
3584
|
+
* subscribed (a document's status when the document changes).
|
|
3585
|
+
*/
|
|
3586
|
+
function computed(compute, watch) {
|
|
3587
|
+
const subs = /* @__PURE__ */ new Set();
|
|
3588
|
+
let cached = false;
|
|
3589
|
+
let stop;
|
|
3590
|
+
const changed = () => {
|
|
3591
|
+
const next = compute();
|
|
3592
|
+
if (next === cached) return;
|
|
3593
|
+
cached = next;
|
|
3594
|
+
for (const sub of [...subs]) sub(next);
|
|
3595
|
+
};
|
|
3596
|
+
return {
|
|
3597
|
+
get: () => stop ? cached : compute(),
|
|
3598
|
+
subscribe(sub) {
|
|
3599
|
+
if (!stop) {
|
|
3600
|
+
cached = compute();
|
|
3601
|
+
stop = watch(changed);
|
|
3602
|
+
}
|
|
3603
|
+
subs.add(sub);
|
|
3604
|
+
return () => {
|
|
3605
|
+
if (!subs.delete(sub) || subs.size > 0) return;
|
|
3606
|
+
stop?.();
|
|
3607
|
+
stop = void 0;
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
/** True while `document` holds a document whose status is ready. */
|
|
3613
|
+
function documentReady(document) {
|
|
3614
|
+
return computed(() => document.get()?.status.get().state === "ready", (changed) => {
|
|
3615
|
+
let stopStatus;
|
|
3616
|
+
const follow = (doc) => {
|
|
3617
|
+
stopStatus?.();
|
|
3618
|
+
stopStatus = doc?.status.subscribe(changed);
|
|
3619
|
+
changed();
|
|
3620
|
+
};
|
|
3621
|
+
const stopDocument = document.subscribe(follow);
|
|
3622
|
+
follow(document.get());
|
|
3623
|
+
return () => {
|
|
3624
|
+
stopDocument();
|
|
3625
|
+
stopStatus?.();
|
|
3626
|
+
};
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Plugin commands by namespaced id. A command is busy from `run()` until the promise its spec
|
|
3631
|
+
* returned settles, and is disabled meanwhile, so a double click runs it once.
|
|
3632
|
+
*/
|
|
3633
|
+
function createCommandRegistry(opts) {
|
|
3634
|
+
const commands = /* @__PURE__ */ new Map();
|
|
3635
|
+
const ready = documentReady(opts.document);
|
|
3636
|
+
return {
|
|
3637
|
+
commands,
|
|
3638
|
+
add(plugin, spec) {
|
|
3639
|
+
const id = `${plugin}.${spec.id}`;
|
|
3640
|
+
if (commands.has(id)) throw new InkError("DuplicateId", `A command with id ${id} already exists`, { id });
|
|
3641
|
+
if (!spec.label.trim()) throw new InkError("InvalidCommandLabel", `Command ${id} needs a label`, { id });
|
|
3642
|
+
const busy = writable(false);
|
|
3643
|
+
const allowed = spec.enabled ?? ready;
|
|
3644
|
+
const enabled = computed(() => allowed.get() && !busy.get(), (changed) => {
|
|
3645
|
+
const stops = [allowed.subscribe(changed), busy.subscribe(changed)];
|
|
3646
|
+
return () => {
|
|
3647
|
+
for (const stop of stops) stop();
|
|
3648
|
+
};
|
|
3649
|
+
});
|
|
3650
|
+
const command = {
|
|
3651
|
+
id,
|
|
3652
|
+
label: spec.label,
|
|
3653
|
+
icon: spec.icon,
|
|
3654
|
+
enabled,
|
|
3655
|
+
busy: readonly(busy),
|
|
3656
|
+
status: spec.status ?? EMPTY,
|
|
3657
|
+
async run() {
|
|
3658
|
+
if (!enabled.get()) return;
|
|
3659
|
+
busy.set(true);
|
|
3660
|
+
try {
|
|
3661
|
+
await spec.run();
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
if (!(isInkError(error) && error.code === "DocumentDestroyed")) console.error(error);
|
|
3664
|
+
} finally {
|
|
3665
|
+
busy.set(false);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
commands.set(id, command);
|
|
3670
|
+
return command;
|
|
3671
|
+
}
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
//#endregion
|
|
3675
|
+
//#region src/dom/plugins/layer.ts
|
|
3676
|
+
function createLayerRegistry() {
|
|
3677
|
+
const layers = [];
|
|
3678
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3679
|
+
const emit = (event) => {
|
|
3680
|
+
for (const listener of [...listeners]) listener(event);
|
|
3681
|
+
};
|
|
3682
|
+
return {
|
|
3683
|
+
layers,
|
|
3684
|
+
watchLayers(listener) {
|
|
3685
|
+
listeners.add(listener);
|
|
3686
|
+
return () => listeners.delete(listener);
|
|
3687
|
+
},
|
|
3688
|
+
add(layer) {
|
|
3689
|
+
layers.push(layer);
|
|
3690
|
+
emit({
|
|
3691
|
+
type: "add",
|
|
3692
|
+
layer
|
|
3693
|
+
});
|
|
3694
|
+
return { invalidate: (page) => emit({
|
|
3695
|
+
type: "invalidate",
|
|
3696
|
+
layer,
|
|
3697
|
+
page
|
|
3698
|
+
}) };
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
/** Runs a plugin's drawing; a throw is logged so one plugin cannot break the page. */
|
|
3703
|
+
function guard$1(fn) {
|
|
3704
|
+
try {
|
|
3705
|
+
fn();
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
console.error(error);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
const CHILD_RULE = ":where(.ink-plugin-layer > *) { pointer-events: auto; }";
|
|
3711
|
+
/** One sheet per window: a constructed sheet can only be adopted in its own window. */
|
|
3712
|
+
const childSheets = /* @__PURE__ */ new WeakMap();
|
|
3713
|
+
/**
|
|
3714
|
+
* Puts the children's `pointer-events: auto` rule in the document or shadow root `el` lives
|
|
3715
|
+
* in: an adopted sheet, again if the host has since replaced its adopted sheets, or one
|
|
3716
|
+
* `<style>` element where adopted sheets are missing. A layer not yet connected gets it on a
|
|
3717
|
+
* later draw, which calls this again.
|
|
3718
|
+
*/
|
|
3719
|
+
function adoptChildRule(el) {
|
|
3720
|
+
if (!el.isConnected) return;
|
|
3721
|
+
const root = el.getRootNode();
|
|
3722
|
+
const doc = el.ownerDocument;
|
|
3723
|
+
const win = doc.defaultView;
|
|
3724
|
+
if (!root.adoptedStyleSheets || !win) {
|
|
3725
|
+
const parent = root === doc ? doc.head ?? doc.documentElement : root;
|
|
3726
|
+
if ([...parent.children].some((c) => c.matches("style[data-ink-plugin-layer]"))) return;
|
|
3727
|
+
const style = doc.createElement("style");
|
|
3728
|
+
style.dataset.inkPluginLayer = "";
|
|
3729
|
+
style.textContent = CHILD_RULE;
|
|
3730
|
+
parent.append(style);
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
let sheet = childSheets.get(win);
|
|
3734
|
+
if (!sheet) {
|
|
3735
|
+
sheet = new win.CSSStyleSheet();
|
|
3736
|
+
sheet.replaceSync(CHILD_RULE);
|
|
3737
|
+
childSheets.set(win, sheet);
|
|
3738
|
+
}
|
|
3739
|
+
if (!root.adoptedStyleSheets.includes(sheet)) root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Mounts `source`'s layers on the pages the engine mounts, and follows it: a layer added later
|
|
3743
|
+
* is drawn on every mounted page, and an invalidate redraws its page, or all of them.
|
|
3744
|
+
*
|
|
3745
|
+
* @internal For bindings, which mount plugin layers; plugin authors add a layer with
|
|
3746
|
+
* `ctx.layer`.
|
|
3747
|
+
*/
|
|
3748
|
+
function createPluginLayers(source) {
|
|
3749
|
+
const pages = /* @__PURE__ */ new Map();
|
|
3750
|
+
const draw = (layer, el, page, t) => {
|
|
3751
|
+
adoptChildRule(el);
|
|
3752
|
+
guard$1(() => layer.render(el, page, t));
|
|
3753
|
+
};
|
|
3754
|
+
const add = (layer, page, state) => {
|
|
3755
|
+
const el = state.host.ownerDocument.createElement("div");
|
|
3756
|
+
el.className = "ink-plugin-layer";
|
|
3757
|
+
el.dataset.plugin = layer.plugin;
|
|
3758
|
+
el.style.cssText = "position:absolute;inset:0;pointer-events:none;";
|
|
3759
|
+
state.host.append(el);
|
|
3760
|
+
state.elements.set(layer, el);
|
|
3761
|
+
draw(layer, el, page, state.transform);
|
|
3762
|
+
};
|
|
3763
|
+
return {
|
|
3764
|
+
mount(page, host, transform) {
|
|
3765
|
+
const state = {
|
|
3766
|
+
host,
|
|
3767
|
+
transform,
|
|
3768
|
+
elements: /* @__PURE__ */ new Map()
|
|
3769
|
+
};
|
|
3770
|
+
pages.set(page, state);
|
|
3771
|
+
for (const layer of source.layers) add(layer, page, state);
|
|
3772
|
+
return {
|
|
3773
|
+
update(next) {
|
|
3774
|
+
state.transform = next;
|
|
3775
|
+
for (const [layer, el] of state.elements) draw(layer, el, page, next);
|
|
3776
|
+
},
|
|
3777
|
+
destroy() {
|
|
3778
|
+
if (pages.get(page) === state) pages.delete(page);
|
|
3779
|
+
for (const [layer, el] of state.elements) {
|
|
3780
|
+
if (layer.destroy) guard$1(() => layer.destroy?.(el, page));
|
|
3781
|
+
el.remove();
|
|
3782
|
+
}
|
|
3783
|
+
state.elements.clear();
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
},
|
|
3787
|
+
destroy: source.watchLayers((event) => {
|
|
3788
|
+
if (event.type === "add") {
|
|
3789
|
+
for (const [page, state] of pages) add(event.layer, page, state);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const redraw = (page, state) => {
|
|
3793
|
+
const el = state.elements.get(event.layer);
|
|
3794
|
+
if (el) draw(event.layer, el, page, state.transform);
|
|
3795
|
+
};
|
|
3796
|
+
if (event.page === void 0) for (const [page, state] of pages) redraw(page, state);
|
|
3797
|
+
else {
|
|
3798
|
+
const state = pages.get(event.page);
|
|
3799
|
+
if (state) redraw(event.page, state);
|
|
3800
|
+
}
|
|
3801
|
+
})
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
//#endregion
|
|
3805
|
+
//#region src/dom/plugins/host.ts
|
|
3806
|
+
/**
|
|
3807
|
+
* `value` seen with `extra` laid over it, read-only. Everything else is read from and written to
|
|
3808
|
+
* `value` itself, and its methods are bound to it, so a method always runs on the original
|
|
3809
|
+
* (a class instance keeps its `#private` fields, and `this.id` is the plugin's own id).
|
|
3810
|
+
*/
|
|
3811
|
+
function withProps(value, extra) {
|
|
3812
|
+
const bound = /* @__PURE__ */ new Map();
|
|
3813
|
+
const own = (key) => Object.hasOwn(extra, key);
|
|
3814
|
+
return new Proxy(Object.create(null), {
|
|
3815
|
+
get(_, key) {
|
|
3816
|
+
if (own(key)) return extra[key];
|
|
3817
|
+
const v = Reflect.get(value, key, value);
|
|
3818
|
+
if (typeof v !== "function") return v;
|
|
3819
|
+
let entry = bound.get(key);
|
|
3820
|
+
if (entry?.fn !== v) {
|
|
3821
|
+
entry = {
|
|
3822
|
+
fn: v,
|
|
3823
|
+
bound: v.bind(value)
|
|
3824
|
+
};
|
|
3825
|
+
bound.set(key, entry);
|
|
3826
|
+
}
|
|
3827
|
+
return entry.bound;
|
|
3828
|
+
},
|
|
3829
|
+
set: (_, key, v) => !own(key) && Reflect.set(value, key, v, value),
|
|
3830
|
+
defineProperty(target, key, d) {
|
|
3831
|
+
if (own(key) || !Reflect.defineProperty(value, key, d)) return false;
|
|
3832
|
+
const defined = Reflect.getOwnPropertyDescriptor(value, key);
|
|
3833
|
+
if (defined && !defined.configurable) Reflect.defineProperty(target, key, defined);
|
|
3834
|
+
return true;
|
|
3835
|
+
},
|
|
3836
|
+
deleteProperty: (_, key) => !own(key) && Reflect.deleteProperty(value, key),
|
|
3837
|
+
has: (_, key) => own(key) || Reflect.has(value, key),
|
|
3838
|
+
ownKeys: () => [.../* @__PURE__ */ new Set([...Reflect.ownKeys(value), ...Reflect.ownKeys(extra)])],
|
|
3839
|
+
getOwnPropertyDescriptor(target, key) {
|
|
3840
|
+
const d = Reflect.getOwnPropertyDescriptor(own(key) ? extra : value, key);
|
|
3841
|
+
const fixed = Reflect.getOwnPropertyDescriptor(target, key)?.configurable === false;
|
|
3842
|
+
return d && {
|
|
3843
|
+
...d,
|
|
3844
|
+
configurable: !fixed
|
|
3845
|
+
};
|
|
3846
|
+
},
|
|
3847
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(value)
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
/** Runs a plugin callback; a throw is logged so one plugin cannot break the others. */
|
|
3851
|
+
function guard(fn) {
|
|
3852
|
+
try {
|
|
3853
|
+
return fn();
|
|
3854
|
+
} catch (error) {
|
|
3855
|
+
console.error(error);
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* @internal For bindings and `createViewer`, which place a viewer's plugins; plugin authors use
|
|
3861
|
+
* `definePlugin` and never call it.
|
|
3862
|
+
*/
|
|
3863
|
+
function createPluginHost(opts) {
|
|
3864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3865
|
+
for (const plugin of opts.plugins) {
|
|
3866
|
+
if (seen.has(plugin.id)) throw new InkError("DuplicatePlugin", `A plugin with id ${plugin.id} is registered twice`, { id: plugin.id });
|
|
3867
|
+
seen.add(plugin.id);
|
|
3868
|
+
}
|
|
3869
|
+
const registry = createCommandRegistry({ document: opts.document });
|
|
3870
|
+
const tools = [];
|
|
3871
|
+
const decorators = [];
|
|
3872
|
+
const layers = createLayerRegistry();
|
|
3873
|
+
const requests = [];
|
|
3874
|
+
const documentFns = [];
|
|
3875
|
+
const editFns = [];
|
|
3876
|
+
const setupCleanups = [];
|
|
3877
|
+
const redecorators = /* @__PURE__ */ new Set();
|
|
3878
|
+
let opened = null;
|
|
3879
|
+
let destroyed = false;
|
|
3880
|
+
/** Creates a requested store for the open document and has its history track it. */
|
|
3881
|
+
const openStore = (request, state, history) => {
|
|
3882
|
+
const store = createRecordStore(request.key, {
|
|
3883
|
+
initial: request.opts.initial?.(state.doc) ?? [],
|
|
3884
|
+
...request.opts.onChange && { onChange: request.opts.onChange }
|
|
3885
|
+
});
|
|
3886
|
+
state.stores.push(store);
|
|
3887
|
+
state.detach.push(history.track(store));
|
|
3888
|
+
request.current.set(store);
|
|
3889
|
+
};
|
|
3890
|
+
const runOnDocument = (fn, state) => {
|
|
3891
|
+
const cleanup = guard(() => fn(state.doc));
|
|
3892
|
+
if (typeof cleanup === "function") state.cleanups.push(cleanup);
|
|
3893
|
+
};
|
|
3894
|
+
const open = (doc) => {
|
|
3895
|
+
const history = opts.history();
|
|
3896
|
+
const state = {
|
|
3897
|
+
doc,
|
|
3898
|
+
stores: [],
|
|
3899
|
+
detach: [],
|
|
3900
|
+
cleanups: []
|
|
3901
|
+
};
|
|
3902
|
+
opened = state;
|
|
3903
|
+
for (const request of requests) openStore(request, state, history);
|
|
3904
|
+
for (const fn of editFns) state.detach.push(history.react(fn));
|
|
3905
|
+
for (const fn of documentFns) runOnDocument(fn, state);
|
|
3906
|
+
};
|
|
3907
|
+
const close = () => {
|
|
3908
|
+
const state = opened;
|
|
3909
|
+
if (!state) return;
|
|
3910
|
+
opened = null;
|
|
3911
|
+
for (const detach of state.detach) detach();
|
|
3912
|
+
for (const store of state.stores) store.destroy();
|
|
3913
|
+
for (const request of requests) request.current.set(null);
|
|
3914
|
+
for (const cleanup of state.cleanups.reverse()) guard(cleanup);
|
|
3915
|
+
};
|
|
3916
|
+
const contextFor = (plugin) => {
|
|
3917
|
+
const storeNames = /* @__PURE__ */ new Set();
|
|
3918
|
+
return {
|
|
3919
|
+
id: plugin.id,
|
|
3920
|
+
document: opts.document,
|
|
3921
|
+
history: { batch: (fn) => opts.history().batch(fn) },
|
|
3922
|
+
activated: opts.activated,
|
|
3923
|
+
setActivated: (id) => opts.setActivated(id),
|
|
3924
|
+
root: opts.root,
|
|
3925
|
+
commands: { add: (spec) => registry.add(plugin.id, spec) },
|
|
3926
|
+
tools: { add(tool) {
|
|
3927
|
+
const id = `${plugin.id}.${tool.id}`;
|
|
3928
|
+
if (tools.some((t) => t.id === id)) throw new InkError("DuplicateId", `A tool with id ${id} already exists`, { id });
|
|
3929
|
+
tools.push(withProps(tool, { id }));
|
|
3930
|
+
} },
|
|
3931
|
+
decorate(fn) {
|
|
3932
|
+
decorators.push(fn);
|
|
3933
|
+
},
|
|
3934
|
+
redecorate() {
|
|
3935
|
+
for (const listener of [...redecorators]) listener();
|
|
3936
|
+
},
|
|
3937
|
+
store(name, storeOpts = {}) {
|
|
3938
|
+
const key = `${plugin.id}.${name}`;
|
|
3939
|
+
if (storeNames.has(name)) throw new InkError("DuplicateId", `A store with key ${key} already exists`, { key });
|
|
3940
|
+
storeNames.add(name);
|
|
3941
|
+
const request = {
|
|
3942
|
+
key,
|
|
3943
|
+
opts: storeOpts,
|
|
3944
|
+
current: writable(null)
|
|
3945
|
+
};
|
|
3946
|
+
requests.push(request);
|
|
3947
|
+
if (opened) openStore(request, opened, opts.history());
|
|
3948
|
+
return readonly(request.current);
|
|
3949
|
+
},
|
|
3950
|
+
onDocument(fn) {
|
|
3951
|
+
documentFns.push(fn);
|
|
3952
|
+
if (opened) runOnDocument(fn, opened);
|
|
3953
|
+
},
|
|
3954
|
+
onAnnotationEdit(fn) {
|
|
3955
|
+
editFns.push(fn);
|
|
3956
|
+
if (opened) opened.detach.push(opts.history().react(fn));
|
|
3957
|
+
},
|
|
3958
|
+
layer(spec) {
|
|
3959
|
+
return layers.add(withProps(spec, { plugin: plugin.id }));
|
|
3960
|
+
},
|
|
3961
|
+
scrollTo(page, rect, scrollOpts) {
|
|
3962
|
+
opts.viewport?.()?.scrollToRect(page, rect, scrollOpts);
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
};
|
|
3966
|
+
for (const plugin of opts.plugins) {
|
|
3967
|
+
let cleanup;
|
|
3968
|
+
try {
|
|
3969
|
+
cleanup = plugin.setup(contextFor(plugin));
|
|
3970
|
+
} catch (error) {
|
|
3971
|
+
for (const done of setupCleanups.reverse()) guard(done);
|
|
3972
|
+
throw error;
|
|
3973
|
+
}
|
|
3974
|
+
if (typeof cleanup === "function") setupCleanups.push(cleanup);
|
|
3975
|
+
}
|
|
3976
|
+
const current = opts.document.get();
|
|
3977
|
+
if (current) open(current);
|
|
3978
|
+
const stopDocument = opts.document.subscribe((doc) => {
|
|
3979
|
+
close();
|
|
3980
|
+
if (doc) open(doc);
|
|
3981
|
+
});
|
|
3982
|
+
return {
|
|
3983
|
+
commands: registry.commands,
|
|
3984
|
+
command: (id) => registry.commands.get(id),
|
|
3985
|
+
tools,
|
|
3986
|
+
decorate(annotation) {
|
|
3987
|
+
const all = [opts.decorate?.(), ...decorators];
|
|
3988
|
+
let merged;
|
|
3989
|
+
for (const fn of all) {
|
|
3990
|
+
const d = fn && guard(() => fn(annotation));
|
|
3991
|
+
if (!d) continue;
|
|
3992
|
+
merged ??= {};
|
|
3993
|
+
if (d.data) merged = {
|
|
3994
|
+
...merged,
|
|
3995
|
+
data: {
|
|
3996
|
+
...d.data,
|
|
3997
|
+
...merged.data
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
if (d.classes) merged = {
|
|
4001
|
+
...merged,
|
|
4002
|
+
classes: [...merged.classes ?? [], ...d.classes]
|
|
4003
|
+
};
|
|
4004
|
+
if (merged.description === void 0 && d.description !== void 0) merged = {
|
|
4005
|
+
...merged,
|
|
4006
|
+
description: d.description
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
return merged;
|
|
4010
|
+
},
|
|
4011
|
+
layers: layers.layers,
|
|
4012
|
+
watchLayers: (listener) => layers.watchLayers(listener),
|
|
4013
|
+
watchRedecorate(listener) {
|
|
4014
|
+
const run = () => listener();
|
|
4015
|
+
redecorators.add(run);
|
|
4016
|
+
return () => {
|
|
4017
|
+
redecorators.delete(run);
|
|
4018
|
+
};
|
|
4019
|
+
},
|
|
4020
|
+
destroy() {
|
|
4021
|
+
if (destroyed) return;
|
|
4022
|
+
destroyed = true;
|
|
4023
|
+
stopDocument();
|
|
4024
|
+
close();
|
|
4025
|
+
for (const cleanup of setupCleanups.reverse()) guard(cleanup);
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
//#endregion
|
|
4030
|
+
//#region src/dom/page-mount.ts
|
|
4031
|
+
/**
|
|
4032
|
+
* Canvas, text layer, annotation overlay and any plugin layers for one page, stacked in that
|
|
4033
|
+
* order.
|
|
4034
|
+
*/
|
|
4035
|
+
function createPageMount(opts) {
|
|
4036
|
+
const { document, page, host, transform } = opts;
|
|
4037
|
+
const callbacks = (layer) => ({
|
|
4038
|
+
...opts.onError ? { onError: (error) => opts.onError?.(error, layer) } : {},
|
|
4039
|
+
...opts.onRendered ? { onRendered: () => opts.onRendered?.(layer) } : {}
|
|
4040
|
+
});
|
|
4041
|
+
const renderer = createPageRenderer({
|
|
4042
|
+
document,
|
|
4043
|
+
page,
|
|
4044
|
+
host,
|
|
4045
|
+
transform,
|
|
4046
|
+
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
4047
|
+
...callbacks("canvas")
|
|
4048
|
+
});
|
|
4049
|
+
const textLayer = createTextLayer({
|
|
4050
|
+
document,
|
|
4051
|
+
page,
|
|
4052
|
+
host,
|
|
4053
|
+
transform,
|
|
4054
|
+
...callbacks("text")
|
|
4055
|
+
});
|
|
4056
|
+
const overlay = createOverlay({
|
|
4057
|
+
host,
|
|
4058
|
+
page,
|
|
4059
|
+
store: opts.store,
|
|
4060
|
+
transform,
|
|
4061
|
+
document,
|
|
4062
|
+
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
4063
|
+
...opts.onActivate ? { onActivate: (id) => opts.onActivate?.(id) } : {},
|
|
4064
|
+
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {},
|
|
4065
|
+
...opts.decorate ? { decorate: opts.decorate } : {}
|
|
4066
|
+
});
|
|
4067
|
+
const layers = opts.layers?.mount(page, host, transform);
|
|
4068
|
+
return {
|
|
4069
|
+
renderer,
|
|
4070
|
+
textLayer,
|
|
4071
|
+
overlay,
|
|
4072
|
+
update(next) {
|
|
4073
|
+
renderer.update(next);
|
|
4074
|
+
textLayer.update(next);
|
|
4075
|
+
overlay.update(next);
|
|
4076
|
+
layers?.update(next);
|
|
4077
|
+
},
|
|
4078
|
+
destroy() {
|
|
4079
|
+
layers?.destroy();
|
|
4080
|
+
overlay.destroy();
|
|
4081
|
+
textLayer.destroy();
|
|
4082
|
+
renderer.destroy();
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
//#endregion
|
|
4087
|
+
//#region src/dom/tools/options.ts
|
|
4088
|
+
function createToolOptions(tools = []) {
|
|
4089
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4090
|
+
const views = /* @__PURE__ */ new Map();
|
|
4091
|
+
const entry = (id) => {
|
|
4092
|
+
const w = byId.get(id);
|
|
4093
|
+
if (!w) throw new RangeError(`Unknown tool ${id}`);
|
|
4094
|
+
return w;
|
|
4095
|
+
};
|
|
4096
|
+
function register(list) {
|
|
4097
|
+
for (const t of list) {
|
|
4098
|
+
if (byId.has(t.id)) continue;
|
|
4099
|
+
const w = writable({ ...t.defaultOptions ?? {} });
|
|
4100
|
+
byId.set(t.id, w);
|
|
4101
|
+
views.set(t.id, readonly(w));
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
register(tools);
|
|
4105
|
+
return {
|
|
4106
|
+
readable(id) {
|
|
4107
|
+
entry(id);
|
|
4108
|
+
return views.get(id);
|
|
4109
|
+
},
|
|
4110
|
+
get: (id) => entry(id).get(),
|
|
4111
|
+
merge(id, patch) {
|
|
4112
|
+
const w = entry(id);
|
|
4113
|
+
w.set({
|
|
4114
|
+
...w.get(),
|
|
4115
|
+
...patch
|
|
4116
|
+
});
|
|
4117
|
+
},
|
|
4118
|
+
register
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
//#endregion
|
|
4122
|
+
//#region src/dom/tools/selection-edit.ts
|
|
4123
|
+
const CORNERS = [
|
|
4124
|
+
"nw",
|
|
4125
|
+
"ne",
|
|
4126
|
+
"sw",
|
|
4127
|
+
"se"
|
|
4128
|
+
];
|
|
4129
|
+
const ENDS = ["start", "end"];
|
|
4130
|
+
const CALLOUT = "callout";
|
|
4131
|
+
/** Pointer travel, CSS px, before a press on an annotation becomes a move. */
|
|
4132
|
+
const DRAG_THRESHOLD = 3;
|
|
4133
|
+
function createSelectionEdit(ctx, opts = {}) {
|
|
4134
|
+
let mode = { kind: "idle" };
|
|
4135
|
+
const batch = (fn) => ctx.history ? ctx.history.batch(fn) : fn();
|
|
4136
|
+
const reset = () => {
|
|
4137
|
+
if (mode.kind === "marquee") mode.el.remove();
|
|
4138
|
+
if (mode.kind === "move") {
|
|
4139
|
+
for (const el of mode.els) el.style.transform = "";
|
|
4140
|
+
ctx.root.removeAttribute("data-ink-dragging");
|
|
4141
|
+
}
|
|
4142
|
+
if (mode.kind === "resize" && mode.el) {
|
|
4143
|
+
mode.el.style.transform = "";
|
|
4144
|
+
mode.el.style.transformOrigin = "";
|
|
4145
|
+
mode.el.style.visibility = "";
|
|
4146
|
+
}
|
|
4147
|
+
if (mode.kind === "resize") mode.preview?.remove();
|
|
4148
|
+
if (mode.kind === "endpoint") {
|
|
4149
|
+
mode.preview.remove();
|
|
4150
|
+
if (mode.el) mode.el.style.visibility = "";
|
|
4151
|
+
}
|
|
4152
|
+
mode = { kind: "idle" };
|
|
4153
|
+
};
|
|
4154
|
+
const elementIn = (root, id) => root.querySelector(`[data-annotation="${CSS.escape(id)}"]`) ?? void 0;
|
|
4155
|
+
const previewFor = (host, a) => {
|
|
4156
|
+
const preview = host.ownerDocument.createElement("div");
|
|
4157
|
+
preview.className = "ink-annotation ink-edit-preview";
|
|
4158
|
+
preview.dataset.inkPreview = "";
|
|
4159
|
+
preview.dataset.type = a.type;
|
|
4160
|
+
preview.style.cssText = "position:absolute;pointer-events:none;";
|
|
4161
|
+
return preview;
|
|
4162
|
+
};
|
|
4163
|
+
/** Shows `draft` in `preview` beside `el`, which is hidden meanwhile. */
|
|
4164
|
+
const showPreview = (preview, el, draft, start) => {
|
|
4165
|
+
drawPreview(preview, draft, start);
|
|
4166
|
+
if (el) el.style.visibility = "hidden";
|
|
4167
|
+
if (!preview.isConnected) (el?.parentElement ?? start.host).append(preview);
|
|
4168
|
+
};
|
|
4169
|
+
function startHandle(e, hit, a, handle) {
|
|
4170
|
+
const el = elementIn(hit.host, a.id);
|
|
4171
|
+
if (CORNERS.includes(handle) && hasCornerHandles(a)) {
|
|
4172
|
+
const text = a.type === "freetext";
|
|
4173
|
+
mode = {
|
|
4174
|
+
kind: "resize",
|
|
4175
|
+
id: a.id,
|
|
4176
|
+
start: hit,
|
|
4177
|
+
corner: handle,
|
|
4178
|
+
box: text ? hit.transform.toCssRect(freeTextBox(a)) : handleBox(a, hit.transform),
|
|
4179
|
+
el,
|
|
4180
|
+
locked: keepsAspect(a),
|
|
4181
|
+
preview: text && (a.fringe !== void 0 || hasCallout(a)) ? previewFor(hit.host, a) : void 0
|
|
4182
|
+
};
|
|
4183
|
+
} else if (ENDS.includes(handle) && a.type === "line" || handle === CALLOUT && hasCallout(a)) {
|
|
4184
|
+
const preview = previewFor(hit.host, a);
|
|
4185
|
+
mode = {
|
|
4186
|
+
kind: "endpoint",
|
|
4187
|
+
id: a.id,
|
|
4188
|
+
start: hit,
|
|
4189
|
+
which: handle,
|
|
4190
|
+
from: hit.transform.toCss(endOf(a, handle)),
|
|
4191
|
+
el,
|
|
4192
|
+
preview
|
|
4193
|
+
};
|
|
4194
|
+
} else return false;
|
|
4195
|
+
e.stopPropagation();
|
|
4196
|
+
e.preventDefault();
|
|
4197
|
+
return true;
|
|
4198
|
+
}
|
|
4199
|
+
return {
|
|
4200
|
+
reset,
|
|
4201
|
+
onPointerDown(e, hit, annotationId) {
|
|
4202
|
+
if (!hit) return;
|
|
4203
|
+
reset();
|
|
4204
|
+
if (annotationId !== void 0) {
|
|
4205
|
+
const a = ctx.store.get(annotationId);
|
|
4206
|
+
if (!a) return;
|
|
4207
|
+
const markup = isTextMarkup(a);
|
|
4208
|
+
if (markup && !opts.marquee) return;
|
|
4209
|
+
const handle = handleOf(e.target);
|
|
4210
|
+
if (handle !== void 0 && !a.locked && startHandle(e, hit, a, handle)) return true;
|
|
4211
|
+
const selected = ctx.store.selection.get();
|
|
4212
|
+
if (e.shiftKey) {
|
|
4213
|
+
e.stopPropagation();
|
|
4214
|
+
ctx.store.select(selected.includes(annotationId) ? selected.filter((s) => s !== annotationId) : [...selected, annotationId]);
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
if (inText(e.target) && canEditFreeText(a, elementIn(hit.host, annotationId))) {
|
|
4218
|
+
e.stopPropagation();
|
|
4219
|
+
ctx.store.select([annotationId]);
|
|
4220
|
+
ctx.onActivate?.(annotationId);
|
|
4221
|
+
mode = {
|
|
4222
|
+
kind: "edit",
|
|
4223
|
+
id: annotationId,
|
|
4224
|
+
start: hit,
|
|
4225
|
+
caret: {
|
|
4226
|
+
clientX: e.clientX,
|
|
4227
|
+
clientY: e.clientY
|
|
4228
|
+
}
|
|
4229
|
+
};
|
|
4230
|
+
e.preventDefault();
|
|
4231
|
+
return true;
|
|
4232
|
+
}
|
|
4233
|
+
const narrow = selected.length > 1 && selected.includes(annotationId);
|
|
4234
|
+
if (narrow) e.stopPropagation();
|
|
4235
|
+
else if (markup) {
|
|
4236
|
+
e.stopPropagation();
|
|
4237
|
+
ctx.store.select([annotationId]);
|
|
4238
|
+
ctx.onActivate?.(annotationId);
|
|
4239
|
+
}
|
|
4240
|
+
mode = {
|
|
4241
|
+
kind: "press",
|
|
4242
|
+
id: annotationId,
|
|
4243
|
+
start: hit,
|
|
4244
|
+
narrow
|
|
4245
|
+
};
|
|
4246
|
+
e.preventDefault();
|
|
4247
|
+
return true;
|
|
4248
|
+
}
|
|
4249
|
+
if (!opts.marquee) {
|
|
4250
|
+
if (!e.shiftKey) ctx.store.select([]);
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
const el = hit.host.ownerDocument.createElement("div");
|
|
4254
|
+
el.className = "ink-marquee";
|
|
4255
|
+
el.dataset.inkMarquee = "";
|
|
4256
|
+
el.style.cssText = `position:absolute;pointer-events:none;left:${hit.css.x}px;top:${hit.css.y}px;width:0;height:0;`;
|
|
4257
|
+
hit.host.append(el);
|
|
4258
|
+
mode = {
|
|
4259
|
+
kind: "marquee",
|
|
4260
|
+
start: hit,
|
|
4261
|
+
el,
|
|
4262
|
+
additive: e.shiftKey
|
|
4263
|
+
};
|
|
4264
|
+
if (!e.shiftKey) ctx.store.select([]);
|
|
4265
|
+
e.preventDefault();
|
|
4266
|
+
return true;
|
|
4267
|
+
},
|
|
4268
|
+
onPointerMove(e) {
|
|
4269
|
+
if (mode.kind === "idle") return;
|
|
4270
|
+
if (e.buttons === 0 || !mode.start.host.isConnected) {
|
|
4271
|
+
reset();
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
const { start } = mode;
|
|
4275
|
+
if (mode.kind === "edit") return;
|
|
4276
|
+
if (mode.kind === "resize") {
|
|
4277
|
+
const { box, el, preview } = mode;
|
|
4278
|
+
const next = resizeCss(box, mode.corner, cornerAt(mode, e), minCss(start), {
|
|
4279
|
+
aspect: mode.locked || e.shiftKey,
|
|
4280
|
+
page: start.transform
|
|
4281
|
+
});
|
|
4282
|
+
if (preview) {
|
|
4283
|
+
const a = ctx.store.get(mode.id);
|
|
4284
|
+
if (a?.type !== "freetext") return;
|
|
4285
|
+
const draft = {
|
|
4286
|
+
...a,
|
|
4287
|
+
...resizedBox(a, userBox(start.transform, next))
|
|
4288
|
+
};
|
|
4289
|
+
showPreview(preview, el, draft, start);
|
|
4290
|
+
} else if (el) {
|
|
4291
|
+
const ox = box.left - (parseFloat(el.style.left) || 0);
|
|
4292
|
+
const oy = box.top - (parseFloat(el.style.top) || 0);
|
|
4293
|
+
el.style.transformOrigin = `${ox}px ${oy}px`;
|
|
4294
|
+
el.style.transform = `translate(${next.left - box.left}px, ${next.top - box.top}px) scale(${next.width / box.width}, ${next.height / box.height})`;
|
|
4295
|
+
}
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
if (mode.kind === "endpoint") {
|
|
4299
|
+
const a = ctx.store.get(mode.id);
|
|
4300
|
+
if (!a) return;
|
|
4301
|
+
const patch = endPatch(a, mode.which, endAt(mode, e));
|
|
4302
|
+
if (!patch) return;
|
|
4303
|
+
showPreview(mode.preview, mode.el, {
|
|
4304
|
+
...a,
|
|
4305
|
+
...patch
|
|
4306
|
+
}, start);
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
const css = cssIn(start.host, e);
|
|
4310
|
+
const dx = css.x - start.css.x;
|
|
4311
|
+
const dy = css.y - start.css.y;
|
|
4312
|
+
if (mode.kind === "press") {
|
|
4313
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
4314
|
+
const a = ctx.store.get(mode.id);
|
|
4315
|
+
if (!a || a.locked) {
|
|
4316
|
+
mode = { kind: "idle" };
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
const selected = ctx.store.selection.get();
|
|
4320
|
+
const pressed = mode.id;
|
|
4321
|
+
const ids = selected.includes(pressed) ? selected.filter((id) => {
|
|
4322
|
+
const m = ctx.store.get(id);
|
|
4323
|
+
return m !== void 0 && !m.locked && (id === pressed || !isTextMarkup(m));
|
|
4324
|
+
}) : [pressed];
|
|
4325
|
+
mode = {
|
|
4326
|
+
kind: "move",
|
|
4327
|
+
ids,
|
|
4328
|
+
start,
|
|
4329
|
+
els: ids.map((id) => elementIn(ctx.root, id)).filter((el) => el !== void 0)
|
|
4330
|
+
};
|
|
4331
|
+
ctx.root.setAttribute("data-ink-dragging", "");
|
|
4332
|
+
}
|
|
4333
|
+
if (mode.kind === "move") {
|
|
4334
|
+
for (const el of mode.els) el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
mode.el.style.left = `${Math.min(start.css.x, css.x)}px`;
|
|
4338
|
+
mode.el.style.top = `${Math.min(start.css.y, css.y)}px`;
|
|
4339
|
+
mode.el.style.width = `${Math.abs(dx)}px`;
|
|
4340
|
+
mode.el.style.height = `${Math.abs(dy)}px`;
|
|
4341
|
+
},
|
|
4342
|
+
onPointerCancel() {
|
|
4343
|
+
reset();
|
|
4344
|
+
},
|
|
4345
|
+
onPointerUp(e) {
|
|
4346
|
+
const current = mode;
|
|
4347
|
+
reset();
|
|
4348
|
+
if (current.kind !== "idle" && !current.start.host.isConnected) return;
|
|
4349
|
+
switch (current.kind) {
|
|
4350
|
+
case "press":
|
|
4351
|
+
if (current.narrow) {
|
|
4352
|
+
ctx.store.select([current.id]);
|
|
4353
|
+
ctx.onActivate?.(current.id);
|
|
4354
|
+
}
|
|
4355
|
+
return;
|
|
4356
|
+
case "edit": {
|
|
4357
|
+
const el = elementIn(current.start.host, current.id);
|
|
4358
|
+
const a = ctx.store.get(current.id);
|
|
4359
|
+
if (!el || !canEditFreeText(a, el) || editorFor(el)) return;
|
|
4360
|
+
editFreeTextIn(ctx.store, el, a.id, ctx.viewport.transformFor(a.page), { caret: current.caret });
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
case "move": {
|
|
4364
|
+
const { start } = current;
|
|
4365
|
+
const end = start.transform.fromCss(cssIn(start.host, e));
|
|
4366
|
+
const dx = end.x - start.user.x;
|
|
4367
|
+
const dy = end.y - start.user.y;
|
|
4368
|
+
batch(() => {
|
|
4369
|
+
for (const id of current.ids) {
|
|
4370
|
+
const a = ctx.store.get(id);
|
|
4371
|
+
if (a && !a.locked) ctx.store.update(id, translated(a, dx, dy));
|
|
4372
|
+
}
|
|
4373
|
+
});
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
case "resize": {
|
|
4377
|
+
const { start, box } = current;
|
|
4378
|
+
const next = resizeCss(box, current.corner, cornerAt(current, e), minCss(start), {
|
|
4379
|
+
aspect: current.locked || e.shiftKey,
|
|
4380
|
+
page: start.transform
|
|
4381
|
+
});
|
|
4382
|
+
if (next.left === box.left && next.top === box.top && next.width === box.width && next.height === box.height) return;
|
|
4383
|
+
const a = ctx.store.get(current.id);
|
|
4384
|
+
if (!a || a.locked) return;
|
|
4385
|
+
const to = userBox(start.transform, next);
|
|
4386
|
+
if (a.type === "freetext") {
|
|
4387
|
+
ctx.store.update(a.id, resizedBox(a, to));
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
const from = hasRectGeometry(a) ? a.rect : start.transform.fromCssRect(box);
|
|
4391
|
+
ctx.store.update(a.id, scaled(a, from, to));
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
case "endpoint": {
|
|
4395
|
+
const a = ctx.store.get(current.id);
|
|
4396
|
+
if (!a || a.locked) return;
|
|
4397
|
+
const css = cssIn(current.start.host, e);
|
|
4398
|
+
if (css.x === current.start.css.x && css.y === current.start.css.y) return;
|
|
4399
|
+
const patch = endPatch(a, current.which, endAt(current, e));
|
|
4400
|
+
if (patch) ctx.store.update(a.id, patch);
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
case "marquee": {
|
|
4404
|
+
const { start } = current;
|
|
4405
|
+
const box = rectFromPoints(start.user, start.transform.fromCss(cssIn(start.host, e)));
|
|
4406
|
+
if (box.width === 0 && box.height === 0) return;
|
|
4407
|
+
const ids = withoutReplies(ctx.store.list.get()).filter((a) => a.page === start.page && rectsIntersect(a.rect, box)).map((a) => a.id);
|
|
4408
|
+
ctx.store.select(current.additive ? [.../* @__PURE__ */ new Set([...ctx.store.selection.get(), ...ids])] : ids);
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
},
|
|
4413
|
+
onKeyDown(e) {
|
|
4414
|
+
if (e.key === "Escape") {
|
|
4415
|
+
reset();
|
|
4416
|
+
ctx.store.select([]);
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
4420
|
+
if (e.defaultPrevented) return;
|
|
4421
|
+
if ((e.target instanceof Element ? e.target : null)?.closest("[data-annotation], input, textarea, [contenteditable]:not([contenteditable=\"false\"])")) return;
|
|
4422
|
+
for (const id of ctx.store.selection.get()) if (ctx.store.get(id)?.locked !== true) ctx.store.remove(id);
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
/** The handle (`nw`, `start`, ...) a press landed on, if it landed on one. */
|
|
4427
|
+
function handleOf(target) {
|
|
4428
|
+
if (!(target instanceof Element)) return void 0;
|
|
4429
|
+
return target.closest("[data-handle]")?.getAttribute("data-handle") ?? void 0;
|
|
4430
|
+
}
|
|
4431
|
+
/** True when a press landed inside a free text's drawn text box. */
|
|
4432
|
+
function inText(target) {
|
|
4433
|
+
return target instanceof Element && target.closest(".ink-free-text") !== null;
|
|
4434
|
+
}
|
|
4435
|
+
/** The point a line's end or a callout's tip handle sits on. */
|
|
4436
|
+
function endOf(a, which) {
|
|
4437
|
+
if (which === "callout") return hasCallout(a) ? a.callout[0] : void 0;
|
|
4438
|
+
return a.type === "line" ? a[which] : void 0;
|
|
4439
|
+
}
|
|
4440
|
+
/** The patch that moves a line's end, or a callout's tip, to `to`; undefined for neither. */
|
|
4441
|
+
function endPatch(a, which, to) {
|
|
4442
|
+
if (which === "callout") return hasCallout(a) ? movedTip(a, to) : void 0;
|
|
4443
|
+
return a.type === "line" ? movedEndpoint(a, which, to) : void 0;
|
|
4444
|
+
}
|
|
4445
|
+
/** A resized CSS box in user space, at least `MIN_SIZE` a side. */
|
|
4446
|
+
function userBox(t, css) {
|
|
4447
|
+
const r = t.fromCssRect(css);
|
|
4448
|
+
return {
|
|
4449
|
+
...r,
|
|
4450
|
+
width: Math.max(1, r.width),
|
|
4451
|
+
height: Math.max(1, r.height)
|
|
4452
|
+
};
|
|
4453
|
+
}
|
|
4454
|
+
/** Draws `a` into the preview element at its own box. */
|
|
4455
|
+
function drawPreview(el, a, start) {
|
|
4456
|
+
const box = annotationBox(a, start.transform);
|
|
4457
|
+
el.style.left = `${box.left}px`;
|
|
4458
|
+
el.style.top = `${box.top}px`;
|
|
4459
|
+
el.style.width = `${box.width}px`;
|
|
4460
|
+
el.style.height = `${box.height}px`;
|
|
4461
|
+
renderAnnotationContent(el, a, start.transform);
|
|
4462
|
+
}
|
|
4463
|
+
/** Keeps a CSS point on the page. */
|
|
4464
|
+
function onPage(start, x, y) {
|
|
4465
|
+
const { cssWidth, cssHeight } = start.transform;
|
|
4466
|
+
return {
|
|
4467
|
+
x: Math.min(Math.max(x, 0), cssWidth),
|
|
4468
|
+
y: Math.min(Math.max(y, 0), cssHeight)
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Where the dragged line end is now, in user space: where it started plus the pointer's
|
|
4473
|
+
* travel, kept on the page.
|
|
4474
|
+
*/
|
|
4475
|
+
function endAt(mode, e) {
|
|
4476
|
+
const { start, from } = mode;
|
|
4477
|
+
const css = cssIn(start.host, e);
|
|
4478
|
+
return start.transform.fromCss(onPage(start, from.x + css.x - start.css.x, from.y + css.y - start.css.y));
|
|
4479
|
+
}
|
|
4480
|
+
/**
|
|
4481
|
+
* Where the dragged corner is now: where it started plus the pointer's travel since the
|
|
4482
|
+
* press, kept on the page. Keeping the press's offset from the corner means a click on a
|
|
4483
|
+
* handle, with no drag, leaves the box as it was.
|
|
4484
|
+
*/
|
|
4485
|
+
function cornerAt(mode, e) {
|
|
4486
|
+
const { start, box, corner } = mode;
|
|
4487
|
+
const css = cssIn(start.host, e);
|
|
4488
|
+
return onPage(start, (corner[1] === "w" ? box.left : box.left + box.width) + css.x - start.css.x, (corner[0] === "n" ? box.top : box.top + box.height) + css.y - start.css.y);
|
|
4489
|
+
}
|
|
4490
|
+
/** `MIN_SIZE` points as CSS px on the press's page. */
|
|
4491
|
+
function minCss(start) {
|
|
4492
|
+
return start.transform.toCssLength(1);
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* `box` with its `corner` dragged to `p` and the opposite corner fixed, at least `min` a side.
|
|
4496
|
+
* With `aspect`, the box keeps its proportions: it scales by the larger of the two axes'
|
|
4497
|
+
* changes, kept on the page.
|
|
4498
|
+
*/
|
|
4499
|
+
function resizeCss(box, corner, p, min, opts) {
|
|
4500
|
+
const west = corner[1] === "w";
|
|
4501
|
+
const north = corner[0] === "n";
|
|
4502
|
+
/** The fixed corner. */
|
|
4503
|
+
const fx = west ? box.left + box.width : box.left;
|
|
4504
|
+
const fy = north ? box.top + box.height : box.top;
|
|
4505
|
+
let width = Math.max(west ? fx - p.x : p.x - fx, min);
|
|
4506
|
+
let height = Math.max(north ? fy - p.y : p.y - fy, min);
|
|
4507
|
+
if (opts.aspect && box.width > 0 && box.height > 0) {
|
|
4508
|
+
const roomX = west ? fx : opts.page.cssWidth - fx;
|
|
4509
|
+
const roomY = north ? fy : opts.page.cssHeight - fy;
|
|
4510
|
+
const lo = min / Math.min(box.width, box.height);
|
|
4511
|
+
const hi = Math.min(roomX / box.width, roomY / box.height);
|
|
4512
|
+
const s = Math.min(Math.max(width / box.width, height / box.height, lo), Math.max(hi, lo));
|
|
4513
|
+
width = box.width * s;
|
|
4514
|
+
height = box.height * s;
|
|
4515
|
+
}
|
|
4516
|
+
return {
|
|
4517
|
+
left: west ? fx - width : fx,
|
|
4518
|
+
top: north ? fy - height : fy,
|
|
4519
|
+
width,
|
|
4520
|
+
height
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4523
|
+
//#endregion
|
|
4524
|
+
//#region src/dom/tools/controller.ts
|
|
4525
|
+
/** Host-relative CSS point for a client point. */
|
|
4526
|
+
function cssIn(host, e) {
|
|
4527
|
+
const r = host.getBoundingClientRect();
|
|
4528
|
+
return {
|
|
4529
|
+
x: e.clientX - r.left,
|
|
4530
|
+
y: e.clientY - r.top
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
/** Like `cssIn`, clamped to the host's box, so a drag past the page edge stops at the edge. */
|
|
4534
|
+
function clampedCssIn(host, e) {
|
|
4535
|
+
const r = host.getBoundingClientRect();
|
|
4536
|
+
return {
|
|
4537
|
+
x: Math.min(Math.max(e.clientX - r.left, 0), r.width),
|
|
4538
|
+
y: Math.min(Math.max(e.clientY - r.top, 0), r.height)
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
/** The mounted page under a client point, with the point in CSS px and user space. */
|
|
4542
|
+
function hitFor(ctx, e) {
|
|
4543
|
+
const hit = ctx.viewport.pageAtCss(e.clientX, e.clientY);
|
|
4544
|
+
if (!hit) return void 0;
|
|
4545
|
+
const css = cssIn(hit.host, e);
|
|
4546
|
+
return {
|
|
2691
4547
|
...hit,
|
|
2692
4548
|
css,
|
|
2693
4549
|
user: hit.transform.fromCss(css)
|
|
@@ -2710,26 +4566,92 @@ function createToolController$1(opts) {
|
|
|
2710
4566
|
activeGroup: () => current?.styleGroup,
|
|
2711
4567
|
active
|
|
2712
4568
|
}),
|
|
2713
|
-
options: () => registry.get(current?.id ?? active.get())
|
|
4569
|
+
options: () => registry.get(current?.id ?? active.get()),
|
|
4570
|
+
made(ids) {
|
|
4571
|
+
if (current?.kind !== "one-shot" || ids.length === 0) return;
|
|
4572
|
+
clearMade();
|
|
4573
|
+
made = [...ids];
|
|
4574
|
+
ctx.root.dataset.inkMade = "";
|
|
4575
|
+
stopMade = [ctx.store.list.subscribe(checkMade), ctx.store.selection.subscribe(checkMade)];
|
|
4576
|
+
},
|
|
4577
|
+
done() {
|
|
4578
|
+
clearMade();
|
|
4579
|
+
ctx.store.select([]);
|
|
4580
|
+
set("select-text");
|
|
4581
|
+
}
|
|
2714
4582
|
};
|
|
2715
4583
|
const originalCursor = ctx.root.style.cursor;
|
|
2716
4584
|
/** The pointer between a primary press and its release or cancel. */
|
|
2717
4585
|
let pressed;
|
|
2718
4586
|
/** Pointers whose release belongs to a tool that is no longer active. */
|
|
2719
4587
|
const swallow = /* @__PURE__ */ new Set();
|
|
4588
|
+
/** What the active one-shot tool made; undefined while it is ready. */
|
|
4589
|
+
let made;
|
|
4590
|
+
let stopMade = [];
|
|
4591
|
+
/** Whether the press now under way found the tool made (see the top of the file). */
|
|
4592
|
+
let pressFoundMade = false;
|
|
4593
|
+
/** Moves and resizes what the active one-shot tool made. */
|
|
4594
|
+
let madeEdit;
|
|
4595
|
+
/** The press now under way went to `madeEdit`, not the tool. */
|
|
4596
|
+
let editingMade = false;
|
|
4597
|
+
function clearMade() {
|
|
4598
|
+
stopMade.forEach((stop) => stop());
|
|
4599
|
+
stopMade = [];
|
|
4600
|
+
made = void 0;
|
|
4601
|
+
delete ctx.root.dataset.inkMade;
|
|
4602
|
+
}
|
|
4603
|
+
/** Leaves the made state when its items are gone, or switches when the selection moved on. */
|
|
4604
|
+
function checkMade() {
|
|
4605
|
+
if (!made) return;
|
|
4606
|
+
const store = ctx.store;
|
|
4607
|
+
if (!made.some((id) => (store.get(id) ?? store._getProposed(id)) !== void 0)) {
|
|
4608
|
+
clearMade();
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
const selected = store.selection.get();
|
|
4612
|
+
if (!made.some((id) => selected.includes(id))) set("select-text");
|
|
4613
|
+
}
|
|
2720
4614
|
/** True for events from a pointer other than the one whose press is live. */
|
|
2721
4615
|
const foreign = (e) => pressed !== void 0 && pressed !== e.pointerId;
|
|
4616
|
+
const readFirst = () => {
|
|
4617
|
+
pressFoundMade = made !== void 0;
|
|
4618
|
+
};
|
|
4619
|
+
const readAfterEditor = () => {
|
|
4620
|
+
pressFoundMade ||= made !== void 0;
|
|
4621
|
+
};
|
|
2722
4622
|
const onDown = (e) => {
|
|
2723
4623
|
if (!e.isPrimary || e.button !== 0 || inCustomContent(e.target)) return;
|
|
2724
4624
|
if (foreign(e)) return;
|
|
2725
4625
|
swallow.delete(e.pointerId);
|
|
2726
|
-
pressed = e.pointerId;
|
|
2727
4626
|
const annotationId = (e.target instanceof Element ? e.target : null)?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
|
|
4627
|
+
if (current?.kind === "one-shot") {
|
|
4628
|
+
if (pressFoundMade && annotationId === void 0 && !onScrollBar(e, ctx.root)) {
|
|
4629
|
+
e.preventDefault();
|
|
4630
|
+
swallow.add(e.pointerId);
|
|
4631
|
+
ctx.done();
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
const a = annotationId === void 0 ? void 0 : ctx.store.get(annotationId);
|
|
4635
|
+
if (a && made?.includes(a.id) && madeEdit) {
|
|
4636
|
+
pressed = e.pointerId;
|
|
4637
|
+
editingMade = true;
|
|
4638
|
+
if (madeEdit.onPointerDown(e, hitFor(ctx, e), a.id) === true) capture(e.pointerId);
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
4641
|
+
if (a && (!isTextMarkup(a) || pressFoundMade)) set("select-text");
|
|
4642
|
+
}
|
|
4643
|
+
pressed = e.pointerId;
|
|
2728
4644
|
if (current?.onPointerDown?.(e, hitFor(ctx, e), annotationId) === true) capture(e.pointerId);
|
|
2729
4645
|
};
|
|
2730
4646
|
const onMove = (e) => {
|
|
2731
4647
|
if (!e.isPrimary || foreign(e)) return;
|
|
2732
|
-
|
|
4648
|
+
const missed = pressed === e.pointerId && e.buttons === 0;
|
|
4649
|
+
if (missed) pressed = void 0;
|
|
4650
|
+
if (editingMade) {
|
|
4651
|
+
madeEdit?.onPointerMove(e, hitFor(ctx, e));
|
|
4652
|
+
if (missed) editingMade = false;
|
|
4653
|
+
return;
|
|
4654
|
+
}
|
|
2733
4655
|
current?.onPointerMove?.(e, hitFor(ctx, e));
|
|
2734
4656
|
};
|
|
2735
4657
|
const onUp = (e) => {
|
|
@@ -2739,7 +4661,10 @@ function createToolController$1(opts) {
|
|
|
2739
4661
|
release(e.pointerId);
|
|
2740
4662
|
return;
|
|
2741
4663
|
}
|
|
2742
|
-
|
|
4664
|
+
if (editingMade) {
|
|
4665
|
+
editingMade = false;
|
|
4666
|
+
madeEdit?.onPointerUp(e, hitFor(ctx, e));
|
|
4667
|
+
} else current?.onPointerUp?.(e, hitFor(ctx, e));
|
|
2743
4668
|
release(e.pointerId);
|
|
2744
4669
|
};
|
|
2745
4670
|
const onCancel = (e) => {
|
|
@@ -2747,13 +4672,41 @@ function createToolController$1(opts) {
|
|
|
2747
4672
|
if (e.type === "lostpointercapture" && pressed !== e.pointerId) return;
|
|
2748
4673
|
if (pressed === e.pointerId) pressed = void 0;
|
|
2749
4674
|
swallow.delete(e.pointerId);
|
|
2750
|
-
|
|
4675
|
+
if (editingMade) {
|
|
4676
|
+
editingMade = false;
|
|
4677
|
+
madeEdit?.onPointerCancel();
|
|
4678
|
+
} else current?.onPointerCancel?.(e);
|
|
2751
4679
|
};
|
|
2752
4680
|
const onKey = (e) => {
|
|
2753
4681
|
if (inCustomContent(e.target)) return;
|
|
4682
|
+
if (made && e.key === "Escape" && !e.defaultPrevented) {
|
|
4683
|
+
e.preventDefault();
|
|
4684
|
+
ctx.done();
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
if (made && (e.key === "Delete" || e.key === "Backspace")) {
|
|
4688
|
+
madeEdit?.onKeyDown(e);
|
|
4689
|
+
return;
|
|
4690
|
+
}
|
|
2754
4691
|
current?.onKeyDown?.(e);
|
|
2755
4692
|
if (isViewportEnter(e, ctx.root)) editSelectedText(e);
|
|
2756
4693
|
};
|
|
4694
|
+
/**
|
|
4695
|
+
* Escape in an editor on a made text box, however it was opened (a press inside, Enter, a
|
|
4696
|
+
* double-click): commits and ends the tool. Captured, as the editor keeps its keys to itself.
|
|
4697
|
+
*/
|
|
4698
|
+
const onEditorEscape = (e) => {
|
|
4699
|
+
if (!made || e.key !== "Escape" || e.isComposing || e.defaultPrevented) return;
|
|
4700
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
4701
|
+
if (!target?.closest("[data-ink-editor]")) return;
|
|
4702
|
+
const el = target.closest("[data-annotation]");
|
|
4703
|
+
const id = el?.getAttribute("data-annotation");
|
|
4704
|
+
if (!el || !id || !made.includes(id)) return;
|
|
4705
|
+
e.preventDefault();
|
|
4706
|
+
e.stopPropagation();
|
|
4707
|
+
editorFor(el)?.close(true);
|
|
4708
|
+
ctx.done();
|
|
4709
|
+
};
|
|
2757
4710
|
function editSelectedText(e) {
|
|
2758
4711
|
const selected = ctx.store.selection.get();
|
|
2759
4712
|
if (selected.length !== 1) return;
|
|
@@ -2774,26 +4727,35 @@ function createToolController$1(opts) {
|
|
|
2774
4727
|
if (ctx.root.hasPointerCapture?.(id)) ctx.root.releasePointerCapture(id);
|
|
2775
4728
|
} catch {}
|
|
2776
4729
|
}
|
|
4730
|
+
const doc = ctx.root.ownerDocument;
|
|
4731
|
+
doc.defaultView?.addEventListener("pointerdown", readFirst, { capture: true });
|
|
4732
|
+
doc.addEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
2777
4733
|
ctx.root.addEventListener("pointerdown", onDown, { capture: true });
|
|
2778
4734
|
ctx.root.addEventListener("pointermove", onMove);
|
|
2779
4735
|
ctx.root.addEventListener("pointerup", onUp);
|
|
2780
4736
|
ctx.root.addEventListener("pointercancel", onCancel);
|
|
2781
4737
|
ctx.root.addEventListener("lostpointercapture", onCancel);
|
|
2782
4738
|
ctx.root.addEventListener("keydown", onKey);
|
|
4739
|
+
ctx.root.addEventListener("keydown", onEditorEscape, { capture: true });
|
|
2783
4740
|
function set(id, options) {
|
|
2784
4741
|
const next = byId.get(id);
|
|
2785
4742
|
if (!next) throw new RangeError(`Unknown tool ${id}`);
|
|
2786
4743
|
if (options) registry.merge(id, options);
|
|
2787
4744
|
if (pressed !== void 0) {
|
|
2788
|
-
|
|
4745
|
+
if (editingMade) madeEdit?.onPointerCancel();
|
|
4746
|
+
else current?.onPointerCancel?.(new PointerEvent("pointercancel", {
|
|
2789
4747
|
pointerId: pressed,
|
|
2790
4748
|
isPrimary: true
|
|
2791
4749
|
}));
|
|
2792
4750
|
swallow.add(pressed);
|
|
2793
4751
|
pressed = void 0;
|
|
2794
4752
|
}
|
|
4753
|
+
editingMade = false;
|
|
4754
|
+
madeEdit?.reset();
|
|
2795
4755
|
current?.deactivate();
|
|
4756
|
+
clearMade();
|
|
2796
4757
|
current = next;
|
|
4758
|
+
madeEdit = next.kind === "one-shot" ? createSelectionEdit(ctx) : void 0;
|
|
2797
4759
|
next.activate(ctx);
|
|
2798
4760
|
ctx.root.dataset.inkTool = id;
|
|
2799
4761
|
ctx.root.style.cursor = next.cursor;
|
|
@@ -2812,14 +4774,20 @@ function createToolController$1(opts) {
|
|
|
2812
4774
|
return registry.readable(id);
|
|
2813
4775
|
},
|
|
2814
4776
|
destroy() {
|
|
4777
|
+
madeEdit?.reset();
|
|
4778
|
+
madeEdit = void 0;
|
|
2815
4779
|
current?.deactivate();
|
|
4780
|
+
clearMade();
|
|
2816
4781
|
current = void 0;
|
|
4782
|
+
doc.defaultView?.removeEventListener("pointerdown", readFirst, { capture: true });
|
|
4783
|
+
doc.removeEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
2817
4784
|
ctx.root.removeEventListener("pointerdown", onDown, { capture: true });
|
|
2818
4785
|
ctx.root.removeEventListener("pointermove", onMove);
|
|
2819
4786
|
ctx.root.removeEventListener("pointerup", onUp);
|
|
2820
4787
|
ctx.root.removeEventListener("pointercancel", onCancel);
|
|
2821
4788
|
ctx.root.removeEventListener("lostpointercapture", onCancel);
|
|
2822
4789
|
ctx.root.removeEventListener("keydown", onKey);
|
|
4790
|
+
ctx.root.removeEventListener("keydown", onEditorEscape, { capture: true });
|
|
2823
4791
|
delete ctx.root.dataset.inkTool;
|
|
2824
4792
|
ctx.root.style.cursor = originalCursor;
|
|
2825
4793
|
}
|
|
@@ -2841,12 +4809,25 @@ function isViewportEnter(e, root) {
|
|
|
2841
4809
|
return target.closest("[data-ink-text-layer], [data-ink-canvas-layer]") !== null;
|
|
2842
4810
|
}
|
|
2843
4811
|
/**
|
|
4812
|
+
* True for a press on `root` itself outside its client area: on a scroll bar (or its border).
|
|
4813
|
+
* Client coordinates only, so a synthetic press gives the same answer as a real one.
|
|
4814
|
+
*/
|
|
4815
|
+
function onScrollBar(e, root) {
|
|
4816
|
+
if (e.target !== root) return false;
|
|
4817
|
+
const r = root.getBoundingClientRect();
|
|
4818
|
+
const x = e.clientX - r.left - root.clientLeft;
|
|
4819
|
+
const y = e.clientY - r.top - root.clientTop;
|
|
4820
|
+
return x < 0 || y < 0 || x >= root.clientWidth || y >= root.clientHeight;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
2844
4823
|
* True for a target inside a custom annotation element (`data-custom`) but not that element
|
|
2845
4824
|
* itself: the host's buttons and inputs there own their presses and keys, as in the overlay.
|
|
2846
4825
|
*/
|
|
2847
4826
|
function inCustomContent(target) {
|
|
2848
4827
|
if (!(target instanceof Element)) return false;
|
|
2849
4828
|
if (target.closest("[data-ink-editor]")) return true;
|
|
4829
|
+
const layer = target.closest(".ink-plugin-layer");
|
|
4830
|
+
if (layer && layer !== target) return true;
|
|
2850
4831
|
const el = target.closest("[data-annotation]");
|
|
2851
4832
|
return el !== null && el !== target && el.hasAttribute("data-custom") && target.closest("[data-handle]") === null;
|
|
2852
4833
|
}
|
|
@@ -2917,6 +4898,7 @@ function freehandTool(opts) {
|
|
|
2917
4898
|
};
|
|
2918
4899
|
return {
|
|
2919
4900
|
id: opts.id,
|
|
4901
|
+
kind: "sticky",
|
|
2920
4902
|
cursor: "crosshair",
|
|
2921
4903
|
styleGroup: "ink",
|
|
2922
4904
|
activate(c) {
|
|
@@ -3123,7 +5105,7 @@ function freetextTool() {
|
|
|
3123
5105
|
};
|
|
3124
5106
|
const editor = editFreeText(el, draftBox, {
|
|
3125
5107
|
transform: t,
|
|
3126
|
-
commit(contents, rect) {
|
|
5108
|
+
commit(contents, rect, _grown, how) {
|
|
3127
5109
|
done();
|
|
3128
5110
|
if (contents.trim() === "") return refocus();
|
|
3129
5111
|
const created = c.store.add({
|
|
@@ -3133,7 +5115,9 @@ function freetextTool() {
|
|
|
3133
5115
|
rect
|
|
3134
5116
|
});
|
|
3135
5117
|
c.store.select([created.id]);
|
|
5118
|
+
c.made([created.id]);
|
|
3136
5119
|
refocus(created.id);
|
|
5120
|
+
if (how === "escape") c.done();
|
|
3137
5121
|
},
|
|
3138
5122
|
cancel() {
|
|
3139
5123
|
done();
|
|
@@ -3159,6 +5143,7 @@ function freetextTool() {
|
|
|
3159
5143
|
};
|
|
3160
5144
|
return {
|
|
3161
5145
|
id: "freetext",
|
|
5146
|
+
kind: "one-shot",
|
|
3162
5147
|
cursor: "text",
|
|
3163
5148
|
styleGroup: "text",
|
|
3164
5149
|
defaultOptions: {},
|
|
@@ -3263,6 +5248,7 @@ function highlightTool() {
|
|
|
3263
5248
|
};
|
|
3264
5249
|
return {
|
|
3265
5250
|
id: "highlight",
|
|
5251
|
+
kind: "sticky",
|
|
3266
5252
|
cursor: "text",
|
|
3267
5253
|
styleGroup: "markup",
|
|
3268
5254
|
defaultOptions: { ...DEFAULT_HIGHLIGHT_OPTIONS },
|
|
@@ -3306,10 +5292,12 @@ function noteTool() {
|
|
|
3306
5292
|
}
|
|
3307
5293
|
});
|
|
3308
5294
|
c.store.select([a.id]);
|
|
5295
|
+
c.made([a.id]);
|
|
3309
5296
|
return a.id;
|
|
3310
5297
|
};
|
|
3311
5298
|
return {
|
|
3312
5299
|
id: "note",
|
|
5300
|
+
kind: "one-shot",
|
|
3313
5301
|
cursor: "copy",
|
|
3314
5302
|
activate(c) {
|
|
3315
5303
|
ctx = c;
|
|
@@ -3378,17 +5366,24 @@ function redactAreaTool() {
|
|
|
3378
5366
|
let ctx;
|
|
3379
5367
|
let start;
|
|
3380
5368
|
let draft;
|
|
5369
|
+
/** Selects what the tool just added, together, and reports it made. */
|
|
5370
|
+
const made = (c, ids) => {
|
|
5371
|
+
c.store.select(ids);
|
|
5372
|
+
c.made(ids);
|
|
5373
|
+
};
|
|
3381
5374
|
/** Text redactions from the live selection, one per page. False when nothing is selected. */
|
|
3382
5375
|
const fromSelection = () => {
|
|
3383
5376
|
if (!ctx) return false;
|
|
3384
5377
|
const selections = ctx.selectionQuads();
|
|
3385
5378
|
if (selections.length === 0) return false;
|
|
3386
|
-
|
|
5379
|
+
const c = ctx;
|
|
5380
|
+
const ids = selections.map((s) => c.store.add(c.toolStyle.resolve("redact", {
|
|
3387
5381
|
type: "redact",
|
|
3388
5382
|
page: s.page,
|
|
3389
5383
|
quads: s.quads
|
|
3390
|
-
}));
|
|
3391
|
-
|
|
5384
|
+
})).id);
|
|
5385
|
+
c.clearSelection();
|
|
5386
|
+
made(c, ids);
|
|
3392
5387
|
return true;
|
|
3393
5388
|
};
|
|
3394
5389
|
const clear = () => {
|
|
@@ -3398,6 +5393,7 @@ function redactAreaTool() {
|
|
|
3398
5393
|
};
|
|
3399
5394
|
return {
|
|
3400
5395
|
id: "redact-area",
|
|
5396
|
+
kind: "one-shot",
|
|
3401
5397
|
cursor: "crosshair",
|
|
3402
5398
|
styleGroup: "redact",
|
|
3403
5399
|
activate(c) {
|
|
@@ -3428,422 +5424,49 @@ function redactAreaTool() {
|
|
|
3428
5424
|
if (!draft) {
|
|
3429
5425
|
draft = start.host.ownerDocument.createElement("div");
|
|
3430
5426
|
draft.className = "ink-draft";
|
|
3431
|
-
draft.dataset.inkDraft = "redact";
|
|
3432
|
-
draft.style.position = "absolute";
|
|
3433
|
-
draft.style.pointerEvents = "none";
|
|
3434
|
-
start.host.append(draft);
|
|
3435
|
-
}
|
|
3436
|
-
draft.style.left = `${Math.min(start.css.x, css.x)}px`;
|
|
3437
|
-
draft.style.top = `${Math.min(start.css.y, css.y)}px`;
|
|
3438
|
-
draft.style.width = `${Math.abs(css.x - start.css.x)}px`;
|
|
3439
|
-
draft.style.height = `${Math.abs(css.y - start.css.y)}px`;
|
|
3440
|
-
},
|
|
3441
|
-
onPointerCancel() {
|
|
3442
|
-
clear();
|
|
3443
|
-
},
|
|
3444
|
-
onPointerUp(e) {
|
|
3445
|
-
if (!ctx || !start) return;
|
|
3446
|
-
if (!start.host.isConnected) {
|
|
3447
|
-
clear();
|
|
3448
|
-
return;
|
|
3449
|
-
}
|
|
3450
|
-
const css = clampedCssIn(start.host, e);
|
|
3451
|
-
const width = Math.abs(css.x - start.css.x);
|
|
3452
|
-
const height = Math.abs(css.y - start.css.y);
|
|
3453
|
-
if (width >= MIN_CSS$1 && height >= MIN_CSS$1) {
|
|
3454
|
-
const rect = start.transform.fromCssRect({
|
|
3455
|
-
left: Math.min(start.css.x, css.x),
|
|
3456
|
-
top: Math.min(start.css.y, css.y),
|
|
3457
|
-
width,
|
|
3458
|
-
height
|
|
3459
|
-
});
|
|
3460
|
-
ctx.store.add(ctx.toolStyle.resolve("redact", {
|
|
3461
|
-
type: "redact",
|
|
3462
|
-
page: start.page,
|
|
3463
|
-
rect
|
|
3464
|
-
}));
|
|
3465
|
-
} else fromSelection();
|
|
3466
|
-
clear();
|
|
3467
|
-
},
|
|
3468
|
-
onKeyDown(e) {
|
|
3469
|
-
if (!!ctx && isViewportEnter(e, ctx.root) && fromSelection()) e.preventDefault();
|
|
3470
|
-
}
|
|
3471
|
-
};
|
|
3472
|
-
}
|
|
3473
|
-
//#endregion
|
|
3474
|
-
//#region src/dom/tools/selection-edit.ts
|
|
3475
|
-
const CORNERS = [
|
|
3476
|
-
"nw",
|
|
3477
|
-
"ne",
|
|
3478
|
-
"sw",
|
|
3479
|
-
"se"
|
|
3480
|
-
];
|
|
3481
|
-
const ENDS = ["start", "end"];
|
|
3482
|
-
const CALLOUT = "callout";
|
|
3483
|
-
/** Pointer travel, CSS px, before a press on an annotation becomes a move. */
|
|
3484
|
-
const DRAG_THRESHOLD = 3;
|
|
3485
|
-
function createSelectionEdit(ctx, opts = {}) {
|
|
3486
|
-
let mode = { kind: "idle" };
|
|
3487
|
-
const batch = (fn) => ctx.history ? ctx.history.batch(fn) : fn();
|
|
3488
|
-
const reset = () => {
|
|
3489
|
-
if (mode.kind === "marquee") mode.el.remove();
|
|
3490
|
-
if (mode.kind === "move") for (const el of mode.els) el.style.transform = "";
|
|
3491
|
-
if (mode.kind === "resize" && mode.el) {
|
|
3492
|
-
mode.el.style.transform = "";
|
|
3493
|
-
mode.el.style.transformOrigin = "";
|
|
3494
|
-
mode.el.style.visibility = "";
|
|
3495
|
-
}
|
|
3496
|
-
if (mode.kind === "resize") mode.preview?.remove();
|
|
3497
|
-
if (mode.kind === "endpoint") {
|
|
3498
|
-
mode.preview.remove();
|
|
3499
|
-
if (mode.el) mode.el.style.visibility = "";
|
|
3500
|
-
}
|
|
3501
|
-
mode = { kind: "idle" };
|
|
3502
|
-
};
|
|
3503
|
-
const elementIn = (root, id) => root.querySelector(`[data-annotation="${CSS.escape(id)}"]`) ?? void 0;
|
|
3504
|
-
const previewFor = (host, a) => {
|
|
3505
|
-
const preview = host.ownerDocument.createElement("div");
|
|
3506
|
-
preview.className = "ink-annotation ink-edit-preview";
|
|
3507
|
-
preview.dataset.inkPreview = "";
|
|
3508
|
-
preview.dataset.type = a.type;
|
|
3509
|
-
preview.style.cssText = "position:absolute;pointer-events:none;";
|
|
3510
|
-
return preview;
|
|
3511
|
-
};
|
|
3512
|
-
/** Shows `draft` in `preview` beside `el`, which is hidden meanwhile. */
|
|
3513
|
-
const showPreview = (preview, el, draft, start) => {
|
|
3514
|
-
drawPreview(preview, draft, start);
|
|
3515
|
-
if (el) el.style.visibility = "hidden";
|
|
3516
|
-
if (!preview.isConnected) (el?.parentElement ?? start.host).append(preview);
|
|
3517
|
-
};
|
|
3518
|
-
function startHandle(e, hit, a, handle) {
|
|
3519
|
-
const el = elementIn(hit.host, a.id);
|
|
3520
|
-
if (CORNERS.includes(handle) && hasCornerHandles(a)) {
|
|
3521
|
-
const text = a.type === "freetext";
|
|
3522
|
-
mode = {
|
|
3523
|
-
kind: "resize",
|
|
3524
|
-
id: a.id,
|
|
3525
|
-
start: hit,
|
|
3526
|
-
corner: handle,
|
|
3527
|
-
box: text ? hit.transform.toCssRect(freeTextBox(a)) : el ? boxOf(el) : annotationBox(a, hit.transform),
|
|
3528
|
-
el,
|
|
3529
|
-
locked: keepsAspect(a),
|
|
3530
|
-
preview: text && (a.fringe !== void 0 || hasCallout(a)) ? previewFor(hit.host, a) : void 0
|
|
3531
|
-
};
|
|
3532
|
-
} else if (ENDS.includes(handle) && a.type === "line" || handle === CALLOUT && hasCallout(a)) {
|
|
3533
|
-
const preview = previewFor(hit.host, a);
|
|
3534
|
-
mode = {
|
|
3535
|
-
kind: "endpoint",
|
|
3536
|
-
id: a.id,
|
|
3537
|
-
start: hit,
|
|
3538
|
-
which: handle,
|
|
3539
|
-
from: hit.transform.toCss(endOf(a, handle)),
|
|
3540
|
-
el,
|
|
3541
|
-
preview
|
|
3542
|
-
};
|
|
3543
|
-
} else return false;
|
|
3544
|
-
e.stopPropagation();
|
|
3545
|
-
e.preventDefault();
|
|
3546
|
-
return true;
|
|
3547
|
-
}
|
|
3548
|
-
return {
|
|
3549
|
-
reset,
|
|
3550
|
-
onPointerDown(e, hit, annotationId) {
|
|
3551
|
-
if (!hit) return;
|
|
3552
|
-
reset();
|
|
3553
|
-
if (annotationId !== void 0) {
|
|
3554
|
-
const a = ctx.store.get(annotationId);
|
|
3555
|
-
if (!a) return;
|
|
3556
|
-
const markup = isTextMarkup(a);
|
|
3557
|
-
if (markup && !opts.marquee) return;
|
|
3558
|
-
const handle = handleOf(e.target);
|
|
3559
|
-
if (handle !== void 0 && !a.locked && startHandle(e, hit, a, handle)) return true;
|
|
3560
|
-
const selected = ctx.store.selection.get();
|
|
3561
|
-
if (e.shiftKey) {
|
|
3562
|
-
e.stopPropagation();
|
|
3563
|
-
ctx.store.select(selected.includes(annotationId) ? selected.filter((s) => s !== annotationId) : [...selected, annotationId]);
|
|
3564
|
-
return;
|
|
3565
|
-
}
|
|
3566
|
-
const narrow = selected.length > 1 && selected.includes(annotationId);
|
|
3567
|
-
if (narrow) e.stopPropagation();
|
|
3568
|
-
else if (markup) {
|
|
3569
|
-
e.stopPropagation();
|
|
3570
|
-
ctx.store.select([annotationId]);
|
|
3571
|
-
ctx.onActivate?.(annotationId);
|
|
3572
|
-
}
|
|
3573
|
-
mode = {
|
|
3574
|
-
kind: "press",
|
|
3575
|
-
id: annotationId,
|
|
3576
|
-
start: hit,
|
|
3577
|
-
narrow
|
|
3578
|
-
};
|
|
3579
|
-
e.preventDefault();
|
|
3580
|
-
return true;
|
|
3581
|
-
}
|
|
3582
|
-
if (!opts.marquee) return;
|
|
3583
|
-
const el = hit.host.ownerDocument.createElement("div");
|
|
3584
|
-
el.className = "ink-marquee";
|
|
3585
|
-
el.dataset.inkMarquee = "";
|
|
3586
|
-
el.style.cssText = `position:absolute;pointer-events:none;left:${hit.css.x}px;top:${hit.css.y}px;width:0;height:0;`;
|
|
3587
|
-
hit.host.append(el);
|
|
3588
|
-
mode = {
|
|
3589
|
-
kind: "marquee",
|
|
3590
|
-
start: hit,
|
|
3591
|
-
el,
|
|
3592
|
-
additive: e.shiftKey
|
|
3593
|
-
};
|
|
3594
|
-
if (!e.shiftKey) ctx.store.select([]);
|
|
3595
|
-
e.preventDefault();
|
|
3596
|
-
return true;
|
|
3597
|
-
},
|
|
3598
|
-
onPointerMove(e) {
|
|
3599
|
-
if (mode.kind === "idle") return;
|
|
3600
|
-
if (e.buttons === 0 || !mode.start.host.isConnected) {
|
|
3601
|
-
reset();
|
|
3602
|
-
return;
|
|
3603
|
-
}
|
|
3604
|
-
const { start } = mode;
|
|
3605
|
-
if (mode.kind === "resize") {
|
|
3606
|
-
const { box, el, preview } = mode;
|
|
3607
|
-
const next = resizeCss(box, mode.corner, cornerAt(mode, e), minCss(start), {
|
|
3608
|
-
aspect: mode.locked || e.shiftKey,
|
|
3609
|
-
page: start.transform
|
|
3610
|
-
});
|
|
3611
|
-
if (preview) {
|
|
3612
|
-
const a = ctx.store.get(mode.id);
|
|
3613
|
-
if (a?.type !== "freetext") return;
|
|
3614
|
-
const draft = {
|
|
3615
|
-
...a,
|
|
3616
|
-
...resizedBox(a, userBox(start.transform, next))
|
|
3617
|
-
};
|
|
3618
|
-
showPreview(preview, el, draft, start);
|
|
3619
|
-
} else if (el) {
|
|
3620
|
-
el.style.transformOrigin = "0 0";
|
|
3621
|
-
el.style.transform = `translate(${next.left - box.left}px, ${next.top - box.top}px) scale(${next.width / box.width}, ${next.height / box.height})`;
|
|
3622
|
-
}
|
|
3623
|
-
return;
|
|
3624
|
-
}
|
|
3625
|
-
if (mode.kind === "endpoint") {
|
|
3626
|
-
const a = ctx.store.get(mode.id);
|
|
3627
|
-
if (!a) return;
|
|
3628
|
-
const patch = endPatch(a, mode.which, endAt(mode, e));
|
|
3629
|
-
if (!patch) return;
|
|
3630
|
-
showPreview(mode.preview, mode.el, {
|
|
3631
|
-
...a,
|
|
3632
|
-
...patch
|
|
3633
|
-
}, start);
|
|
3634
|
-
return;
|
|
3635
|
-
}
|
|
3636
|
-
const css = cssIn(start.host, e);
|
|
3637
|
-
const dx = css.x - start.css.x;
|
|
3638
|
-
const dy = css.y - start.css.y;
|
|
3639
|
-
if (mode.kind === "press") {
|
|
3640
|
-
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
3641
|
-
const a = ctx.store.get(mode.id);
|
|
3642
|
-
if (!a || a.locked) {
|
|
3643
|
-
mode = { kind: "idle" };
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
const selected = ctx.store.selection.get();
|
|
3647
|
-
const pressed = mode.id;
|
|
3648
|
-
const ids = selected.includes(pressed) ? selected.filter((id) => {
|
|
3649
|
-
const m = ctx.store.get(id);
|
|
3650
|
-
return m !== void 0 && !m.locked && (id === pressed || !isTextMarkup(m));
|
|
3651
|
-
}) : [pressed];
|
|
3652
|
-
mode = {
|
|
3653
|
-
kind: "move",
|
|
3654
|
-
ids,
|
|
3655
|
-
start,
|
|
3656
|
-
els: ids.map((id) => elementIn(ctx.root, id)).filter((el) => el !== void 0)
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
if (mode.kind === "move") {
|
|
3660
|
-
for (const el of mode.els) el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
3661
|
-
return;
|
|
5427
|
+
draft.dataset.inkDraft = "redact";
|
|
5428
|
+
draft.style.position = "absolute";
|
|
5429
|
+
draft.style.pointerEvents = "none";
|
|
5430
|
+
start.host.append(draft);
|
|
3662
5431
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
5432
|
+
draft.style.left = `${Math.min(start.css.x, css.x)}px`;
|
|
5433
|
+
draft.style.top = `${Math.min(start.css.y, css.y)}px`;
|
|
5434
|
+
draft.style.width = `${Math.abs(css.x - start.css.x)}px`;
|
|
5435
|
+
draft.style.height = `${Math.abs(css.y - start.css.y)}px`;
|
|
3667
5436
|
},
|
|
3668
5437
|
onPointerCancel() {
|
|
3669
|
-
|
|
5438
|
+
clear();
|
|
3670
5439
|
},
|
|
3671
5440
|
onPointerUp(e) {
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
case "press":
|
|
3677
|
-
if (current.narrow) {
|
|
3678
|
-
ctx.store.select([current.id]);
|
|
3679
|
-
ctx.onActivate?.(current.id);
|
|
3680
|
-
}
|
|
3681
|
-
return;
|
|
3682
|
-
case "move": {
|
|
3683
|
-
const { start } = current;
|
|
3684
|
-
const end = start.transform.fromCss(cssIn(start.host, e));
|
|
3685
|
-
const dx = end.x - start.user.x;
|
|
3686
|
-
const dy = end.y - start.user.y;
|
|
3687
|
-
batch(() => {
|
|
3688
|
-
for (const id of current.ids) {
|
|
3689
|
-
const a = ctx.store.get(id);
|
|
3690
|
-
if (a && !a.locked) ctx.store.update(id, translated(a, dx, dy));
|
|
3691
|
-
}
|
|
3692
|
-
});
|
|
3693
|
-
return;
|
|
3694
|
-
}
|
|
3695
|
-
case "resize": {
|
|
3696
|
-
const { start, box } = current;
|
|
3697
|
-
const next = resizeCss(box, current.corner, cornerAt(current, e), minCss(start), {
|
|
3698
|
-
aspect: current.locked || e.shiftKey,
|
|
3699
|
-
page: start.transform
|
|
3700
|
-
});
|
|
3701
|
-
if (next.left === box.left && next.top === box.top && next.width === box.width && next.height === box.height) return;
|
|
3702
|
-
const a = ctx.store.get(current.id);
|
|
3703
|
-
if (!a || a.locked) return;
|
|
3704
|
-
const to = userBox(start.transform, next);
|
|
3705
|
-
if (a.type === "freetext") {
|
|
3706
|
-
ctx.store.update(a.id, resizedBox(a, to));
|
|
3707
|
-
return;
|
|
3708
|
-
}
|
|
3709
|
-
const from = hasRectGeometry(a) ? a.rect : start.transform.fromCssRect(box);
|
|
3710
|
-
ctx.store.update(a.id, scaled(a, from, to));
|
|
3711
|
-
return;
|
|
3712
|
-
}
|
|
3713
|
-
case "endpoint": {
|
|
3714
|
-
const a = ctx.store.get(current.id);
|
|
3715
|
-
if (!a || a.locked) return;
|
|
3716
|
-
const css = cssIn(current.start.host, e);
|
|
3717
|
-
if (css.x === current.start.css.x && css.y === current.start.css.y) return;
|
|
3718
|
-
const patch = endPatch(a, current.which, endAt(current, e));
|
|
3719
|
-
if (patch) ctx.store.update(a.id, patch);
|
|
3720
|
-
return;
|
|
3721
|
-
}
|
|
3722
|
-
case "marquee": {
|
|
3723
|
-
const { start } = current;
|
|
3724
|
-
const box = rectFromPoints(start.user, start.transform.fromCss(cssIn(start.host, e)));
|
|
3725
|
-
if (box.width === 0 && box.height === 0) return;
|
|
3726
|
-
const ids = withoutReplies(ctx.store.list.get()).filter((a) => a.page === start.page && rectsIntersect(a.rect, box)).map((a) => a.id);
|
|
3727
|
-
ctx.store.select(current.additive ? [.../* @__PURE__ */ new Set([...ctx.store.selection.get(), ...ids])] : ids);
|
|
3728
|
-
return;
|
|
3729
|
-
}
|
|
5441
|
+
if (!ctx || !start) return;
|
|
5442
|
+
if (!start.host.isConnected) {
|
|
5443
|
+
clear();
|
|
5444
|
+
return;
|
|
3730
5445
|
}
|
|
5446
|
+
const css = clampedCssIn(start.host, e);
|
|
5447
|
+
const width = Math.abs(css.x - start.css.x);
|
|
5448
|
+
const height = Math.abs(css.y - start.css.y);
|
|
5449
|
+
if (width >= MIN_CSS$1 && height >= MIN_CSS$1) {
|
|
5450
|
+
const rect = start.transform.fromCssRect({
|
|
5451
|
+
left: Math.min(start.css.x, css.x),
|
|
5452
|
+
top: Math.min(start.css.y, css.y),
|
|
5453
|
+
width,
|
|
5454
|
+
height
|
|
5455
|
+
});
|
|
5456
|
+
const a = ctx.store.add(ctx.toolStyle.resolve("redact", {
|
|
5457
|
+
type: "redact",
|
|
5458
|
+
page: start.page,
|
|
5459
|
+
rect
|
|
5460
|
+
}));
|
|
5461
|
+
made(ctx, [a.id]);
|
|
5462
|
+
} else fromSelection();
|
|
5463
|
+
clear();
|
|
3731
5464
|
},
|
|
3732
5465
|
onKeyDown(e) {
|
|
3733
|
-
if (e.
|
|
3734
|
-
reset();
|
|
3735
|
-
ctx.store.select([]);
|
|
3736
|
-
return;
|
|
3737
|
-
}
|
|
3738
|
-
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
3739
|
-
if (e.defaultPrevented) return;
|
|
3740
|
-
if ((e.target instanceof Element ? e.target : null)?.closest("[data-annotation], input, textarea, [contenteditable]:not([contenteditable=\"false\"])")) return;
|
|
3741
|
-
for (const id of ctx.store.selection.get()) if (ctx.store.get(id)?.locked !== true) ctx.store.remove(id);
|
|
5466
|
+
if (!!ctx && isViewportEnter(e, ctx.root) && fromSelection()) e.preventDefault();
|
|
3742
5467
|
}
|
|
3743
5468
|
};
|
|
3744
5469
|
}
|
|
3745
|
-
/** The handle (`nw`, `start`, ...) a press landed on, if it landed on one. */
|
|
3746
|
-
function handleOf(target) {
|
|
3747
|
-
if (!(target instanceof Element)) return void 0;
|
|
3748
|
-
return target.closest("[data-handle]")?.getAttribute("data-handle") ?? void 0;
|
|
3749
|
-
}
|
|
3750
|
-
/** An overlay element's CSS box, from its inline geometry. */
|
|
3751
|
-
function boxOf(el) {
|
|
3752
|
-
return {
|
|
3753
|
-
left: parseFloat(el.style.left) || 0,
|
|
3754
|
-
top: parseFloat(el.style.top) || 0,
|
|
3755
|
-
width: parseFloat(el.style.width) || 0,
|
|
3756
|
-
height: parseFloat(el.style.height) || 0
|
|
3757
|
-
};
|
|
3758
|
-
}
|
|
3759
|
-
/** The point a line's end or a callout's tip handle sits on. */
|
|
3760
|
-
function endOf(a, which) {
|
|
3761
|
-
if (which === "callout") return hasCallout(a) ? a.callout[0] : void 0;
|
|
3762
|
-
return a.type === "line" ? a[which] : void 0;
|
|
3763
|
-
}
|
|
3764
|
-
/** The patch that moves a line's end, or a callout's tip, to `to`; undefined for neither. */
|
|
3765
|
-
function endPatch(a, which, to) {
|
|
3766
|
-
if (which === "callout") return hasCallout(a) ? movedTip(a, to) : void 0;
|
|
3767
|
-
return a.type === "line" ? movedEndpoint(a, which, to) : void 0;
|
|
3768
|
-
}
|
|
3769
|
-
/** A resized CSS box in user space, at least `MIN_SIZE` a side. */
|
|
3770
|
-
function userBox(t, css) {
|
|
3771
|
-
const r = t.fromCssRect(css);
|
|
3772
|
-
return {
|
|
3773
|
-
...r,
|
|
3774
|
-
width: Math.max(1, r.width),
|
|
3775
|
-
height: Math.max(1, r.height)
|
|
3776
|
-
};
|
|
3777
|
-
}
|
|
3778
|
-
/** Draws `a` into the preview element at its own box. */
|
|
3779
|
-
function drawPreview(el, a, start) {
|
|
3780
|
-
const box = annotationBox(a, start.transform);
|
|
3781
|
-
el.style.left = `${box.left}px`;
|
|
3782
|
-
el.style.top = `${box.top}px`;
|
|
3783
|
-
el.style.width = `${box.width}px`;
|
|
3784
|
-
el.style.height = `${box.height}px`;
|
|
3785
|
-
renderAnnotationContent(el, a, start.transform);
|
|
3786
|
-
}
|
|
3787
|
-
/** Keeps a CSS point on the page. */
|
|
3788
|
-
function onPage(start, x, y) {
|
|
3789
|
-
const { cssWidth, cssHeight } = start.transform;
|
|
3790
|
-
return {
|
|
3791
|
-
x: Math.min(Math.max(x, 0), cssWidth),
|
|
3792
|
-
y: Math.min(Math.max(y, 0), cssHeight)
|
|
3793
|
-
};
|
|
3794
|
-
}
|
|
3795
|
-
/**
|
|
3796
|
-
* Where the dragged line end is now, in user space: where it started plus the pointer's
|
|
3797
|
-
* travel, kept on the page.
|
|
3798
|
-
*/
|
|
3799
|
-
function endAt(mode, e) {
|
|
3800
|
-
const { start, from } = mode;
|
|
3801
|
-
const css = cssIn(start.host, e);
|
|
3802
|
-
return start.transform.fromCss(onPage(start, from.x + css.x - start.css.x, from.y + css.y - start.css.y));
|
|
3803
|
-
}
|
|
3804
|
-
/**
|
|
3805
|
-
* Where the dragged corner is now: where it started plus the pointer's travel since the
|
|
3806
|
-
* press, kept on the page. Keeping the press's offset from the corner means a click on a
|
|
3807
|
-
* handle, with no drag, leaves the box as it was.
|
|
3808
|
-
*/
|
|
3809
|
-
function cornerAt(mode, e) {
|
|
3810
|
-
const { start, box, corner } = mode;
|
|
3811
|
-
const css = cssIn(start.host, e);
|
|
3812
|
-
return onPage(start, (corner[1] === "w" ? box.left : box.left + box.width) + css.x - start.css.x, (corner[0] === "n" ? box.top : box.top + box.height) + css.y - start.css.y);
|
|
3813
|
-
}
|
|
3814
|
-
/** `MIN_SIZE` points as CSS px on the press's page. */
|
|
3815
|
-
function minCss(start) {
|
|
3816
|
-
return start.transform.toCssLength(1);
|
|
3817
|
-
}
|
|
3818
|
-
/**
|
|
3819
|
-
* `box` with its `corner` dragged to `p` and the opposite corner fixed, at least `min` a side.
|
|
3820
|
-
* With `aspect`, the box keeps its proportions: it scales by the larger of the two axes'
|
|
3821
|
-
* changes, kept on the page.
|
|
3822
|
-
*/
|
|
3823
|
-
function resizeCss(box, corner, p, min, opts) {
|
|
3824
|
-
const west = corner[1] === "w";
|
|
3825
|
-
const north = corner[0] === "n";
|
|
3826
|
-
/** The fixed corner. */
|
|
3827
|
-
const fx = west ? box.left + box.width : box.left;
|
|
3828
|
-
const fy = north ? box.top + box.height : box.top;
|
|
3829
|
-
let width = Math.max(west ? fx - p.x : p.x - fx, min);
|
|
3830
|
-
let height = Math.max(north ? fy - p.y : p.y - fy, min);
|
|
3831
|
-
if (opts.aspect && box.width > 0 && box.height > 0) {
|
|
3832
|
-
const roomX = west ? fx : opts.page.cssWidth - fx;
|
|
3833
|
-
const roomY = north ? fy : opts.page.cssHeight - fy;
|
|
3834
|
-
const lo = min / Math.min(box.width, box.height);
|
|
3835
|
-
const hi = Math.min(roomX / box.width, roomY / box.height);
|
|
3836
|
-
const s = Math.min(Math.max(width / box.width, height / box.height, lo), Math.max(hi, lo));
|
|
3837
|
-
width = box.width * s;
|
|
3838
|
-
height = box.height * s;
|
|
3839
|
-
}
|
|
3840
|
-
return {
|
|
3841
|
-
left: west ? fx - width : fx,
|
|
3842
|
-
top: north ? fy - height : fy,
|
|
3843
|
-
width,
|
|
3844
|
-
height
|
|
3845
|
-
};
|
|
3846
|
-
}
|
|
3847
5470
|
//#endregion
|
|
3848
5471
|
//#region src/dom/tools/select.ts
|
|
3849
5472
|
/**
|
|
@@ -3921,6 +5544,15 @@ function shapeTool() {
|
|
|
3921
5544
|
};
|
|
3922
5545
|
/** The input with the shape group's style and, for a bubble, the text group's text style. */
|
|
3923
5546
|
const styled = (c, input) => {
|
|
5547
|
+
if (input.type === "line") {
|
|
5548
|
+
const line = c.toolStyle.resolve("shape", input);
|
|
5549
|
+
const arrow = input.intent === "LineArrow";
|
|
5550
|
+
return {
|
|
5551
|
+
...line,
|
|
5552
|
+
head: line.head ?? "None",
|
|
5553
|
+
tail: line.tail ?? (arrow ? "OpenArrow" : "None")
|
|
5554
|
+
};
|
|
5555
|
+
}
|
|
3924
5556
|
if (input.type !== "freetext") return c.toolStyle.resolve("shape", input);
|
|
3925
5557
|
const shape = c.toolStyle.resolve("shape", {
|
|
3926
5558
|
...input,
|
|
@@ -3955,7 +5587,9 @@ function shapeTool() {
|
|
|
3955
5587
|
let waiting;
|
|
3956
5588
|
const openEditor = (c, a) => {
|
|
3957
5589
|
const el = c.root.querySelector(`[data-annotation="${CSS.escape(a.id)}"]`);
|
|
3958
|
-
return el !== null && editFreeTextIn(c.store, el, a.id, c.viewport.transformFor(a.page))
|
|
5590
|
+
return el !== null && editFreeTextIn(c.store, el, a.id, c.viewport.transformFor(a.page), { onClose: (how) => {
|
|
5591
|
+
if (how === "escape" && ctx === c) c.done();
|
|
5592
|
+
} }) !== void 0;
|
|
3959
5593
|
};
|
|
3960
5594
|
/**
|
|
3961
5595
|
* Opens the in-place editor on a new bubble. When a controlled host has not applied it yet,
|
|
@@ -3977,6 +5611,7 @@ function shapeTool() {
|
|
|
3977
5611
|
if (!input) return;
|
|
3978
5612
|
const created = c.store.add(styled(c, input));
|
|
3979
5613
|
c.store.select([created.id]);
|
|
5614
|
+
c.made([created.id]);
|
|
3980
5615
|
if (isBubble(created)) edit(c, created);
|
|
3981
5616
|
};
|
|
3982
5617
|
const paint = (c, from, e) => {
|
|
@@ -4045,6 +5680,7 @@ function shapeTool() {
|
|
|
4045
5680
|
};
|
|
4046
5681
|
return {
|
|
4047
5682
|
id: "shape",
|
|
5683
|
+
kind: "one-shot",
|
|
4048
5684
|
cursor: "crosshair",
|
|
4049
5685
|
styleGroup: "shape",
|
|
4050
5686
|
defaultOptions: { ...DEFAULT_SHAPE_OPTIONS },
|
|
@@ -4114,6 +5750,7 @@ function signatureTool() {
|
|
|
4114
5750
|
const input = placeSignature(t, page, at, 150, rotation);
|
|
4115
5751
|
const a = c.store.add(input.type === "ink" ? c.toolStyle.resolve("ink", input) : input);
|
|
4116
5752
|
c.store.select([a.id]);
|
|
5753
|
+
c.made([a.id]);
|
|
4117
5754
|
};
|
|
4118
5755
|
/**
|
|
4119
5756
|
* Places the selected template, from the library at once, or after reloading it when the
|
|
@@ -4137,6 +5774,7 @@ function signatureTool() {
|
|
|
4137
5774
|
};
|
|
4138
5775
|
return {
|
|
4139
5776
|
id: "signature",
|
|
5777
|
+
kind: "one-shot",
|
|
4140
5778
|
cursor: "copy",
|
|
4141
5779
|
defaultOptions: {},
|
|
4142
5780
|
activate(c) {
|
|
@@ -4647,9 +6285,11 @@ const COLOR_LABELS = {
|
|
|
4647
6285
|
const COLOR_COLUMNS = 12;
|
|
4648
6286
|
/**
|
|
4649
6287
|
* What a colour menu shows for a style: the palette with the matching swatch checked, "No fill"
|
|
4650
|
-
* for `fill`, and the trigger's swatch. Shared by the bindings and the vanilla menu.
|
|
6288
|
+
* for `fill`, and the trigger's swatch. Shared by the bindings and the vanilla menu. `fallback`
|
|
6289
|
+
* is the colour new annotations get while the style leaves it unset: the trigger shows it
|
|
6290
|
+
* rather than an empty swatch.
|
|
4651
6291
|
*/
|
|
4652
|
-
function colorMenuModel(property, current, supported) {
|
|
6292
|
+
function colorMenuModel(property, current, supported, fallback) {
|
|
4653
6293
|
const value = current[property];
|
|
4654
6294
|
const enabled = supported.has(property);
|
|
4655
6295
|
const color = typeof value === "string" && value !== "mixed" ? value.toUpperCase() : void 0;
|
|
@@ -4662,6 +6302,9 @@ function colorMenuModel(property, current, supported) {
|
|
|
4662
6302
|
const trigger = !enabled ? { kind: "empty" } : value === "mixed" ? { kind: "mixed" } : value === null ? { kind: "none" } : typeof value === "string" ? {
|
|
4663
6303
|
kind: "color",
|
|
4664
6304
|
color: value
|
|
6305
|
+
} : fallback !== void 0 ? {
|
|
6306
|
+
kind: "color",
|
|
6307
|
+
color: fallback
|
|
4665
6308
|
} : { kind: "empty" };
|
|
4666
6309
|
return {
|
|
4667
6310
|
label: COLOR_LABELS[property],
|
|
@@ -4679,7 +6322,7 @@ function colorMenuModel(property, current, supported) {
|
|
|
4679
6322
|
checked: value === null,
|
|
4680
6323
|
patch: { fill: null }
|
|
4681
6324
|
} : null,
|
|
4682
|
-
customValue: color ?? "#000000",
|
|
6325
|
+
customValue: color ?? fallback?.toUpperCase() ?? "#000000",
|
|
4683
6326
|
custom: (hex) => patchFor(hex.toUpperCase())
|
|
4684
6327
|
};
|
|
4685
6328
|
}
|
|
@@ -4955,7 +6598,7 @@ function handleTabKeys(e, tablist) {
|
|
|
4955
6598
|
tabs[next].focus({ preventScroll: true });
|
|
4956
6599
|
tabs[next].click();
|
|
4957
6600
|
}
|
|
4958
|
-
const TABBABLE
|
|
6601
|
+
const TABBABLE = [
|
|
4959
6602
|
"a[href]",
|
|
4960
6603
|
"button:not(:disabled)",
|
|
4961
6604
|
"input:not(:disabled):not([type=\"hidden\"])",
|
|
@@ -4964,104 +6607,26 @@ const TABBABLE$1 = [
|
|
|
4964
6607
|
"[tabindex]"
|
|
4965
6608
|
].map((s) => `${s}:not([tabindex="-1"])`).join(", ");
|
|
4966
6609
|
/**
|
|
4967
|
-
* Keeps Tab inside `container`, a modal: Tab from its last stop goes to its first, and
|
|
4968
|
-
* Shift+Tab from the first to the last.
|
|
4969
|
-
*/
|
|
4970
|
-
function trapTab(e, container) {
|
|
4971
|
-
if (e.key !== "Tab" || e.defaultPrevented) return;
|
|
4972
|
-
const stops = [...container.querySelectorAll(TABBABLE$1)].filter((el) => !el.matches(":disabled") && el.getClientRects().length > 0);
|
|
4973
|
-
if (stops.length === 0) {
|
|
4974
|
-
e.preventDefault();
|
|
4975
|
-
return;
|
|
4976
|
-
}
|
|
4977
|
-
const first = stops[0];
|
|
4978
|
-
const last = stops[stops.length - 1];
|
|
4979
|
-
const active = document.activeElement;
|
|
4980
|
-
if (e.shiftKey && (active === first || !container.contains(active))) {
|
|
4981
|
-
e.preventDefault();
|
|
4982
|
-
last.focus();
|
|
4983
|
-
} else if (!e.shiftKey && (active === last || !container.contains(active))) {
|
|
4984
|
-
e.preventDefault();
|
|
4985
|
-
first.focus();
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
//#endregion
|
|
4989
|
-
//#region src/dom/annotation-element.ts
|
|
4990
|
-
/** The overlay element drawn for annotation `id` under `root`, if it is in the DOM. */
|
|
4991
|
-
function annotationElementFor(root, id) {
|
|
4992
|
-
return root.querySelector(`[data-annotation="${CSS.escape(id)}"]`);
|
|
4993
|
-
}
|
|
4994
|
-
/**
|
|
4995
|
-
* Resolves the overlay element for `id` and keeps resolving while `root`'s subtree changes:
|
|
4996
|
-
* the overlay creates and removes these elements outside any framework (a page mounts as it
|
|
4997
|
-
* scrolls into view), so a popover anchored to one has to follow. Calls `onChange` once now
|
|
4998
|
-
* and again whenever the answer changes. The returned function stops observing.
|
|
4999
|
-
*/
|
|
5000
|
-
function observeAnnotationElement(root, id, onChange) {
|
|
5001
|
-
let last;
|
|
5002
|
-
const resolve = () => {
|
|
5003
|
-
const found = annotationElementFor(root, id);
|
|
5004
|
-
if (found === last) return;
|
|
5005
|
-
last = found;
|
|
5006
|
-
onChange(found);
|
|
5007
|
-
};
|
|
5008
|
-
resolve();
|
|
5009
|
-
const observer = new MutationObserver(resolve);
|
|
5010
|
-
observer.observe(root, {
|
|
5011
|
-
childList: true,
|
|
5012
|
-
subtree: true
|
|
5013
|
-
});
|
|
5014
|
-
return () => observer.disconnect();
|
|
5015
|
-
}
|
|
5016
|
-
//#endregion
|
|
5017
|
-
//#region src/dom/popover-focus.ts
|
|
5018
|
-
/**
|
|
5019
|
-
* A press that will activate an annotation is not a dismissal: the popover follows the
|
|
5020
|
-
* activation. Mirrors the overlay's rule: a press on the annotation element or on its drawn
|
|
5021
|
-
* shape activates; a press on a custom element's own content belongs to that content.
|
|
6610
|
+
* Keeps Tab inside `container`, a modal: Tab from its last stop goes to its first, and
|
|
6611
|
+
* Shift+Tab from the first to the last.
|
|
5022
6612
|
*/
|
|
5023
|
-
function
|
|
5024
|
-
if (
|
|
5025
|
-
const
|
|
5026
|
-
if (
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
const after = siblings.slice(i + 1);
|
|
5041
|
-
const preferred = prefer ? after.filter((el) => el.matches(prefer)) : [];
|
|
5042
|
-
fallbacks.push(...preferred, ...after, ...siblings.slice(0, i).reverse());
|
|
5043
|
-
}
|
|
5044
|
-
},
|
|
5045
|
-
onCloseAutoFocus(event, anchor) {
|
|
5046
|
-
event.preventDefault();
|
|
5047
|
-
const plan = fallbacks;
|
|
5048
|
-
const removed = removing;
|
|
5049
|
-
fallbacks = [];
|
|
5050
|
-
removing = false;
|
|
5051
|
-
const root = opts.root();
|
|
5052
|
-
if (!root) return;
|
|
5053
|
-
const activated = opts.activated();
|
|
5054
|
-
if (activated !== null && annotationElementFor(root, activated)) return;
|
|
5055
|
-
const active = root.ownerDocument.activeElement;
|
|
5056
|
-
if (active && active !== root && root.contains(active)) return;
|
|
5057
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5058
|
-
(removed ? [...plan, scroll] : [
|
|
5059
|
-
anchor,
|
|
5060
|
-
...plan,
|
|
5061
|
-
scroll
|
|
5062
|
-
]).find((el) => el?.isConnected)?.focus({ preventScroll: true });
|
|
5063
|
-
}
|
|
5064
|
-
};
|
|
6613
|
+
function trapTab(e, container) {
|
|
6614
|
+
if (e.key !== "Tab" || e.defaultPrevented) return;
|
|
6615
|
+
const stops = [...container.querySelectorAll(TABBABLE)].filter((el) => !el.matches(":disabled") && el.getClientRects().length > 0);
|
|
6616
|
+
if (stops.length === 0) {
|
|
6617
|
+
e.preventDefault();
|
|
6618
|
+
return;
|
|
6619
|
+
}
|
|
6620
|
+
const first = stops[0];
|
|
6621
|
+
const last = stops[stops.length - 1];
|
|
6622
|
+
const active = document.activeElement;
|
|
6623
|
+
if (e.shiftKey && (active === first || !container.contains(active))) {
|
|
6624
|
+
e.preventDefault();
|
|
6625
|
+
last.focus();
|
|
6626
|
+
} else if (!e.shiftKey && (active === last || !container.contains(active))) {
|
|
6627
|
+
e.preventDefault();
|
|
6628
|
+
first.focus();
|
|
6629
|
+
}
|
|
5065
6630
|
}
|
|
5066
6631
|
//#endregion
|
|
5067
6632
|
//#region src/dom/viewer-engine.ts
|
|
@@ -5076,7 +6641,15 @@ function mountViewer(opts) {
|
|
|
5076
6641
|
let stopKeys = () => void 0;
|
|
5077
6642
|
let stopActive = () => void 0;
|
|
5078
6643
|
let destroyed = false;
|
|
5079
|
-
const
|
|
6644
|
+
const plugins = opts.plugins;
|
|
6645
|
+
const hostTools = [...opts.tools ?? [], ...plugins?.tools ?? []];
|
|
6646
|
+
/** Plugin layers follow the host for the engine's life, across rebuilds. */
|
|
6647
|
+
const layers = plugins && createPluginLayers(plugins);
|
|
6648
|
+
const decorate = plugins ? (a) => plugins.decorate(a) : (a) => opts.decorateAnnotation?.(a);
|
|
6649
|
+
/** A plugin's `ctx.redecorate()` reaches every page this engine has mounted. */
|
|
6650
|
+
const stopRedecorate = plugins?.watchRedecorate(() => {
|
|
6651
|
+
for (const mount of pages.values()) mount.overlay.redecorate();
|
|
6652
|
+
});
|
|
5080
6653
|
/** Option values outlive rebuilds: every controller shares this registry. */
|
|
5081
6654
|
const toolOptions = opts.toolOptions ?? createToolOptions();
|
|
5082
6655
|
/** Without a host style, the engine's own follows the active tool across rebuilds. */
|
|
@@ -5105,6 +6678,8 @@ function mountViewer(opts) {
|
|
|
5105
6678
|
store: doc.annotations,
|
|
5106
6679
|
onActivate: (id) => opts.onActivate?.(id),
|
|
5107
6680
|
isCustom: () => isCustom,
|
|
6681
|
+
decorate,
|
|
6682
|
+
...layers ? { layers } : {},
|
|
5108
6683
|
onError: (cause, layer) => {
|
|
5109
6684
|
failed.add(layer);
|
|
5110
6685
|
if (error) return;
|
|
@@ -5281,232 +6856,20 @@ function mountViewer(opts) {
|
|
|
5281
6856
|
if (next.customAnnotations !== void 0) custom = next.customAnnotations;
|
|
5282
6857
|
build(state);
|
|
5283
6858
|
},
|
|
6859
|
+
redecorate() {
|
|
6860
|
+
if (destroyed) return;
|
|
6861
|
+
for (const mount of pages.values()) mount.overlay.redecorate();
|
|
6862
|
+
},
|
|
5284
6863
|
destroy() {
|
|
5285
6864
|
if (destroyed) return;
|
|
5286
6865
|
destroyed = true;
|
|
5287
6866
|
teardown();
|
|
6867
|
+
layers?.destroy();
|
|
6868
|
+
stopRedecorate?.();
|
|
5288
6869
|
}
|
|
5289
6870
|
};
|
|
5290
6871
|
}
|
|
5291
6872
|
//#endregion
|
|
5292
|
-
//#region src/dom/viewer/dom.ts
|
|
5293
|
-
/** `document.createElement` with attributes and children. */
|
|
5294
|
-
function h(tag, attrs = {}, ...children) {
|
|
5295
|
-
const el = document.createElement(tag);
|
|
5296
|
-
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
5297
|
-
el.append(...children);
|
|
5298
|
-
return el;
|
|
5299
|
-
}
|
|
5300
|
-
function button$1(label, onClick) {
|
|
5301
|
-
const b = h("button", { type: "button" }, label);
|
|
5302
|
-
b.addEventListener("click", onClick);
|
|
5303
|
-
return b;
|
|
5304
|
-
}
|
|
5305
|
-
//#endregion
|
|
5306
|
-
//#region src/dom/viewer/popover.ts
|
|
5307
|
-
/** Space between the anchor and the panel, and between the panel and the window edge. */
|
|
5308
|
-
const GAP = 6;
|
|
5309
|
-
const MARGIN = 8;
|
|
5310
|
-
const TABBABLE = "button, input, select, textarea, a[href], [tabindex]";
|
|
5311
|
-
/**
|
|
5312
|
-
* Fixed-positions `panel` below `anchor` (above it when only that fits), clamped to the
|
|
5313
|
-
* window. Shared by the annotation popovers and the trigger-anchored menus.
|
|
5314
|
-
*/
|
|
5315
|
-
function placePanel(panel, anchor) {
|
|
5316
|
-
const { innerWidth: vw, innerHeight: vh } = window;
|
|
5317
|
-
const w = panel.offsetWidth;
|
|
5318
|
-
const ht = panel.offsetHeight;
|
|
5319
|
-
let top = anchor.bottom + GAP;
|
|
5320
|
-
if (top + ht > vh - MARGIN && anchor.top - GAP - ht >= MARGIN) top = anchor.top - GAP - ht;
|
|
5321
|
-
top = Math.max(MARGIN, Math.min(top, vh - ht - MARGIN));
|
|
5322
|
-
const left = Math.max(MARGIN, Math.min(anchor.left, vw - w - MARGIN));
|
|
5323
|
-
panel.style.position = "fixed";
|
|
5324
|
-
panel.style.left = `${left}px`;
|
|
5325
|
-
panel.style.top = `${top}px`;
|
|
5326
|
-
}
|
|
5327
|
-
function createPopover(opts) {
|
|
5328
|
-
const { root, focusReturn } = opts;
|
|
5329
|
-
const panel = h("div", {
|
|
5330
|
-
...opts.attrs,
|
|
5331
|
-
role: "dialog",
|
|
5332
|
-
hidden: ""
|
|
5333
|
-
});
|
|
5334
|
-
document.body.append(panel);
|
|
5335
|
-
let anchoredId = null;
|
|
5336
|
-
let anchor = null;
|
|
5337
|
-
let stopObserving = () => void 0;
|
|
5338
|
-
/** The element that had focus in the panel when the anchor went away. */
|
|
5339
|
-
let stranded = null;
|
|
5340
|
-
const place = () => {
|
|
5341
|
-
if (!anchor || panel.hidden) return;
|
|
5342
|
-
placePanel(panel, anchor.getBoundingClientRect());
|
|
5343
|
-
};
|
|
5344
|
-
const resizes = new ResizeObserver(place);
|
|
5345
|
-
resizes.observe(panel);
|
|
5346
|
-
const onAnchor = (el, closing = false) => {
|
|
5347
|
-
if (anchor) resizes.unobserve(anchor);
|
|
5348
|
-
anchor = el;
|
|
5349
|
-
if (!el) {
|
|
5350
|
-
const active = document.activeElement;
|
|
5351
|
-
if (!closing && !panel.hidden && active instanceof HTMLElement && panel.contains(active)) {
|
|
5352
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5353
|
-
if (scroll) {
|
|
5354
|
-
stranded = active;
|
|
5355
|
-
scroll.focus({ preventScroll: true });
|
|
5356
|
-
}
|
|
5357
|
-
}
|
|
5358
|
-
panel.hidden = true;
|
|
5359
|
-
return;
|
|
5360
|
-
}
|
|
5361
|
-
resizes.observe(el);
|
|
5362
|
-
const wasHidden = panel.hidden;
|
|
5363
|
-
panel.hidden = false;
|
|
5364
|
-
place();
|
|
5365
|
-
if (wasHidden && stranded) {
|
|
5366
|
-
const back = stranded;
|
|
5367
|
-
stranded = null;
|
|
5368
|
-
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5369
|
-
if (panel.contains(back) && document.activeElement === scroll) back.focus({ preventScroll: true });
|
|
5370
|
-
}
|
|
5371
|
-
};
|
|
5372
|
-
const onKey = (e) => {
|
|
5373
|
-
if (e.key !== "Escape" || panel.hidden || e.defaultPrevented) return;
|
|
5374
|
-
const target = e.target;
|
|
5375
|
-
if (!(target instanceof Node) || !(panel.contains(target) || root.contains(target))) return;
|
|
5376
|
-
e.preventDefault();
|
|
5377
|
-
opts.onDismiss();
|
|
5378
|
-
};
|
|
5379
|
-
const onPress = (e) => {
|
|
5380
|
-
if (panel.hidden || panel.contains(e.target)) return;
|
|
5381
|
-
if (shouldKeepOpenOnAnnotation(e.target)) return;
|
|
5382
|
-
opts.onDismiss();
|
|
5383
|
-
};
|
|
5384
|
-
const onTab = (e) => {
|
|
5385
|
-
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
5386
|
-
const tabbable = [...panel.querySelectorAll(TABBABLE)].filter((el) => !el.disabled && el.tabIndex >= 0);
|
|
5387
|
-
const first = tabbable[0];
|
|
5388
|
-
const last = tabbable.at(-1);
|
|
5389
|
-
if (!first || !last) return;
|
|
5390
|
-
const active = document.activeElement;
|
|
5391
|
-
if (e.shiftKey ? active !== first : active !== last) return;
|
|
5392
|
-
e.preventDefault();
|
|
5393
|
-
(e.shiftKey ? last : first).focus();
|
|
5394
|
-
};
|
|
5395
|
-
panel.addEventListener("keydown", onTab);
|
|
5396
|
-
document.addEventListener("keydown", onKey);
|
|
5397
|
-
document.addEventListener("pointerdown", onPress, true);
|
|
5398
|
-
document.addEventListener("scroll", place, true);
|
|
5399
|
-
window.addEventListener("resize", place);
|
|
5400
|
-
return {
|
|
5401
|
-
panel,
|
|
5402
|
-
get anchor() {
|
|
5403
|
-
return anchor;
|
|
5404
|
-
},
|
|
5405
|
-
show(id) {
|
|
5406
|
-
if (id === anchoredId) return;
|
|
5407
|
-
stopObserving();
|
|
5408
|
-
anchoredId = id;
|
|
5409
|
-
stopObserving = observeAnnotationElement(root, id, onAnchor);
|
|
5410
|
-
},
|
|
5411
|
-
hide() {
|
|
5412
|
-
if (anchoredId === null) return;
|
|
5413
|
-
stopObserving();
|
|
5414
|
-
stopObserving = () => void 0;
|
|
5415
|
-
anchoredId = null;
|
|
5416
|
-
const back = anchor;
|
|
5417
|
-
stranded = null;
|
|
5418
|
-
onAnchor(null, true);
|
|
5419
|
-
queueMicrotask(() => {
|
|
5420
|
-
if (anchoredId !== null || !panel.isConnected) return;
|
|
5421
|
-
focusReturn.onCloseAutoFocus(new Event("closeautofocus", { cancelable: true }), back);
|
|
5422
|
-
});
|
|
5423
|
-
},
|
|
5424
|
-
destroy() {
|
|
5425
|
-
stopObserving();
|
|
5426
|
-
resizes.disconnect();
|
|
5427
|
-
document.removeEventListener("keydown", onKey);
|
|
5428
|
-
document.removeEventListener("pointerdown", onPress, true);
|
|
5429
|
-
panel.removeEventListener("keydown", onTab);
|
|
5430
|
-
document.removeEventListener("scroll", place, true);
|
|
5431
|
-
window.removeEventListener("resize", place);
|
|
5432
|
-
panel.remove();
|
|
5433
|
-
}
|
|
5434
|
-
};
|
|
5435
|
-
}
|
|
5436
|
-
//#endregion
|
|
5437
|
-
//#region src/dom/viewer/suggestion-card.ts
|
|
5438
|
-
function createSuggestionCard(opts) {
|
|
5439
|
-
const { root, store, activated, focusReturn } = opts;
|
|
5440
|
-
const popover = createPopover({
|
|
5441
|
-
root,
|
|
5442
|
-
attrs: {
|
|
5443
|
-
class: "ink-suggestion-card",
|
|
5444
|
-
"data-ink-suggestion-card": "",
|
|
5445
|
-
"aria-label": "Suggested redaction"
|
|
5446
|
-
},
|
|
5447
|
-
focusReturn,
|
|
5448
|
-
onDismiss: close
|
|
5449
|
-
});
|
|
5450
|
-
/** The suggestion as last rendered. */
|
|
5451
|
-
let shown = null;
|
|
5452
|
-
function suggestion(id) {
|
|
5453
|
-
const a = id ? store.get(id) : void 0;
|
|
5454
|
-
return a?.type === "redact" && a.origin === "suggested" ? a : null;
|
|
5455
|
-
}
|
|
5456
|
-
function target() {
|
|
5457
|
-
const selection = store.selection.get();
|
|
5458
|
-
return suggestion(activated.get()) ?? (selection.length === 1 ? suggestion(selection[0]) : null);
|
|
5459
|
-
}
|
|
5460
|
-
function update() {
|
|
5461
|
-
const t = target();
|
|
5462
|
-
if (t === shown) return;
|
|
5463
|
-
const previous = shown;
|
|
5464
|
-
shown = t;
|
|
5465
|
-
if (!t) {
|
|
5466
|
-
popover.hide();
|
|
5467
|
-
return;
|
|
5468
|
-
}
|
|
5469
|
-
const hadFocus = popover.panel.contains(document.activeElement);
|
|
5470
|
-
const opening = previous === null || previous.id !== t.id;
|
|
5471
|
-
render(t);
|
|
5472
|
-
popover.show(t.id);
|
|
5473
|
-
if ((opening || hadFocus) && !popover.panel.hidden) popover.panel.querySelector("button")?.focus();
|
|
5474
|
-
}
|
|
5475
|
-
function render(a) {
|
|
5476
|
-
const { suggestion: s } = a;
|
|
5477
|
-
const bucket = s ? confidenceBucket(s.confidence) : void 0;
|
|
5478
|
-
popover.panel.replaceChildren(h("p", { class: "ink-suggestion-reason" }, h("strong", {}, s?.reason ?? "Suggested redaction"), ...bucket ? [h("span", {
|
|
5479
|
-
class: "ink-suggestion-confidence",
|
|
5480
|
-
"data-confidence": bucket
|
|
5481
|
-
}, `${bucket} confidence`)] : []), ...s?.explanation ? [h("p", { class: "ink-suggestion-explanation" }, s.explanation)] : [], h("div", { class: "ink-suggestion-actions" }, button$1("Accept", accept), ...a.locked ? [] : [button$1("Dismiss", dismiss)]));
|
|
5482
|
-
}
|
|
5483
|
-
function close() {
|
|
5484
|
-
activated.set(null);
|
|
5485
|
-
store.select([]);
|
|
5486
|
-
}
|
|
5487
|
-
function accept() {
|
|
5488
|
-
if (shown) acceptSuggestion(store, shown.id);
|
|
5489
|
-
close();
|
|
5490
|
-
}
|
|
5491
|
-
/** Locked annotations cannot be removed: Dismiss is not offered for them. */
|
|
5492
|
-
function dismiss() {
|
|
5493
|
-
if (!shown || shown.locked) return;
|
|
5494
|
-
focusReturn.beforeRemove(popover.anchor, "[data-origin=\"suggested\"]");
|
|
5495
|
-
store.remove(shown.id);
|
|
5496
|
-
close();
|
|
5497
|
-
}
|
|
5498
|
-
const stops = [
|
|
5499
|
-
store.list.subscribe(update),
|
|
5500
|
-
store.selection.subscribe(update),
|
|
5501
|
-
activated.subscribe(update)
|
|
5502
|
-
];
|
|
5503
|
-
update();
|
|
5504
|
-
return { destroy() {
|
|
5505
|
-
for (const stop of stops) stop();
|
|
5506
|
-
popover.destroy();
|
|
5507
|
-
} };
|
|
5508
|
-
}
|
|
5509
|
-
//#endregion
|
|
5510
6873
|
//#region src/dom/viewer/thread-popover.ts
|
|
5511
6874
|
function createThreadPopover(opts) {
|
|
5512
6875
|
const { root, store, activated, focusReturn } = opts;
|
|
@@ -5675,8 +7038,14 @@ function createThreadForm(n, actions) {
|
|
|
5675
7038
|
}
|
|
5676
7039
|
//#endregion
|
|
5677
7040
|
//#region src/dom/viewer/create-viewer.ts
|
|
7041
|
+
const noop = () => void 0;
|
|
7042
|
+
const passThrough = {
|
|
7043
|
+
batch: (fn) => fn(),
|
|
7044
|
+
track: () => noop,
|
|
7045
|
+
react: () => noop
|
|
7046
|
+
};
|
|
5678
7047
|
function createViewer(el, opts) {
|
|
5679
|
-
const { source: initial, assets, password, defaults, history, threads: withThreads = true,
|
|
7048
|
+
const { source: initial, assets, password, defaults, history, plugins, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, signatures: signatureStore, ...view } = opts;
|
|
5680
7049
|
/** One library for the viewer's life, so templates and the menu outlive each document. */
|
|
5681
7050
|
const signatures = createSignatureLibrary(signatureStore ?? createMemorySignatureStore());
|
|
5682
7051
|
/** Tool options persist for the viewer's life; the tool style is per document. */
|
|
@@ -5686,8 +7055,9 @@ function createViewer(el, opts) {
|
|
|
5686
7055
|
const engine = writable(null);
|
|
5687
7056
|
const viewport = writable(null);
|
|
5688
7057
|
const tools = writable(null);
|
|
5689
|
-
/** The activated annotation: the note thread
|
|
7058
|
+
/** The activated annotation: the note thread opens from it. */
|
|
5690
7059
|
const activated = writable(null);
|
|
7060
|
+
const rootEl = writable(null);
|
|
5691
7061
|
const root = h("div", {
|
|
5692
7062
|
"data-ink-root": "",
|
|
5693
7063
|
"data-state": "idle"
|
|
@@ -5706,6 +7076,24 @@ function createViewer(el, opts) {
|
|
|
5706
7076
|
}, scroll);
|
|
5707
7077
|
root.append(viewportRoot);
|
|
5708
7078
|
el.append(root);
|
|
7079
|
+
rootEl.set(scroll);
|
|
7080
|
+
/**
|
|
7081
|
+
* One host for the viewer's life. It reads the history when a document opens, and `current`
|
|
7082
|
+
* is set before it notifies, so that is the new document's. With no document open, a
|
|
7083
|
+
* command's `batch` just runs its function.
|
|
7084
|
+
*/
|
|
7085
|
+
const host = createPluginHost({
|
|
7086
|
+
plugins: plugins ?? [],
|
|
7087
|
+
document: readonly(current),
|
|
7088
|
+
history: () => current.get()?.history ?? passThrough,
|
|
7089
|
+
activated: readonly(activated),
|
|
7090
|
+
setActivated: (id) => {
|
|
7091
|
+
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
7092
|
+
},
|
|
7093
|
+
root: readonly(rootEl),
|
|
7094
|
+
decorate: () => view.decorateAnnotation,
|
|
7095
|
+
viewport: () => viewport.get()
|
|
7096
|
+
});
|
|
5709
7097
|
const focusReturn = createFocusReturn({
|
|
5710
7098
|
root: () => root,
|
|
5711
7099
|
activated: () => activated.get()
|
|
@@ -5734,20 +7122,15 @@ function createViewer(el, opts) {
|
|
|
5734
7122
|
renderStatus(s);
|
|
5735
7123
|
status.set(s);
|
|
5736
7124
|
}
|
|
5737
|
-
/** Mounts the engine and the
|
|
7125
|
+
/** Mounts the engine and the thread popover for a ready document; returns their teardown. */
|
|
5738
7126
|
function start(doc) {
|
|
5739
7127
|
const store = doc.annotations;
|
|
5740
|
-
const
|
|
5741
|
-
root,
|
|
5742
|
-
store,
|
|
5743
|
-
activated,
|
|
5744
|
-
focusReturn
|
|
5745
|
-
}) : null, withSuggestions ? createSuggestionCard({
|
|
7128
|
+
const threads = withThreads ? createThreadPopover({
|
|
5746
7129
|
root,
|
|
5747
7130
|
store,
|
|
5748
7131
|
activated,
|
|
5749
7132
|
focusReturn
|
|
5750
|
-
}) : null
|
|
7133
|
+
}) : null;
|
|
5751
7134
|
const stopList = store.list.subscribe(() => {
|
|
5752
7135
|
const id = activated.get();
|
|
5753
7136
|
if (id !== null && !store.get(id)) activated.set(null);
|
|
@@ -5760,6 +7143,7 @@ function createViewer(el, opts) {
|
|
|
5760
7143
|
};
|
|
5761
7144
|
const mounted = mountViewer({
|
|
5762
7145
|
...view,
|
|
7146
|
+
plugins: host,
|
|
5763
7147
|
container: scroll,
|
|
5764
7148
|
document: doc,
|
|
5765
7149
|
toolOptions,
|
|
@@ -5800,7 +7184,7 @@ function createViewer(el, opts) {
|
|
|
5800
7184
|
engine.set(mounted);
|
|
5801
7185
|
return () => {
|
|
5802
7186
|
stopList();
|
|
5803
|
-
|
|
7187
|
+
threads?.destroy();
|
|
5804
7188
|
activated.set(null);
|
|
5805
7189
|
engine.set(null);
|
|
5806
7190
|
mounted.destroy();
|
|
@@ -5856,22 +7240,41 @@ function createViewer(el, opts) {
|
|
|
5856
7240
|
});
|
|
5857
7241
|
}
|
|
5858
7242
|
open(initial);
|
|
5859
|
-
|
|
7243
|
+
const viewer = {
|
|
5860
7244
|
element: root,
|
|
5861
7245
|
status: readonly(status),
|
|
5862
7246
|
document: readonly(current),
|
|
5863
7247
|
engine: readonly(engine),
|
|
5864
7248
|
viewport: readonly(viewport),
|
|
5865
7249
|
tools: readonly(tools),
|
|
7250
|
+
activated: readonly(activated),
|
|
7251
|
+
setActivated: (id) => {
|
|
7252
|
+
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
7253
|
+
},
|
|
7254
|
+
commands: { get: (id) => host.command(id) },
|
|
5866
7255
|
open,
|
|
5867
7256
|
destroy() {
|
|
5868
7257
|
if (destroyed) return;
|
|
5869
7258
|
destroyed = true;
|
|
5870
7259
|
loads++;
|
|
5871
7260
|
close();
|
|
7261
|
+
host.destroy();
|
|
5872
7262
|
root.remove();
|
|
5873
7263
|
}
|
|
5874
7264
|
};
|
|
7265
|
+
toolsByViewer.set(viewer, [...view.tools ?? [], ...host.tools]);
|
|
7266
|
+
commandIdsByViewer.set(viewer, () => [...host.commands.keys()]);
|
|
7267
|
+
return viewer;
|
|
7268
|
+
}
|
|
7269
|
+
const toolsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7270
|
+
/** The host tools a viewer was given (its own and its plugins'), for the controls' labels and icons. */
|
|
7271
|
+
function hostToolsOf(viewer) {
|
|
7272
|
+
return toolsByViewer.get(viewer) ?? [];
|
|
7273
|
+
}
|
|
7274
|
+
const commandIdsByViewer = /* @__PURE__ */ new WeakMap();
|
|
7275
|
+
/** The ids of the commands a viewer's plugins registered, for the error on an unknown one. */
|
|
7276
|
+
function hostCommandIdsOf(viewer) {
|
|
7277
|
+
return commandIdsByViewer.get(viewer)?.() ?? [];
|
|
5875
7278
|
}
|
|
5876
7279
|
//#endregion
|
|
5877
7280
|
//#region src/dom/viewer/controls.ts
|
|
@@ -5983,15 +7386,16 @@ function historyButton(viewer, action) {
|
|
|
5983
7386
|
return b;
|
|
5984
7387
|
}
|
|
5985
7388
|
/**
|
|
5986
|
-
* Sets the tool; pressed while it is the active one. A host tool's button
|
|
5987
|
-
*
|
|
7389
|
+
* Sets the tool; pressed while it is the active one. A host or plugin tool's button takes the
|
|
7390
|
+
* tool's own `label` and `icon`; `opts.label` wins, and without either it is named by the tool id.
|
|
5988
7391
|
*/
|
|
5989
7392
|
function createToolButton(viewer, tool, opts = {}) {
|
|
5990
|
-
const
|
|
7393
|
+
const own = hostToolsOf(viewer).find((t) => t.id === tool);
|
|
7394
|
+
const b = button(opts.label ?? TOOL_LABELS[tool] ?? own?.label ?? tool, {
|
|
5991
7395
|
class: "ink-tool-button",
|
|
5992
7396
|
"data-ink-tool-button": "",
|
|
5993
7397
|
"data-tool": tool
|
|
5994
|
-
}, TOOL_ICONS[tool]);
|
|
7398
|
+
}, TOOL_ICONS[tool] ?? own?.icon);
|
|
5995
7399
|
const show = (active) => {
|
|
5996
7400
|
const on = active === tool;
|
|
5997
7401
|
b.dataset.state = on ? "on" : "off";
|
|
@@ -6001,6 +7405,48 @@ function createToolButton(viewer, tool, opts = {}) {
|
|
|
6001
7405
|
follow(viewer.tools, b, (tools, watch) => watch(tools.active, show), () => show(null));
|
|
6002
7406
|
return b;
|
|
6003
7407
|
}
|
|
7408
|
+
/**
|
|
7409
|
+
* A plugin command as a button with its status text beside it. The button is named by the
|
|
7410
|
+
* command's label and disabled while the command is, except while it runs: then it keeps focus,
|
|
7411
|
+
* with `aria-busy` and `aria-disabled`, and a press does nothing. The status is a live region. A command without an icon shows its label as the button's text. For an id no
|
|
7412
|
+
* plugin registered it logs `InkError('UnknownCommand')` to the console and returns the same
|
|
7413
|
+
* markup disabled, `aria-hidden` and unnamed.
|
|
7414
|
+
*/
|
|
7415
|
+
function createCommandButton(viewer, id) {
|
|
7416
|
+
const command = viewer.commands.get(id);
|
|
7417
|
+
const status = h("span", {
|
|
7418
|
+
class: "ink-command-status",
|
|
7419
|
+
role: "status"
|
|
7420
|
+
});
|
|
7421
|
+
if (!command) {
|
|
7422
|
+
console.error(unknownCommand(id, hostCommandIdsOf(viewer)));
|
|
7423
|
+
const pending = h("button", {
|
|
7424
|
+
type: "button",
|
|
7425
|
+
"data-command": id,
|
|
7426
|
+
"aria-hidden": "true"
|
|
7427
|
+
});
|
|
7428
|
+
pending.disabled = true;
|
|
7429
|
+
return h("div", { class: "ink-command" }, pending, status);
|
|
7430
|
+
}
|
|
7431
|
+
const b = button(command.label, { "data-command": id }, command.icon);
|
|
7432
|
+
if (!command.icon) b.textContent = command.label;
|
|
7433
|
+
b.addEventListener("click", () => void command.run());
|
|
7434
|
+
const sync = () => {
|
|
7435
|
+
const busy = command.busy.get();
|
|
7436
|
+
b.disabled = !command.enabled.get() && !busy;
|
|
7437
|
+
b.setAttribute("aria-busy", String(busy));
|
|
7438
|
+
if (busy) b.setAttribute("aria-disabled", "true");
|
|
7439
|
+
else b.removeAttribute("aria-disabled");
|
|
7440
|
+
};
|
|
7441
|
+
command.enabled.subscribe(sync);
|
|
7442
|
+
command.busy.subscribe(sync);
|
|
7443
|
+
sync();
|
|
7444
|
+
command.status.subscribe((text) => {
|
|
7445
|
+
status.textContent = text;
|
|
7446
|
+
});
|
|
7447
|
+
status.textContent = command.status.get();
|
|
7448
|
+
return h("div", { class: "ink-command" }, b, status);
|
|
7449
|
+
}
|
|
6004
7450
|
function createZoomIn(viewer) {
|
|
6005
7451
|
const b = button("Zoom in", {
|
|
6006
7452
|
class: "ink-zoom-in",
|
|
@@ -6188,7 +7634,7 @@ function createMenu(trigger, opts) {
|
|
|
6188
7634
|
let panel = null;
|
|
6189
7635
|
let teardown = null;
|
|
6190
7636
|
const place = () => {
|
|
6191
|
-
if (panel) placePanel(panel, trigger.getBoundingClientRect());
|
|
7637
|
+
if (panel) placePanel(panel, (opts.anchor ?? trigger).getBoundingClientRect());
|
|
6192
7638
|
};
|
|
6193
7639
|
function close(restoreFocus = false) {
|
|
6194
7640
|
if (!panel) return;
|
|
@@ -6404,7 +7850,7 @@ function createColorMenu(viewer, opts) {
|
|
|
6404
7850
|
/** What the menu reads and writes: the whole tool style, or one group's view of it. */
|
|
6405
7851
|
let style = null;
|
|
6406
7852
|
let stops = [];
|
|
6407
|
-
const model = () => colorMenuModel(property, style?.current.get() ?? {}, style?.supported.get() ?? /* @__PURE__ */ new Set());
|
|
7853
|
+
const model = () => colorMenuModel(property, style?.current.get() ?? {}, style?.supported.get() ?? /* @__PURE__ */ new Set(), opts.fallback?.get());
|
|
6408
7854
|
function refresh() {
|
|
6409
7855
|
const m = model();
|
|
6410
7856
|
trigger.disabled = !m.enabled;
|
|
@@ -6518,6 +7964,7 @@ function createColorMenu(viewer, opts) {
|
|
|
6518
7964
|
};
|
|
6519
7965
|
follow(viewer.engine.get());
|
|
6520
7966
|
viewer.engine.subscribe(follow);
|
|
7967
|
+
opts.fallback?.subscribe(refresh);
|
|
6521
7968
|
return trigger;
|
|
6522
7969
|
}
|
|
6523
7970
|
//#endregion
|
|
@@ -6801,6 +8248,7 @@ function createShapeMenu(viewer) {
|
|
|
6801
8248
|
}
|
|
6802
8249
|
const menu = createMenu(trigger, {
|
|
6803
8250
|
label: "Shapes",
|
|
8251
|
+
anchor: root,
|
|
6804
8252
|
attrs: { "data-ink-menu": "shape" },
|
|
6805
8253
|
render: (panel) => paint(panel, model())
|
|
6806
8254
|
});
|
|
@@ -6859,10 +8307,13 @@ function createHighlightMenu(viewer) {
|
|
|
6859
8307
|
}, main, trigger);
|
|
6860
8308
|
main.disabled = true;
|
|
6861
8309
|
trigger.disabled = true;
|
|
8310
|
+
/** The colour the chosen kind gets while the markup style leaves it unset. */
|
|
8311
|
+
const defaultColour = writable(void 0);
|
|
6862
8312
|
const colour = createColorMenu(viewer, {
|
|
6863
8313
|
property: "border",
|
|
6864
8314
|
group: "markup",
|
|
6865
|
-
label: COLOUR_LABEL
|
|
8315
|
+
label: COLOUR_LABEL,
|
|
8316
|
+
fallback: defaultColour
|
|
6866
8317
|
});
|
|
6867
8318
|
colour.setAttribute("role", "menuitem");
|
|
6868
8319
|
colour.tabIndex = -1;
|
|
@@ -6883,6 +8334,7 @@ function createHighlightMenu(viewer) {
|
|
|
6883
8334
|
const on = tools?.active.get() === "highlight";
|
|
6884
8335
|
main.dataset.state = on ? "on" : "off";
|
|
6885
8336
|
main.setAttribute("aria-pressed", String(on));
|
|
8337
|
+
defaultColour.set(viewer.document.get()?.annotations.defaults.color(m.kind));
|
|
6886
8338
|
menu.refresh();
|
|
6887
8339
|
}
|
|
6888
8340
|
function paint(panel, m) {
|
|
@@ -6929,6 +8381,7 @@ function createHighlightMenu(viewer) {
|
|
|
6929
8381
|
}
|
|
6930
8382
|
const menu = createMenu(trigger, {
|
|
6931
8383
|
label: "Highlights",
|
|
8384
|
+
anchor: root,
|
|
6932
8385
|
attrs: { "data-ink-menu": "highlight" },
|
|
6933
8386
|
owns: (t) => t instanceof Element && t.closest("[data-ink-menu=\"color-border\"]") !== null,
|
|
6934
8387
|
render: (panel) => paint(panel, model())
|
|
@@ -6938,7 +8391,11 @@ function createHighlightMenu(viewer) {
|
|
|
6938
8391
|
for (const stop of stops) stop();
|
|
6939
8392
|
stops = [];
|
|
6940
8393
|
tools = next;
|
|
6941
|
-
if (tools) stops = [
|
|
8394
|
+
if (tools) stops = [
|
|
8395
|
+
tools.active.subscribe(refresh),
|
|
8396
|
+
tools.options("highlight").subscribe(refresh),
|
|
8397
|
+
viewer.document.subscribe(refresh)
|
|
8398
|
+
];
|
|
6942
8399
|
if (!tools) menu.close();
|
|
6943
8400
|
refresh();
|
|
6944
8401
|
};
|
|
@@ -7695,6 +9152,7 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
7695
9152
|
}
|
|
7696
9153
|
const menu = createMenu(trigger, {
|
|
7697
9154
|
label: SIGNATURE_LABELS.menu,
|
|
9155
|
+
anchor: root,
|
|
7698
9156
|
attrs: { "data-ink-menu": "signature" },
|
|
7699
9157
|
render: (panel) => paint(panel, model())
|
|
7700
9158
|
});
|
|
@@ -7731,6 +9189,6 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
7731
9189
|
return root;
|
|
7732
9190
|
}
|
|
7733
9191
|
//#endregion
|
|
7734
|
-
export { ARROWHEAD_CHOICES, ARROWHEAD_ENDING, COLOR_COLUMNS, COLOR_LABELS, DASH_PATTERN, DEFAULT_CORNER_RADIUS, DEFAULT_FREETEXT_WIDTH, DEFAULT_HIGHLIGHT_OPTIONS, DEFAULT_SHAPE_SIZE, FONT_FAMILIES, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, arrowheadChoice, attachViewportKeys, builtinTools, clampFontSize, colorMenuModel, computeCssLayout,
|
|
9192
|
+
export { ARROWHEAD_CHOICES, ARROWHEAD_ENDING, COLOR_COLUMNS, COLOR_LABELS, DASH_PATTERN, DEFAULT_CORNER_RADIUS, DEFAULT_FREETEXT_WIDTH, DEFAULT_HIGHLIGHT_OPTIONS, DEFAULT_SHAPE_SIZE, FONT_FAMILIES, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, arrowheadChoice, attachViewportKeys, builtinTools, clampFontSize, colorMenuModel, computeCssLayout, createColorMenu, createCommandButton, createFocusReturn, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPluginHost, createPluginLayers, createPopover, createPrevPage, createRedo, createShapeMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, definePlugin, editFreeText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, highlightTool, hitFor, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, placePanel, redactAreaTool, renderAnnotationContent, renderTypedSignature, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, signatureTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, trapTab, unknownCommand, visibleRangeCss, withHostTools };
|
|
7735
9193
|
|
|
7736
9194
|
//# sourceMappingURL=dom.mjs.map
|