@inkbar/core 0.4.0 → 0.5.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 +18 -7
- package/dist/dom.d.mts +142 -26
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +1277 -898
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +3 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BHYKTwYE.d.mts → signatures-BwaXi7C8.d.mts} +615 -611
- package/dist/signatures-BwaXi7C8.d.mts.map +1 -0
- package/dist/{signatures-DF2VmNVa.mjs → signatures-CMapTKrC.mjs} +1071 -281
- package/dist/signatures-CMapTKrC.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 readonly, 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, 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, _n as isInkError, b as isAbandonedNote, d as recogniseStroke, dn as rectsIntersect, en as writable, f as DEFAULT_CORNER_RADIUS, g as constrain, gn as InkError, h as SHAPE_KINDS, i as createInkTemplate, l as placeSignature, ln as rectFromPoints, m as DEFAULT_SHAPE_SIZE, o as createMemorySignatureStore, p as DEFAULT_SHAPE_OPTIONS, q as translated, r as createImageTemplate, rn as invertMatrix, s as createSignatureLibrary, tn as applyMatrix, tt as isProduction, v as isPointerOnlyKind, w as threadOf, y as shapeInput, z as isBubble, zt as isStandardCallout } from "./signatures-CMapTKrC.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
|
|
@@ -966,6 +966,8 @@ const CORNERS$1 = [
|
|
|
966
966
|
];
|
|
967
967
|
/** Width in CSS px of the band along a mask's edge that takes presses. */
|
|
968
968
|
const MASK_EDGE = 6;
|
|
969
|
+
/** Width in CSS px of the band straddling a text box's edge, which drags the box. */
|
|
970
|
+
const TEXT_EDGE = 6;
|
|
969
971
|
/**
|
|
970
972
|
* Syncs an element's edit handles (`.ink-handle[data-handle]`) with `spec`. Each is an 8px
|
|
971
973
|
* `position:absolute; pointer-events:auto` box centred on its point; its look is the theme's.
|
|
@@ -999,15 +1001,62 @@ function syncHandles(node, spec) {
|
|
|
999
1001
|
h.style.top = `${at.y - HANDLE_PX / 2}px`;
|
|
1000
1002
|
}
|
|
1001
1003
|
}
|
|
1004
|
+
/** Width in CSS px a thin stroke's invisible hit twin is widened to. */
|
|
1005
|
+
const HIT_STROKE = 8;
|
|
1006
|
+
/**
|
|
1007
|
+
* True when `a` is drawn as a stroke alone (a line, polyline, ink, or an unfilled square,
|
|
1008
|
+
* circle or polygon with a border), so a press beside the stroke would miss it. These get an
|
|
1009
|
+
* invisible, wider hit twin of their geometry.
|
|
1010
|
+
*/
|
|
1011
|
+
function hasHitTwin(a) {
|
|
1012
|
+
switch (a.type) {
|
|
1013
|
+
case "line":
|
|
1014
|
+
case "polyline":
|
|
1015
|
+
case "ink": return true;
|
|
1016
|
+
case "polygon":
|
|
1017
|
+
case "square":
|
|
1018
|
+
case "circle": return a.interiorColor === void 0 && a.width > 0;
|
|
1019
|
+
default: return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/** The width in CSS px of `a`'s hit twin: 8px, or the drawn stroke if that is wider. */
|
|
1023
|
+
function hitWidth(a, t) {
|
|
1024
|
+
return Math.max(HIT_STROKE, "width" in a ? t.toCssLength(a.width) : 0);
|
|
1025
|
+
}
|
|
1002
1026
|
/**
|
|
1003
1027
|
* The CSS box an annotation's default visual occupies. Usually `toCssRect(a.rect)`. Stroked
|
|
1004
1028
|
* paths (lines, polylines, polygons, ink) use the bounds of their own points instead of
|
|
1005
1029
|
* `rect` (which may already carry stroke padding, as a derived ink rect does), padded once by
|
|
1006
1030
|
* 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
|
-
*
|
|
1031
|
+
* line still has a box to draw and hit-test in. A shape with a hit twin (see `hasHitTwin`)
|
|
1032
|
+
* is padded by at least 4px more, so its wider hit area is not clipped. Custom elements do
|
|
1033
|
+
* not use this: they sit on `toCssRect(a.rect)`.
|
|
1009
1034
|
*/
|
|
1010
1035
|
function annotationBox(a, t) {
|
|
1036
|
+
const drawn = handleBox(a, t);
|
|
1037
|
+
if (!hasHitTwin(a)) return drawn;
|
|
1038
|
+
const pad = hitPad(a, t);
|
|
1039
|
+
return {
|
|
1040
|
+
left: drawn.left - pad,
|
|
1041
|
+
top: drawn.top - pad,
|
|
1042
|
+
width: drawn.width + pad * 2,
|
|
1043
|
+
height: drawn.height + pad * 2
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* How far past `handleBox` a shape with a hit twin extends its element: the twin's overhang
|
|
1048
|
+
* beyond the drawn stroke, and at least 4px.
|
|
1049
|
+
*/
|
|
1050
|
+
function hitPad(a, t) {
|
|
1051
|
+
const overhang = Math.max(0, hitWidth(a, t) - t.toCssLength("width" in a ? a.width : 0));
|
|
1052
|
+
return Math.max(HIT_STROKE / 2, overhang / 2);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The CSS box an annotation's edit handles and resizing work on: `annotationBox` without the
|
|
1056
|
+
* padding its hit twin needs. `toCssRect(a.rect)` for a rect-shaped annotation; the padded
|
|
1057
|
+
* bounds of its points for a stroked path.
|
|
1058
|
+
*/
|
|
1059
|
+
function handleBox(a, t) {
|
|
1011
1060
|
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
1061
|
if (!points || points.length === 0 || !("width" in a)) return t.toCssRect(a.rect);
|
|
1013
1062
|
const box = t.toCssRect(pointBounds(points));
|
|
@@ -1200,7 +1249,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1200
1249
|
text.style.height = `${inner.height}px`;
|
|
1201
1250
|
}
|
|
1202
1251
|
text.style.pointerEvents = "auto";
|
|
1203
|
-
el.append(text);
|
|
1252
|
+
el.append(text, edgeBand(doc, box, freeTextLocalBox(a, t)));
|
|
1204
1253
|
appendSelectionFrame(el, a, t);
|
|
1205
1254
|
return;
|
|
1206
1255
|
}
|
|
@@ -1210,6 +1259,8 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1210
1259
|
}
|
|
1211
1260
|
const svg = boxSvg(doc, box.width, box.height);
|
|
1212
1261
|
svg.classList.add("ink-annotation-shape");
|
|
1262
|
+
/** Invisible wider copies of stroke-only geometry, added once the drawn shapes are. */
|
|
1263
|
+
const twins = [];
|
|
1213
1264
|
switch (a.type) {
|
|
1214
1265
|
case "highlight":
|
|
1215
1266
|
for (const q of a.quads) svg.append(node("polygon", {
|
|
@@ -1252,23 +1303,34 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1252
1303
|
case "square":
|
|
1253
1304
|
case "circle": {
|
|
1254
1305
|
const sw = t.toCssLength(a.width);
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1306
|
+
const pad = hasHitTwin(a) ? hitPad(a, t) : 0;
|
|
1307
|
+
const w = box.width - pad * 2;
|
|
1308
|
+
const h = box.height - pad * 2;
|
|
1309
|
+
const geometry = a.type === "square" ? {
|
|
1310
|
+
name: "rect",
|
|
1311
|
+
attrs: {
|
|
1312
|
+
x: pad + sw / 2,
|
|
1313
|
+
y: pad + sw / 2,
|
|
1314
|
+
width: Math.max(0, w - sw),
|
|
1315
|
+
height: Math.max(0, h - sw),
|
|
1316
|
+
...a.cornerRadius ? { rx: t.toCssLength(a.cornerRadius) } : {}
|
|
1317
|
+
}
|
|
1318
|
+
} : {
|
|
1319
|
+
name: "ellipse",
|
|
1320
|
+
attrs: {
|
|
1321
|
+
cx: box.width / 2,
|
|
1322
|
+
cy: box.height / 2,
|
|
1323
|
+
rx: Math.max(0, w / 2 - sw / 2),
|
|
1324
|
+
ry: Math.max(0, h / 2 - sw / 2)
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
const shape = node(geometry.name, geometry.attrs);
|
|
1267
1328
|
shape.setAttribute("fill", a.interiorColor ?? "none");
|
|
1268
1329
|
shape.setAttribute("stroke", a.color);
|
|
1269
1330
|
shape.setAttribute("stroke-width", String(sw));
|
|
1270
1331
|
dash(shape, a.dashes, t);
|
|
1271
1332
|
svg.append(shape);
|
|
1333
|
+
if (hasHitTwin(a)) twins.push(hitTwin(node, geometry.name, geometry.attrs, hitWidth(a, t)));
|
|
1272
1334
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1273
1335
|
break;
|
|
1274
1336
|
}
|
|
@@ -1320,6 +1382,10 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1320
1382
|
});
|
|
1321
1383
|
dash(stroked, a.dashes, t);
|
|
1322
1384
|
svg.append(stroked);
|
|
1385
|
+
if (hasHitTwin(a)) twins.push(hitTwin(node, stroked.tagName, {
|
|
1386
|
+
points: stroked.getAttribute("points"),
|
|
1387
|
+
"stroke-linejoin": "round"
|
|
1388
|
+
}, hitWidth(a, t)));
|
|
1323
1389
|
const ends = endingKinds(a);
|
|
1324
1390
|
if (ends && pts.length >= 2) for (const [name, kind, tip, other] of [[
|
|
1325
1391
|
"head",
|
|
@@ -1344,6 +1410,11 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1344
1410
|
});
|
|
1345
1411
|
ending.setAttribute("data-ending", name);
|
|
1346
1412
|
svg.append(ending);
|
|
1413
|
+
twins.push(hitTwin(node, "path", {
|
|
1414
|
+
d,
|
|
1415
|
+
"stroke-linejoin": "round",
|
|
1416
|
+
"stroke-linecap": "round"
|
|
1417
|
+
}, hitWidth(a, t)));
|
|
1347
1418
|
}
|
|
1348
1419
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1349
1420
|
break;
|
|
@@ -1356,14 +1427,54 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1356
1427
|
d: outlinePath(outline),
|
|
1357
1428
|
fill: a.color
|
|
1358
1429
|
}));
|
|
1430
|
+
twins.push(hitTwin(node, "polyline", {
|
|
1431
|
+
points: path.map(local).join(" "),
|
|
1432
|
+
"stroke-linejoin": "round",
|
|
1433
|
+
"stroke-linecap": "round"
|
|
1434
|
+
}, hitWidth(a, t)));
|
|
1359
1435
|
});
|
|
1360
1436
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1361
1437
|
}
|
|
1362
1438
|
for (const shape of svg.children) shape.setAttribute("pointer-events", "visiblePainted");
|
|
1439
|
+
svg.append(...twins);
|
|
1363
1440
|
el.append(svg);
|
|
1364
1441
|
appendCommentMarker(el, a, t);
|
|
1365
1442
|
}
|
|
1366
1443
|
/**
|
|
1444
|
+
* An invisible copy of a stroke-only shape's geometry, `width` CSS px wide, that takes
|
|
1445
|
+
* presses on the stroke alone (`[data-ink-hit-stroke]`), so a thin line is easy to hit.
|
|
1446
|
+
*/
|
|
1447
|
+
function hitTwin(node, name, attrs, width) {
|
|
1448
|
+
const twin = node(name, attrs);
|
|
1449
|
+
twin.setAttribute("data-ink-hit-stroke", "");
|
|
1450
|
+
twin.setAttribute("fill", "none");
|
|
1451
|
+
twin.setAttribute("stroke", "transparent");
|
|
1452
|
+
twin.setAttribute("stroke-width", String(width));
|
|
1453
|
+
twin.setAttribute("pointer-events", "stroke");
|
|
1454
|
+
return twin;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* The band a press on a free text's edge lands on: an invisible `rect[data-ink-edge]` stroke
|
|
1458
|
+
* `TEXT_EDGE` px wide along the text box's edge, half inside it and half out, in its own SVG
|
|
1459
|
+
* over the element. A press inside the box edits it; one on the band selects and drags it.
|
|
1460
|
+
* It is there whether or not the box draws a border. Geometry only.
|
|
1461
|
+
*/
|
|
1462
|
+
function edgeBand(doc, el, box) {
|
|
1463
|
+
const svg = boxSvg(doc, el.width, el.height);
|
|
1464
|
+
const band = doc.createElementNS(SVG$2, "rect");
|
|
1465
|
+
band.setAttribute("x", String(box.left));
|
|
1466
|
+
band.setAttribute("y", String(box.top));
|
|
1467
|
+
band.setAttribute("width", String(box.width));
|
|
1468
|
+
band.setAttribute("height", String(box.height));
|
|
1469
|
+
band.setAttribute("fill", "none");
|
|
1470
|
+
band.setAttribute("stroke", "transparent");
|
|
1471
|
+
band.setAttribute("stroke-width", String(TEXT_EDGE));
|
|
1472
|
+
band.setAttribute("pointer-events", "stroke");
|
|
1473
|
+
band.setAttribute("data-ink-edge", "");
|
|
1474
|
+
svg.append(band);
|
|
1475
|
+
return svg;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1367
1478
|
* A stamp's `img.ink-stamp`, filling the element's box. The image is upright in user space
|
|
1368
1479
|
* turned by the stamp's `rotation` (anticlockwise), and the page turns it again on screen
|
|
1369
1480
|
* (clockwise), so it is drawn turned by the difference: a signature placed against the page's
|
|
@@ -1660,7 +1771,8 @@ const SAME_PX = .5;
|
|
|
1660
1771
|
const MIN_WIDTH_EM = 2;
|
|
1661
1772
|
/**
|
|
1662
1773
|
* Opens an editor on `el`, the overlay element showing `a` (or a stand-in for a box not in the
|
|
1663
|
-
* store yet)
|
|
1774
|
+
* store yet), with the caret at `opts.caret` or after the text. `Escape` commits; a press
|
|
1775
|
+
* outside the editor, or focus leaving it, commits too.
|
|
1664
1776
|
* Closing while the editor has focus hands focus to `el`. Keys typed in the
|
|
1665
1777
|
* editor stop there, so no viewport or tool shortcut sees them. Opening a second editor on the
|
|
1666
1778
|
* same element commits the first.
|
|
@@ -1716,7 +1828,7 @@ function editFreeText(el, a, opts) {
|
|
|
1716
1828
|
return Math.abs(grown) < SAME_PX ? 0 : grown;
|
|
1717
1829
|
};
|
|
1718
1830
|
let closed = false;
|
|
1719
|
-
const finish = (commit) => {
|
|
1831
|
+
const finish = (commit, how) => {
|
|
1720
1832
|
if (closed) return;
|
|
1721
1833
|
closed = true;
|
|
1722
1834
|
const contents = read();
|
|
@@ -1730,27 +1842,27 @@ function editFreeText(el, a, opts) {
|
|
|
1730
1842
|
box?.style.removeProperty("visibility");
|
|
1731
1843
|
el.style.height = `${base.height}px`;
|
|
1732
1844
|
refitBubble(el, a, opts.transform, base);
|
|
1733
|
-
if (commit) opts.commit(contents, next, grown);
|
|
1845
|
+
if (commit) opts.commit(contents, next, grown, how);
|
|
1734
1846
|
else opts.cancel();
|
|
1735
1847
|
};
|
|
1736
1848
|
const onOutside = (e) => {
|
|
1737
1849
|
if (e.target instanceof Node && field.contains(e.target)) return;
|
|
1738
1850
|
closingPresses.add(e);
|
|
1739
|
-
finish(true);
|
|
1851
|
+
finish(true, "press");
|
|
1740
1852
|
};
|
|
1741
1853
|
view?.addEventListener("pointerdown", onOutside, true);
|
|
1742
1854
|
field.addEventListener("keydown", (e) => {
|
|
1743
1855
|
e.stopPropagation();
|
|
1744
1856
|
if (e.key === "Escape" && !e.isComposing) {
|
|
1745
1857
|
e.preventDefault();
|
|
1746
|
-
finish(true);
|
|
1858
|
+
finish(true, "escape");
|
|
1747
1859
|
}
|
|
1748
1860
|
});
|
|
1749
1861
|
field.addEventListener("focusout", (e) => {
|
|
1750
1862
|
if (!doc.hasFocus()) return;
|
|
1751
1863
|
const to = e.relatedTarget;
|
|
1752
1864
|
if (to instanceof Node && field.contains(to)) return;
|
|
1753
|
-
finish(true);
|
|
1865
|
+
finish(true, "focus");
|
|
1754
1866
|
});
|
|
1755
1867
|
field.addEventListener("paste", (e) => {
|
|
1756
1868
|
e.preventDefault();
|
|
@@ -1762,14 +1874,17 @@ function editFreeText(el, a, opts) {
|
|
|
1762
1874
|
field.focus({ preventScroll: true });
|
|
1763
1875
|
const selection = doc.getSelection();
|
|
1764
1876
|
if (selection) {
|
|
1765
|
-
|
|
1766
|
-
range
|
|
1767
|
-
|
|
1877
|
+
let range = opts.caret && caretIn(field, opts.caret);
|
|
1878
|
+
if (!range) {
|
|
1879
|
+
range = doc.createRange();
|
|
1880
|
+
range.selectNodeContents(field);
|
|
1881
|
+
range.collapse(false);
|
|
1882
|
+
}
|
|
1768
1883
|
selection.removeAllRanges();
|
|
1769
1884
|
selection.addRange(range);
|
|
1770
1885
|
}
|
|
1771
1886
|
grow();
|
|
1772
|
-
const editor = { close: (commit) => finish(commit) };
|
|
1887
|
+
const editor = { close: (commit) => finish(commit, "api") };
|
|
1773
1888
|
editors.set(el, editor);
|
|
1774
1889
|
return editor;
|
|
1775
1890
|
}
|
|
@@ -1785,37 +1900,44 @@ function canEditFreeText(a, el) {
|
|
|
1785
1900
|
* Opens an editor on the overlay element `el` for the free-text box `id`, wired to the store: a
|
|
1786
1901
|
* commit is one `update` with the text and the grown rect (none when neither changed), and an
|
|
1787
1902
|
* 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
|
-
*
|
|
1903
|
+
* shows without text, so it is kept. `onClose` runs after the store change, with what closed
|
|
1904
|
+
* the editor; `caret` is a client point to put the caret at (`editFreeText`). Returns
|
|
1905
|
+
* undefined when the box cannot be edited (`canEditFreeText`).
|
|
1790
1906
|
*/
|
|
1791
|
-
function editFreeTextIn(store, el, id, transform) {
|
|
1907
|
+
function editFreeTextIn(store, el, id, transform, opts = {}) {
|
|
1792
1908
|
const a = store._getProposed(id) ?? store.get(id);
|
|
1793
1909
|
if (!canEditFreeText(a, el)) return void 0;
|
|
1794
1910
|
const current = () => {
|
|
1795
1911
|
const now = store._getProposed(id) ?? store.get(id);
|
|
1796
1912
|
return now?.type === "freetext" ? now : void 0;
|
|
1797
1913
|
};
|
|
1914
|
+
const write = (contents, grown) => {
|
|
1915
|
+
const now = current();
|
|
1916
|
+
if (!now) return;
|
|
1917
|
+
if (contents.trim() === "" && !isBubble(now)) {
|
|
1918
|
+
store.remove(id);
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (grown === 0) {
|
|
1922
|
+
if (contents !== now.contents) store.update(id, { contents });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
store.update(id, {
|
|
1926
|
+
contents,
|
|
1927
|
+
...grownFreeText(now, grown, transform)
|
|
1928
|
+
});
|
|
1929
|
+
};
|
|
1798
1930
|
return editFreeText(el, a, {
|
|
1799
1931
|
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
|
-
});
|
|
1932
|
+
caret: opts.caret,
|
|
1933
|
+
commit(contents, _rect, grown, how) {
|
|
1934
|
+
write(contents, grown);
|
|
1935
|
+
opts.onClose?.(how);
|
|
1815
1936
|
},
|
|
1816
1937
|
cancel() {
|
|
1817
1938
|
const now = current();
|
|
1818
1939
|
if (now && now.contents.trim() === "" && !isBubble(now)) store.remove(id);
|
|
1940
|
+
opts.onClose?.("api");
|
|
1819
1941
|
}
|
|
1820
1942
|
});
|
|
1821
1943
|
}
|
|
@@ -1844,6 +1966,28 @@ function grownFreeText(a, grown, t) {
|
|
|
1844
1966
|
};
|
|
1845
1967
|
return withBox(a, box, resized.callout.map((p, i) => i === last ? p : down(p)));
|
|
1846
1968
|
}
|
|
1969
|
+
/**
|
|
1970
|
+
* A collapsed range at the text position under a client point, if that position is in `field`.
|
|
1971
|
+
* The editor lies over the box with the same look, so a point on the box lands on the same
|
|
1972
|
+
* character in the editor.
|
|
1973
|
+
*/
|
|
1974
|
+
function caretIn(field, at) {
|
|
1975
|
+
const doc = field.ownerDocument;
|
|
1976
|
+
let node;
|
|
1977
|
+
let offset = 0;
|
|
1978
|
+
if (typeof doc.caretPositionFromPoint === "function") {
|
|
1979
|
+
const p = doc.caretPositionFromPoint(at.clientX, at.clientY);
|
|
1980
|
+
if (p) ({offsetNode: node, offset} = p);
|
|
1981
|
+
} else if (typeof doc.caretRangeFromPoint === "function") {
|
|
1982
|
+
const r = doc.caretRangeFromPoint(at.clientX, at.clientY);
|
|
1983
|
+
if (r) ({startContainer: node, startOffset: offset} = r);
|
|
1984
|
+
}
|
|
1985
|
+
if (!node || !field.contains(node)) return void 0;
|
|
1986
|
+
const range = doc.createRange();
|
|
1987
|
+
range.setStart(node, offset);
|
|
1988
|
+
range.collapse(true);
|
|
1989
|
+
return range;
|
|
1990
|
+
}
|
|
1847
1991
|
/** `plaintext-only` where the browser has it, else `true` (paste and formatting are then guarded). */
|
|
1848
1992
|
function makeEditable(field) {
|
|
1849
1993
|
try {
|
|
@@ -2160,12 +2304,8 @@ const LABELS = {
|
|
|
2160
2304
|
mask: "Mask",
|
|
2161
2305
|
redact: "Redaction"
|
|
2162
2306
|
};
|
|
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
2307
|
function ariaLabel(a) {
|
|
2168
|
-
const base = a.
|
|
2308
|
+
const base = a.subject === "Signature" && (a.type === "ink" || a.type === "stamp") ? "Signature" : LABELS[a.type];
|
|
2169
2309
|
const named = hasComment(a) ? `${base} with comment` : base;
|
|
2170
2310
|
return a.author ? `${named} by ${a.author}` : named;
|
|
2171
2311
|
}
|
|
@@ -2173,14 +2313,60 @@ function ariaLabel(a) {
|
|
|
2173
2313
|
function readingOrder(a, b) {
|
|
2174
2314
|
return b.rect.y + b.rect.height - (a.rect.y + a.rect.height) || a.rect.x - b.rect.x;
|
|
2175
2315
|
}
|
|
2316
|
+
/** A decoration's data key: lower-case kebab, as written after `data-`. */
|
|
2317
|
+
const DECORATION_KEY = /^[a-z][a-z0-9-]*$/;
|
|
2318
|
+
/**
|
|
2319
|
+
* `data-*` keys core writes on an annotation element or inside it; a decoration cannot set
|
|
2320
|
+
* them. `data-ink-*` is core's too.
|
|
2321
|
+
*/
|
|
2322
|
+
const RESERVED_DATA = /* @__PURE__ */ new Set([
|
|
2323
|
+
"annotation",
|
|
2324
|
+
"type",
|
|
2325
|
+
"source",
|
|
2326
|
+
"locked",
|
|
2327
|
+
"state",
|
|
2328
|
+
"custom",
|
|
2329
|
+
"handle",
|
|
2330
|
+
"framed",
|
|
2331
|
+
"align",
|
|
2332
|
+
"bubble",
|
|
2333
|
+
"border",
|
|
2334
|
+
"icon",
|
|
2335
|
+
"ending"
|
|
2336
|
+
]);
|
|
2337
|
+
/** Skipped decoration keys and classes already warned about. */
|
|
2338
|
+
const warnedDecorations = /* @__PURE__ */ new Set();
|
|
2339
|
+
function warnDecoration(what, why) {
|
|
2340
|
+
if (isProduction() || warnedDecorations.has(what)) return;
|
|
2341
|
+
warnedDecorations.add(what);
|
|
2342
|
+
console.warn(`[inkbar] annotation decoration: skipped ${what}: ${why}`);
|
|
2343
|
+
}
|
|
2344
|
+
function usableData(key) {
|
|
2345
|
+
if (!DECORATION_KEY.test(key)) {
|
|
2346
|
+
warnDecoration(`data key "${key}"`, "keys are lower-case kebab");
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
if (RESERVED_DATA.has(key) || key === "ink" || key.startsWith("ink-")) {
|
|
2350
|
+
warnDecoration(`data key "${key}"`, "core sets it");
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
function usableClass(name) {
|
|
2356
|
+
if (name.startsWith("ink-")) {
|
|
2357
|
+
warnDecoration(`class "${name}"`, "`ink-` classes are core's");
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
if (name === "" || /\s/.test(name)) {
|
|
2361
|
+
warnDecoration(`class "${name}"`, "not a single class name");
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2176
2366
|
function setFlag(node, key, on) {
|
|
2177
2367
|
if (on) node.dataset[key] = "";
|
|
2178
2368
|
else delete node.dataset[key];
|
|
2179
2369
|
}
|
|
2180
|
-
function setData(node, key, value) {
|
|
2181
|
-
if (value === void 0) delete node.dataset[key];
|
|
2182
|
-
else node.dataset[key] = value;
|
|
2183
|
-
}
|
|
2184
2370
|
function createOverlay(opts) {
|
|
2185
2371
|
const { store, page } = opts;
|
|
2186
2372
|
const doc = opts.host.ownerDocument;
|
|
@@ -2199,6 +2385,8 @@ function createOverlay(opts) {
|
|
|
2199
2385
|
/** What each element's content was last drawn from; absent when it has none of ours. */
|
|
2200
2386
|
const drawn = /* @__PURE__ */ new Map();
|
|
2201
2387
|
const hovered = /* @__PURE__ */ new Set();
|
|
2388
|
+
/** The decoration each element last got, so a later one can drop what it no longer has. */
|
|
2389
|
+
const decorated = /* @__PURE__ */ new WeakMap();
|
|
2202
2390
|
let order = [];
|
|
2203
2391
|
let destroyed = false;
|
|
2204
2392
|
function stateOf(id, selected) {
|
|
@@ -2213,7 +2401,17 @@ function createOverlay(opts) {
|
|
|
2213
2401
|
node.setAttribute("aria-pressed", String(state === "selected"));
|
|
2214
2402
|
node.tabIndex = !anyHere || state === "selected" ? 0 : -1;
|
|
2215
2403
|
const a = store.get(id);
|
|
2216
|
-
syncHandles(node, a && !a.locked && state === "selected" &&
|
|
2404
|
+
syncHandles(node, a && !a.locked && state === "selected" && editsSelection() ? handleSpec(node, a) : { kind: "none" });
|
|
2405
|
+
}
|
|
2406
|
+
/** `handleBox` relative to an element at `left`,`top`. */
|
|
2407
|
+
function handleLocalBox(a, left, top) {
|
|
2408
|
+
const box = handleBox(a, transform);
|
|
2409
|
+
return {
|
|
2410
|
+
left: box.left - left,
|
|
2411
|
+
top: box.top - top,
|
|
2412
|
+
width: box.width,
|
|
2413
|
+
height: box.height
|
|
2414
|
+
};
|
|
2217
2415
|
}
|
|
2218
2416
|
/**
|
|
2219
2417
|
* Line ends for a line, corners for what a corner drag resizes (a free text's on its text
|
|
@@ -2242,8 +2440,7 @@ function createOverlay(opts) {
|
|
|
2242
2440
|
...freeTextLocalBox(a, transform)
|
|
2243
2441
|
} : {
|
|
2244
2442
|
kind: "corners",
|
|
2245
|
-
|
|
2246
|
-
height: parseFloat(node.style.height) || 0
|
|
2443
|
+
...handleLocalBox(a, left, top)
|
|
2247
2444
|
};
|
|
2248
2445
|
if (!hasCallout(a)) return corners;
|
|
2249
2446
|
const tip = transform.toCss(a.callout[0]);
|
|
@@ -2257,11 +2454,13 @@ function createOverlay(opts) {
|
|
|
2257
2454
|
}
|
|
2258
2455
|
/**
|
|
2259
2456
|
* True while the tool controller marks the tool root (an ancestor) with a select tool,
|
|
2260
|
-
* `data-ink-tool="select"` or `"select-text"
|
|
2457
|
+
* `data-ink-tool="select"` or `"select-text"`, or with `data-ink-made`: what a creation tool
|
|
2458
|
+
* just made can be moved and resized too.
|
|
2261
2459
|
*/
|
|
2262
|
-
function
|
|
2263
|
-
const
|
|
2264
|
-
|
|
2460
|
+
function editsSelection() {
|
|
2461
|
+
const root = opts.host.closest("[data-ink-tool]");
|
|
2462
|
+
const tool = root?.getAttribute("data-ink-tool");
|
|
2463
|
+
return tool === "select" || tool === "select-text" || root?.hasAttribute("data-ink-made") === true;
|
|
2265
2464
|
}
|
|
2266
2465
|
const toolRoot = opts.host.closest("[data-ink-tool]") ?? opts.host.closest("[data-ink-viewport]");
|
|
2267
2466
|
const Observer = doc.defaultView?.MutationObserver;
|
|
@@ -2273,16 +2472,48 @@ function createOverlay(opts) {
|
|
|
2273
2472
|
});
|
|
2274
2473
|
toolObserver.observe(toolRoot, {
|
|
2275
2474
|
attributes: true,
|
|
2276
|
-
attributeFilter: ["data-ink-tool"]
|
|
2475
|
+
attributeFilter: ["data-ink-tool", "data-ink-made"]
|
|
2277
2476
|
});
|
|
2278
2477
|
}
|
|
2279
2478
|
function applyAttributes(node, a) {
|
|
2280
2479
|
node.dataset.type = a.type;
|
|
2281
2480
|
node.dataset.source = a.source;
|
|
2282
2481
|
setFlag(node, "locked", a.locked === true);
|
|
2283
|
-
setData(node, "origin", a.type === "redact" ? a.origin : void 0);
|
|
2284
|
-
setData(node, "confidence", confidenceAttr(a));
|
|
2285
2482
|
node.setAttribute("aria-label", ariaLabel(a));
|
|
2483
|
+
applyDecoration(node, a);
|
|
2484
|
+
}
|
|
2485
|
+
/** The app's data attributes, classes and description, from `decorate`; a throw adds none. */
|
|
2486
|
+
function applyDecoration(node, a) {
|
|
2487
|
+
let decoration;
|
|
2488
|
+
try {
|
|
2489
|
+
decoration = opts.decorate?.(a);
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
console.error(error);
|
|
2492
|
+
}
|
|
2493
|
+
const prev = decorated.get(node);
|
|
2494
|
+
if (!decoration && !prev) return;
|
|
2495
|
+
const next = {
|
|
2496
|
+
data: /* @__PURE__ */ new Set(),
|
|
2497
|
+
classes: /* @__PURE__ */ new Set(),
|
|
2498
|
+
description: false
|
|
2499
|
+
};
|
|
2500
|
+
for (const [key, value] of Object.entries(decoration?.data ?? {})) {
|
|
2501
|
+
if (!usableData(key)) continue;
|
|
2502
|
+
node.setAttribute(`data-${key}`, value);
|
|
2503
|
+
next.data.add(key);
|
|
2504
|
+
}
|
|
2505
|
+
for (const name of decoration?.classes ?? []) {
|
|
2506
|
+
if (!usableClass(name)) continue;
|
|
2507
|
+
node.classList.add(name);
|
|
2508
|
+
next.classes.add(name);
|
|
2509
|
+
}
|
|
2510
|
+
if (decoration?.description) {
|
|
2511
|
+
node.setAttribute("aria-description", decoration.description);
|
|
2512
|
+
next.description = true;
|
|
2513
|
+
} else if (prev?.description) node.removeAttribute("aria-description");
|
|
2514
|
+
for (const key of prev?.data ?? []) if (!next.data.has(key)) node.removeAttribute(`data-${key}`);
|
|
2515
|
+
for (const name of prev?.classes ?? []) if (!next.classes.has(name)) node.classList.remove(name);
|
|
2516
|
+
decorated.set(node, next);
|
|
2286
2517
|
}
|
|
2287
2518
|
function applyGeometry(node, a) {
|
|
2288
2519
|
const box = opts.isCustom?.(a) === true ? transform.toCssRect(a.rect) : annotationBox(a, transform);
|
|
@@ -2561,6 +2792,13 @@ function createOverlay(opts) {
|
|
|
2561
2792
|
render();
|
|
2562
2793
|
},
|
|
2563
2794
|
elementFor: (id) => elements.get(id),
|
|
2795
|
+
redecorate() {
|
|
2796
|
+
if (destroyed) return;
|
|
2797
|
+
for (const [id, node] of elements) {
|
|
2798
|
+
const a = store.get(id);
|
|
2799
|
+
if (a) applyDecoration(node, a);
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2564
2802
|
destroy() {
|
|
2565
2803
|
if (destroyed) return;
|
|
2566
2804
|
closeEditors();
|
|
@@ -2611,7 +2849,8 @@ function createPageMount(opts) {
|
|
|
2611
2849
|
document,
|
|
2612
2850
|
...opts.devicePixelRatio === void 0 ? {} : { devicePixelRatio: opts.devicePixelRatio },
|
|
2613
2851
|
...opts.onActivate ? { onActivate: (id) => opts.onActivate?.(id) } : {},
|
|
2614
|
-
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {}
|
|
2852
|
+
...opts.isCustom ? { isCustom: (a) => opts.isCustom?.(a) === true } : {},
|
|
2853
|
+
...opts.decorate ? { decorate: opts.decorate } : {}
|
|
2615
2854
|
});
|
|
2616
2855
|
return {
|
|
2617
2856
|
renderer,
|
|
@@ -2665,135 +2904,643 @@ function createToolOptions(tools = []) {
|
|
|
2665
2904
|
};
|
|
2666
2905
|
}
|
|
2667
2906
|
//#endregion
|
|
2668
|
-
//#region src/dom/tools/
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
/**
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
const hit = ctx.viewport.pageAtCss(e.clientX, e.clientY);
|
|
2688
|
-
if (!hit) return void 0;
|
|
2689
|
-
const css = cssIn(hit.host, e);
|
|
2690
|
-
return {
|
|
2691
|
-
...hit,
|
|
2692
|
-
css,
|
|
2693
|
-
user: hit.transform.fromCss(css)
|
|
2694
|
-
};
|
|
2695
|
-
}
|
|
2696
|
-
function createToolController$1(opts) {
|
|
2697
|
-
const byId = new Map(opts.tools.map((t) => [t.id, t]));
|
|
2698
|
-
const active = writable(opts.initial ?? "select-text");
|
|
2699
|
-
let current;
|
|
2700
|
-
const registry = opts.options ?? createToolOptions();
|
|
2701
|
-
registry.register(opts.tools);
|
|
2702
|
-
const ctx = {
|
|
2703
|
-
...opts.ctx,
|
|
2704
|
-
get signatures() {
|
|
2705
|
-
return opts.ctx.signatures;
|
|
2706
|
-
},
|
|
2707
|
-
toolStyle: opts.ctx.toolStyle ?? createToolStyle({
|
|
2708
|
-
store: opts.ctx.store,
|
|
2709
|
-
history: opts.ctx.history ?? { batch: (fn) => fn() },
|
|
2710
|
-
activeGroup: () => current?.styleGroup,
|
|
2711
|
-
active
|
|
2712
|
-
}),
|
|
2713
|
-
options: () => registry.get(current?.id ?? active.get())
|
|
2714
|
-
};
|
|
2715
|
-
const originalCursor = ctx.root.style.cursor;
|
|
2716
|
-
/** The pointer between a primary press and its release or cancel. */
|
|
2717
|
-
let pressed;
|
|
2718
|
-
/** Pointers whose release belongs to a tool that is no longer active. */
|
|
2719
|
-
const swallow = /* @__PURE__ */ new Set();
|
|
2720
|
-
/** True for events from a pointer other than the one whose press is live. */
|
|
2721
|
-
const foreign = (e) => pressed !== void 0 && pressed !== e.pointerId;
|
|
2722
|
-
const onDown = (e) => {
|
|
2723
|
-
if (!e.isPrimary || e.button !== 0 || inCustomContent(e.target)) return;
|
|
2724
|
-
if (foreign(e)) return;
|
|
2725
|
-
swallow.delete(e.pointerId);
|
|
2726
|
-
pressed = e.pointerId;
|
|
2727
|
-
const annotationId = (e.target instanceof Element ? e.target : null)?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
|
|
2728
|
-
if (current?.onPointerDown?.(e, hitFor(ctx, e), annotationId) === true) capture(e.pointerId);
|
|
2729
|
-
};
|
|
2730
|
-
const onMove = (e) => {
|
|
2731
|
-
if (!e.isPrimary || foreign(e)) return;
|
|
2732
|
-
if (pressed === e.pointerId && e.buttons === 0) pressed = void 0;
|
|
2733
|
-
current?.onPointerMove?.(e, hitFor(ctx, e));
|
|
2734
|
-
};
|
|
2735
|
-
const onUp = (e) => {
|
|
2736
|
-
if (!e.isPrimary || e.button !== 0 || foreign(e)) return;
|
|
2737
|
-
if (pressed === e.pointerId) pressed = void 0;
|
|
2738
|
-
if (swallow.delete(e.pointerId)) {
|
|
2739
|
-
release(e.pointerId);
|
|
2740
|
-
return;
|
|
2907
|
+
//#region src/dom/tools/selection-edit.ts
|
|
2908
|
+
const CORNERS = [
|
|
2909
|
+
"nw",
|
|
2910
|
+
"ne",
|
|
2911
|
+
"sw",
|
|
2912
|
+
"se"
|
|
2913
|
+
];
|
|
2914
|
+
const ENDS = ["start", "end"];
|
|
2915
|
+
const CALLOUT = "callout";
|
|
2916
|
+
/** Pointer travel, CSS px, before a press on an annotation becomes a move. */
|
|
2917
|
+
const DRAG_THRESHOLD = 3;
|
|
2918
|
+
function createSelectionEdit(ctx, opts = {}) {
|
|
2919
|
+
let mode = { kind: "idle" };
|
|
2920
|
+
const batch = (fn) => ctx.history ? ctx.history.batch(fn) : fn();
|
|
2921
|
+
const reset = () => {
|
|
2922
|
+
if (mode.kind === "marquee") mode.el.remove();
|
|
2923
|
+
if (mode.kind === "move") {
|
|
2924
|
+
for (const el of mode.els) el.style.transform = "";
|
|
2925
|
+
ctx.root.removeAttribute("data-ink-dragging");
|
|
2741
2926
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2927
|
+
if (mode.kind === "resize" && mode.el) {
|
|
2928
|
+
mode.el.style.transform = "";
|
|
2929
|
+
mode.el.style.transformOrigin = "";
|
|
2930
|
+
mode.el.style.visibility = "";
|
|
2931
|
+
}
|
|
2932
|
+
if (mode.kind === "resize") mode.preview?.remove();
|
|
2933
|
+
if (mode.kind === "endpoint") {
|
|
2934
|
+
mode.preview.remove();
|
|
2935
|
+
if (mode.el) mode.el.style.visibility = "";
|
|
2936
|
+
}
|
|
2937
|
+
mode = { kind: "idle" };
|
|
2744
2938
|
};
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2939
|
+
const elementIn = (root, id) => root.querySelector(`[data-annotation="${CSS.escape(id)}"]`) ?? void 0;
|
|
2940
|
+
const previewFor = (host, a) => {
|
|
2941
|
+
const preview = host.ownerDocument.createElement("div");
|
|
2942
|
+
preview.className = "ink-annotation ink-edit-preview";
|
|
2943
|
+
preview.dataset.inkPreview = "";
|
|
2944
|
+
preview.dataset.type = a.type;
|
|
2945
|
+
preview.style.cssText = "position:absolute;pointer-events:none;";
|
|
2946
|
+
return preview;
|
|
2751
2947
|
};
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
if (
|
|
2948
|
+
/** Shows `draft` in `preview` beside `el`, which is hidden meanwhile. */
|
|
2949
|
+
const showPreview = (preview, el, draft, start) => {
|
|
2950
|
+
drawPreview(preview, draft, start);
|
|
2951
|
+
if (el) el.style.visibility = "hidden";
|
|
2952
|
+
if (!preview.isConnected) (el?.parentElement ?? start.host).append(preview);
|
|
2756
2953
|
};
|
|
2757
|
-
function
|
|
2758
|
-
const
|
|
2759
|
-
if (
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2954
|
+
function startHandle(e, hit, a, handle) {
|
|
2955
|
+
const el = elementIn(hit.host, a.id);
|
|
2956
|
+
if (CORNERS.includes(handle) && hasCornerHandles(a)) {
|
|
2957
|
+
const text = a.type === "freetext";
|
|
2958
|
+
mode = {
|
|
2959
|
+
kind: "resize",
|
|
2960
|
+
id: a.id,
|
|
2961
|
+
start: hit,
|
|
2962
|
+
corner: handle,
|
|
2963
|
+
box: text ? hit.transform.toCssRect(freeTextBox(a)) : handleBox(a, hit.transform),
|
|
2964
|
+
el,
|
|
2965
|
+
locked: keepsAspect(a),
|
|
2966
|
+
preview: text && (a.fringe !== void 0 || hasCallout(a)) ? previewFor(hit.host, a) : void 0
|
|
2967
|
+
};
|
|
2968
|
+
} else if (ENDS.includes(handle) && a.type === "line" || handle === CALLOUT && hasCallout(a)) {
|
|
2969
|
+
const preview = previewFor(hit.host, a);
|
|
2970
|
+
mode = {
|
|
2971
|
+
kind: "endpoint",
|
|
2972
|
+
id: a.id,
|
|
2973
|
+
start: hit,
|
|
2974
|
+
which: handle,
|
|
2975
|
+
from: hit.transform.toCss(endOf(a, handle)),
|
|
2976
|
+
el,
|
|
2977
|
+
preview
|
|
2978
|
+
};
|
|
2979
|
+
} else return false;
|
|
2980
|
+
e.stopPropagation();
|
|
2764
2981
|
e.preventDefault();
|
|
2765
|
-
|
|
2766
|
-
}
|
|
2767
|
-
function capture(id) {
|
|
2768
|
-
try {
|
|
2769
|
-
ctx.root.setPointerCapture?.(id);
|
|
2770
|
-
} catch {}
|
|
2771
|
-
}
|
|
2772
|
-
function release(id) {
|
|
2773
|
-
try {
|
|
2774
|
-
if (ctx.root.hasPointerCapture?.(id)) ctx.root.releasePointerCapture(id);
|
|
2775
|
-
} catch {}
|
|
2982
|
+
return true;
|
|
2776
2983
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2984
|
+
return {
|
|
2985
|
+
reset,
|
|
2986
|
+
onPointerDown(e, hit, annotationId) {
|
|
2987
|
+
if (!hit) return;
|
|
2988
|
+
reset();
|
|
2989
|
+
if (annotationId !== void 0) {
|
|
2990
|
+
const a = ctx.store.get(annotationId);
|
|
2991
|
+
if (!a) return;
|
|
2992
|
+
const markup = isTextMarkup(a);
|
|
2993
|
+
if (markup && !opts.marquee) return;
|
|
2994
|
+
const handle = handleOf(e.target);
|
|
2995
|
+
if (handle !== void 0 && !a.locked && startHandle(e, hit, a, handle)) return true;
|
|
2996
|
+
const selected = ctx.store.selection.get();
|
|
2997
|
+
if (e.shiftKey) {
|
|
2998
|
+
e.stopPropagation();
|
|
2999
|
+
ctx.store.select(selected.includes(annotationId) ? selected.filter((s) => s !== annotationId) : [...selected, annotationId]);
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
if (inText(e.target) && canEditFreeText(a, elementIn(hit.host, annotationId))) {
|
|
3003
|
+
e.stopPropagation();
|
|
3004
|
+
ctx.store.select([annotationId]);
|
|
3005
|
+
ctx.onActivate?.(annotationId);
|
|
3006
|
+
mode = {
|
|
3007
|
+
kind: "edit",
|
|
3008
|
+
id: annotationId,
|
|
3009
|
+
start: hit,
|
|
3010
|
+
caret: {
|
|
3011
|
+
clientX: e.clientX,
|
|
3012
|
+
clientY: e.clientY
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
e.preventDefault();
|
|
3016
|
+
return true;
|
|
3017
|
+
}
|
|
3018
|
+
const narrow = selected.length > 1 && selected.includes(annotationId);
|
|
3019
|
+
if (narrow) e.stopPropagation();
|
|
3020
|
+
else if (markup) {
|
|
3021
|
+
e.stopPropagation();
|
|
3022
|
+
ctx.store.select([annotationId]);
|
|
3023
|
+
ctx.onActivate?.(annotationId);
|
|
3024
|
+
}
|
|
3025
|
+
mode = {
|
|
3026
|
+
kind: "press",
|
|
3027
|
+
id: annotationId,
|
|
3028
|
+
start: hit,
|
|
3029
|
+
narrow
|
|
3030
|
+
};
|
|
3031
|
+
e.preventDefault();
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
3034
|
+
if (!opts.marquee) {
|
|
3035
|
+
if (!e.shiftKey) ctx.store.select([]);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const el = hit.host.ownerDocument.createElement("div");
|
|
3039
|
+
el.className = "ink-marquee";
|
|
3040
|
+
el.dataset.inkMarquee = "";
|
|
3041
|
+
el.style.cssText = `position:absolute;pointer-events:none;left:${hit.css.x}px;top:${hit.css.y}px;width:0;height:0;`;
|
|
3042
|
+
hit.host.append(el);
|
|
3043
|
+
mode = {
|
|
3044
|
+
kind: "marquee",
|
|
3045
|
+
start: hit,
|
|
3046
|
+
el,
|
|
3047
|
+
additive: e.shiftKey
|
|
3048
|
+
};
|
|
3049
|
+
if (!e.shiftKey) ctx.store.select([]);
|
|
3050
|
+
e.preventDefault();
|
|
3051
|
+
return true;
|
|
3052
|
+
},
|
|
3053
|
+
onPointerMove(e) {
|
|
3054
|
+
if (mode.kind === "idle") return;
|
|
3055
|
+
if (e.buttons === 0 || !mode.start.host.isConnected) {
|
|
3056
|
+
reset();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
const { start } = mode;
|
|
3060
|
+
if (mode.kind === "edit") return;
|
|
3061
|
+
if (mode.kind === "resize") {
|
|
3062
|
+
const { box, el, preview } = mode;
|
|
3063
|
+
const next = resizeCss(box, mode.corner, cornerAt(mode, e), minCss(start), {
|
|
3064
|
+
aspect: mode.locked || e.shiftKey,
|
|
3065
|
+
page: start.transform
|
|
3066
|
+
});
|
|
3067
|
+
if (preview) {
|
|
3068
|
+
const a = ctx.store.get(mode.id);
|
|
3069
|
+
if (a?.type !== "freetext") return;
|
|
3070
|
+
const draft = {
|
|
3071
|
+
...a,
|
|
3072
|
+
...resizedBox(a, userBox(start.transform, next))
|
|
3073
|
+
};
|
|
3074
|
+
showPreview(preview, el, draft, start);
|
|
3075
|
+
} else if (el) {
|
|
3076
|
+
const ox = box.left - (parseFloat(el.style.left) || 0);
|
|
3077
|
+
const oy = box.top - (parseFloat(el.style.top) || 0);
|
|
3078
|
+
el.style.transformOrigin = `${ox}px ${oy}px`;
|
|
3079
|
+
el.style.transform = `translate(${next.left - box.left}px, ${next.top - box.top}px) scale(${next.width / box.width}, ${next.height / box.height})`;
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
if (mode.kind === "endpoint") {
|
|
3084
|
+
const a = ctx.store.get(mode.id);
|
|
3085
|
+
if (!a) return;
|
|
3086
|
+
const patch = endPatch(a, mode.which, endAt(mode, e));
|
|
3087
|
+
if (!patch) return;
|
|
3088
|
+
showPreview(mode.preview, mode.el, {
|
|
3089
|
+
...a,
|
|
3090
|
+
...patch
|
|
3091
|
+
}, start);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const css = cssIn(start.host, e);
|
|
3095
|
+
const dx = css.x - start.css.x;
|
|
3096
|
+
const dy = css.y - start.css.y;
|
|
3097
|
+
if (mode.kind === "press") {
|
|
3098
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
3099
|
+
const a = ctx.store.get(mode.id);
|
|
3100
|
+
if (!a || a.locked) {
|
|
3101
|
+
mode = { kind: "idle" };
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
const selected = ctx.store.selection.get();
|
|
3105
|
+
const pressed = mode.id;
|
|
3106
|
+
const ids = selected.includes(pressed) ? selected.filter((id) => {
|
|
3107
|
+
const m = ctx.store.get(id);
|
|
3108
|
+
return m !== void 0 && !m.locked && (id === pressed || !isTextMarkup(m));
|
|
3109
|
+
}) : [pressed];
|
|
3110
|
+
mode = {
|
|
3111
|
+
kind: "move",
|
|
3112
|
+
ids,
|
|
3113
|
+
start,
|
|
3114
|
+
els: ids.map((id) => elementIn(ctx.root, id)).filter((el) => el !== void 0)
|
|
3115
|
+
};
|
|
3116
|
+
ctx.root.setAttribute("data-ink-dragging", "");
|
|
3117
|
+
}
|
|
3118
|
+
if (mode.kind === "move") {
|
|
3119
|
+
for (const el of mode.els) el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
mode.el.style.left = `${Math.min(start.css.x, css.x)}px`;
|
|
3123
|
+
mode.el.style.top = `${Math.min(start.css.y, css.y)}px`;
|
|
3124
|
+
mode.el.style.width = `${Math.abs(dx)}px`;
|
|
3125
|
+
mode.el.style.height = `${Math.abs(dy)}px`;
|
|
3126
|
+
},
|
|
3127
|
+
onPointerCancel() {
|
|
3128
|
+
reset();
|
|
3129
|
+
},
|
|
3130
|
+
onPointerUp(e) {
|
|
3131
|
+
const current = mode;
|
|
3132
|
+
reset();
|
|
3133
|
+
if (current.kind !== "idle" && !current.start.host.isConnected) return;
|
|
3134
|
+
switch (current.kind) {
|
|
3135
|
+
case "press":
|
|
3136
|
+
if (current.narrow) {
|
|
3137
|
+
ctx.store.select([current.id]);
|
|
3138
|
+
ctx.onActivate?.(current.id);
|
|
3139
|
+
}
|
|
3140
|
+
return;
|
|
3141
|
+
case "edit": {
|
|
3142
|
+
const el = elementIn(current.start.host, current.id);
|
|
3143
|
+
const a = ctx.store.get(current.id);
|
|
3144
|
+
if (!el || !canEditFreeText(a, el) || editorFor(el)) return;
|
|
3145
|
+
editFreeTextIn(ctx.store, el, a.id, ctx.viewport.transformFor(a.page), { caret: current.caret });
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
case "move": {
|
|
3149
|
+
const { start } = current;
|
|
3150
|
+
const end = start.transform.fromCss(cssIn(start.host, e));
|
|
3151
|
+
const dx = end.x - start.user.x;
|
|
3152
|
+
const dy = end.y - start.user.y;
|
|
3153
|
+
batch(() => {
|
|
3154
|
+
for (const id of current.ids) {
|
|
3155
|
+
const a = ctx.store.get(id);
|
|
3156
|
+
if (a && !a.locked) ctx.store.update(id, translated(a, dx, dy));
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
case "resize": {
|
|
3162
|
+
const { start, box } = current;
|
|
3163
|
+
const next = resizeCss(box, current.corner, cornerAt(current, e), minCss(start), {
|
|
3164
|
+
aspect: current.locked || e.shiftKey,
|
|
3165
|
+
page: start.transform
|
|
3166
|
+
});
|
|
3167
|
+
if (next.left === box.left && next.top === box.top && next.width === box.width && next.height === box.height) return;
|
|
3168
|
+
const a = ctx.store.get(current.id);
|
|
3169
|
+
if (!a || a.locked) return;
|
|
3170
|
+
const to = userBox(start.transform, next);
|
|
3171
|
+
if (a.type === "freetext") {
|
|
3172
|
+
ctx.store.update(a.id, resizedBox(a, to));
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
const from = hasRectGeometry(a) ? a.rect : start.transform.fromCssRect(box);
|
|
3176
|
+
ctx.store.update(a.id, scaled(a, from, to));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
case "endpoint": {
|
|
3180
|
+
const a = ctx.store.get(current.id);
|
|
3181
|
+
if (!a || a.locked) return;
|
|
3182
|
+
const css = cssIn(current.start.host, e);
|
|
3183
|
+
if (css.x === current.start.css.x && css.y === current.start.css.y) return;
|
|
3184
|
+
const patch = endPatch(a, current.which, endAt(current, e));
|
|
3185
|
+
if (patch) ctx.store.update(a.id, patch);
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
case "marquee": {
|
|
3189
|
+
const { start } = current;
|
|
3190
|
+
const box = rectFromPoints(start.user, start.transform.fromCss(cssIn(start.host, e)));
|
|
3191
|
+
if (box.width === 0 && box.height === 0) return;
|
|
3192
|
+
const ids = withoutReplies(ctx.store.list.get()).filter((a) => a.page === start.page && rectsIntersect(a.rect, box)).map((a) => a.id);
|
|
3193
|
+
ctx.store.select(current.additive ? [.../* @__PURE__ */ new Set([...ctx.store.selection.get(), ...ids])] : ids);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
},
|
|
3198
|
+
onKeyDown(e) {
|
|
3199
|
+
if (e.key === "Escape") {
|
|
3200
|
+
reset();
|
|
3201
|
+
ctx.store.select([]);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
3205
|
+
if (e.defaultPrevented) return;
|
|
3206
|
+
if ((e.target instanceof Element ? e.target : null)?.closest("[data-annotation], input, textarea, [contenteditable]:not([contenteditable=\"false\"])")) return;
|
|
3207
|
+
for (const id of ctx.store.selection.get()) if (ctx.store.get(id)?.locked !== true) ctx.store.remove(id);
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
/** The handle (`nw`, `start`, ...) a press landed on, if it landed on one. */
|
|
3212
|
+
function handleOf(target) {
|
|
3213
|
+
if (!(target instanceof Element)) return void 0;
|
|
3214
|
+
return target.closest("[data-handle]")?.getAttribute("data-handle") ?? void 0;
|
|
3215
|
+
}
|
|
3216
|
+
/** True when a press landed inside a free text's drawn text box. */
|
|
3217
|
+
function inText(target) {
|
|
3218
|
+
return target instanceof Element && target.closest(".ink-free-text") !== null;
|
|
3219
|
+
}
|
|
3220
|
+
/** The point a line's end or a callout's tip handle sits on. */
|
|
3221
|
+
function endOf(a, which) {
|
|
3222
|
+
if (which === "callout") return hasCallout(a) ? a.callout[0] : void 0;
|
|
3223
|
+
return a.type === "line" ? a[which] : void 0;
|
|
3224
|
+
}
|
|
3225
|
+
/** The patch that moves a line's end, or a callout's tip, to `to`; undefined for neither. */
|
|
3226
|
+
function endPatch(a, which, to) {
|
|
3227
|
+
if (which === "callout") return hasCallout(a) ? movedTip(a, to) : void 0;
|
|
3228
|
+
return a.type === "line" ? movedEndpoint(a, which, to) : void 0;
|
|
3229
|
+
}
|
|
3230
|
+
/** A resized CSS box in user space, at least `MIN_SIZE` a side. */
|
|
3231
|
+
function userBox(t, css) {
|
|
3232
|
+
const r = t.fromCssRect(css);
|
|
3233
|
+
return {
|
|
3234
|
+
...r,
|
|
3235
|
+
width: Math.max(1, r.width),
|
|
3236
|
+
height: Math.max(1, r.height)
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
/** Draws `a` into the preview element at its own box. */
|
|
3240
|
+
function drawPreview(el, a, start) {
|
|
3241
|
+
const box = annotationBox(a, start.transform);
|
|
3242
|
+
el.style.left = `${box.left}px`;
|
|
3243
|
+
el.style.top = `${box.top}px`;
|
|
3244
|
+
el.style.width = `${box.width}px`;
|
|
3245
|
+
el.style.height = `${box.height}px`;
|
|
3246
|
+
renderAnnotationContent(el, a, start.transform);
|
|
3247
|
+
}
|
|
3248
|
+
/** Keeps a CSS point on the page. */
|
|
3249
|
+
function onPage(start, x, y) {
|
|
3250
|
+
const { cssWidth, cssHeight } = start.transform;
|
|
3251
|
+
return {
|
|
3252
|
+
x: Math.min(Math.max(x, 0), cssWidth),
|
|
3253
|
+
y: Math.min(Math.max(y, 0), cssHeight)
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Where the dragged line end is now, in user space: where it started plus the pointer's
|
|
3258
|
+
* travel, kept on the page.
|
|
3259
|
+
*/
|
|
3260
|
+
function endAt(mode, e) {
|
|
3261
|
+
const { start, from } = mode;
|
|
3262
|
+
const css = cssIn(start.host, e);
|
|
3263
|
+
return start.transform.fromCss(onPage(start, from.x + css.x - start.css.x, from.y + css.y - start.css.y));
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Where the dragged corner is now: where it started plus the pointer's travel since the
|
|
3267
|
+
* press, kept on the page. Keeping the press's offset from the corner means a click on a
|
|
3268
|
+
* handle, with no drag, leaves the box as it was.
|
|
3269
|
+
*/
|
|
3270
|
+
function cornerAt(mode, e) {
|
|
3271
|
+
const { start, box, corner } = mode;
|
|
3272
|
+
const css = cssIn(start.host, e);
|
|
3273
|
+
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);
|
|
3274
|
+
}
|
|
3275
|
+
/** `MIN_SIZE` points as CSS px on the press's page. */
|
|
3276
|
+
function minCss(start) {
|
|
3277
|
+
return start.transform.toCssLength(1);
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* `box` with its `corner` dragged to `p` and the opposite corner fixed, at least `min` a side.
|
|
3281
|
+
* With `aspect`, the box keeps its proportions: it scales by the larger of the two axes'
|
|
3282
|
+
* changes, kept on the page.
|
|
3283
|
+
*/
|
|
3284
|
+
function resizeCss(box, corner, p, min, opts) {
|
|
3285
|
+
const west = corner[1] === "w";
|
|
3286
|
+
const north = corner[0] === "n";
|
|
3287
|
+
/** The fixed corner. */
|
|
3288
|
+
const fx = west ? box.left + box.width : box.left;
|
|
3289
|
+
const fy = north ? box.top + box.height : box.top;
|
|
3290
|
+
let width = Math.max(west ? fx - p.x : p.x - fx, min);
|
|
3291
|
+
let height = Math.max(north ? fy - p.y : p.y - fy, min);
|
|
3292
|
+
if (opts.aspect && box.width > 0 && box.height > 0) {
|
|
3293
|
+
const roomX = west ? fx : opts.page.cssWidth - fx;
|
|
3294
|
+
const roomY = north ? fy : opts.page.cssHeight - fy;
|
|
3295
|
+
const lo = min / Math.min(box.width, box.height);
|
|
3296
|
+
const hi = Math.min(roomX / box.width, roomY / box.height);
|
|
3297
|
+
const s = Math.min(Math.max(width / box.width, height / box.height, lo), Math.max(hi, lo));
|
|
3298
|
+
width = box.width * s;
|
|
3299
|
+
height = box.height * s;
|
|
3300
|
+
}
|
|
3301
|
+
return {
|
|
3302
|
+
left: west ? fx - width : fx,
|
|
3303
|
+
top: north ? fy - height : fy,
|
|
3304
|
+
width,
|
|
3305
|
+
height
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
//#endregion
|
|
3309
|
+
//#region src/dom/tools/controller.ts
|
|
3310
|
+
/** Host-relative CSS point for a client point. */
|
|
3311
|
+
function cssIn(host, e) {
|
|
3312
|
+
const r = host.getBoundingClientRect();
|
|
3313
|
+
return {
|
|
3314
|
+
x: e.clientX - r.left,
|
|
3315
|
+
y: e.clientY - r.top
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
/** Like `cssIn`, clamped to the host's box, so a drag past the page edge stops at the edge. */
|
|
3319
|
+
function clampedCssIn(host, e) {
|
|
3320
|
+
const r = host.getBoundingClientRect();
|
|
3321
|
+
return {
|
|
3322
|
+
x: Math.min(Math.max(e.clientX - r.left, 0), r.width),
|
|
3323
|
+
y: Math.min(Math.max(e.clientY - r.top, 0), r.height)
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
/** The mounted page under a client point, with the point in CSS px and user space. */
|
|
3327
|
+
function hitFor(ctx, e) {
|
|
3328
|
+
const hit = ctx.viewport.pageAtCss(e.clientX, e.clientY);
|
|
3329
|
+
if (!hit) return void 0;
|
|
3330
|
+
const css = cssIn(hit.host, e);
|
|
3331
|
+
return {
|
|
3332
|
+
...hit,
|
|
3333
|
+
css,
|
|
3334
|
+
user: hit.transform.fromCss(css)
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
function createToolController$1(opts) {
|
|
3338
|
+
const byId = new Map(opts.tools.map((t) => [t.id, t]));
|
|
3339
|
+
const active = writable(opts.initial ?? "select-text");
|
|
3340
|
+
let current;
|
|
3341
|
+
const registry = opts.options ?? createToolOptions();
|
|
3342
|
+
registry.register(opts.tools);
|
|
3343
|
+
const ctx = {
|
|
3344
|
+
...opts.ctx,
|
|
3345
|
+
get signatures() {
|
|
3346
|
+
return opts.ctx.signatures;
|
|
3347
|
+
},
|
|
3348
|
+
toolStyle: opts.ctx.toolStyle ?? createToolStyle({
|
|
3349
|
+
store: opts.ctx.store,
|
|
3350
|
+
history: opts.ctx.history ?? { batch: (fn) => fn() },
|
|
3351
|
+
activeGroup: () => current?.styleGroup,
|
|
3352
|
+
active
|
|
3353
|
+
}),
|
|
3354
|
+
options: () => registry.get(current?.id ?? active.get()),
|
|
3355
|
+
made(ids) {
|
|
3356
|
+
if (current?.kind !== "one-shot" || ids.length === 0) return;
|
|
3357
|
+
clearMade();
|
|
3358
|
+
made = [...ids];
|
|
3359
|
+
ctx.root.dataset.inkMade = "";
|
|
3360
|
+
stopMade = [ctx.store.list.subscribe(checkMade), ctx.store.selection.subscribe(checkMade)];
|
|
3361
|
+
},
|
|
3362
|
+
done() {
|
|
3363
|
+
clearMade();
|
|
3364
|
+
ctx.store.select([]);
|
|
3365
|
+
set("select-text");
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
const originalCursor = ctx.root.style.cursor;
|
|
3369
|
+
/** The pointer between a primary press and its release or cancel. */
|
|
3370
|
+
let pressed;
|
|
3371
|
+
/** Pointers whose release belongs to a tool that is no longer active. */
|
|
3372
|
+
const swallow = /* @__PURE__ */ new Set();
|
|
3373
|
+
/** What the active one-shot tool made; undefined while it is ready. */
|
|
3374
|
+
let made;
|
|
3375
|
+
let stopMade = [];
|
|
3376
|
+
/** Whether the press now under way found the tool made (see the top of the file). */
|
|
3377
|
+
let pressFoundMade = false;
|
|
3378
|
+
/** Moves and resizes what the active one-shot tool made. */
|
|
3379
|
+
let madeEdit;
|
|
3380
|
+
/** The press now under way went to `madeEdit`, not the tool. */
|
|
3381
|
+
let editingMade = false;
|
|
3382
|
+
function clearMade() {
|
|
3383
|
+
stopMade.forEach((stop) => stop());
|
|
3384
|
+
stopMade = [];
|
|
3385
|
+
made = void 0;
|
|
3386
|
+
delete ctx.root.dataset.inkMade;
|
|
3387
|
+
}
|
|
3388
|
+
/** Leaves the made state when its items are gone, or switches when the selection moved on. */
|
|
3389
|
+
function checkMade() {
|
|
3390
|
+
if (!made) return;
|
|
3391
|
+
const store = ctx.store;
|
|
3392
|
+
if (!made.some((id) => (store.get(id) ?? store._getProposed(id)) !== void 0)) {
|
|
3393
|
+
clearMade();
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
const selected = store.selection.get();
|
|
3397
|
+
if (!made.some((id) => selected.includes(id))) set("select-text");
|
|
3398
|
+
}
|
|
3399
|
+
/** True for events from a pointer other than the one whose press is live. */
|
|
3400
|
+
const foreign = (e) => pressed !== void 0 && pressed !== e.pointerId;
|
|
3401
|
+
const readFirst = () => {
|
|
3402
|
+
pressFoundMade = made !== void 0;
|
|
3403
|
+
};
|
|
3404
|
+
const readAfterEditor = () => {
|
|
3405
|
+
pressFoundMade ||= made !== void 0;
|
|
3406
|
+
};
|
|
3407
|
+
const onDown = (e) => {
|
|
3408
|
+
if (!e.isPrimary || e.button !== 0 || inCustomContent(e.target)) return;
|
|
3409
|
+
if (foreign(e)) return;
|
|
3410
|
+
swallow.delete(e.pointerId);
|
|
3411
|
+
const annotationId = (e.target instanceof Element ? e.target : null)?.closest("[data-annotation]")?.getAttribute("data-annotation") ?? void 0;
|
|
3412
|
+
if (current?.kind === "one-shot") {
|
|
3413
|
+
if (pressFoundMade && annotationId === void 0 && !onScrollBar(e, ctx.root)) {
|
|
3414
|
+
e.preventDefault();
|
|
3415
|
+
swallow.add(e.pointerId);
|
|
3416
|
+
ctx.done();
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
const a = annotationId === void 0 ? void 0 : ctx.store.get(annotationId);
|
|
3420
|
+
if (a && made?.includes(a.id) && madeEdit) {
|
|
3421
|
+
pressed = e.pointerId;
|
|
3422
|
+
editingMade = true;
|
|
3423
|
+
if (madeEdit.onPointerDown(e, hitFor(ctx, e), a.id) === true) capture(e.pointerId);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
if (a && (!isTextMarkup(a) || pressFoundMade)) set("select-text");
|
|
3427
|
+
}
|
|
3428
|
+
pressed = e.pointerId;
|
|
3429
|
+
if (current?.onPointerDown?.(e, hitFor(ctx, e), annotationId) === true) capture(e.pointerId);
|
|
3430
|
+
};
|
|
3431
|
+
const onMove = (e) => {
|
|
3432
|
+
if (!e.isPrimary || foreign(e)) return;
|
|
3433
|
+
const missed = pressed === e.pointerId && e.buttons === 0;
|
|
3434
|
+
if (missed) pressed = void 0;
|
|
3435
|
+
if (editingMade) {
|
|
3436
|
+
madeEdit?.onPointerMove(e, hitFor(ctx, e));
|
|
3437
|
+
if (missed) editingMade = false;
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
current?.onPointerMove?.(e, hitFor(ctx, e));
|
|
3441
|
+
};
|
|
3442
|
+
const onUp = (e) => {
|
|
3443
|
+
if (!e.isPrimary || e.button !== 0 || foreign(e)) return;
|
|
3444
|
+
if (pressed === e.pointerId) pressed = void 0;
|
|
3445
|
+
if (swallow.delete(e.pointerId)) {
|
|
3446
|
+
release(e.pointerId);
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
if (editingMade) {
|
|
3450
|
+
editingMade = false;
|
|
3451
|
+
madeEdit?.onPointerUp(e, hitFor(ctx, e));
|
|
3452
|
+
} else current?.onPointerUp?.(e, hitFor(ctx, e));
|
|
3453
|
+
release(e.pointerId);
|
|
3454
|
+
};
|
|
3455
|
+
const onCancel = (e) => {
|
|
3456
|
+
if (!e.isPrimary || foreign(e)) return;
|
|
3457
|
+
if (e.type === "lostpointercapture" && pressed !== e.pointerId) return;
|
|
3458
|
+
if (pressed === e.pointerId) pressed = void 0;
|
|
3459
|
+
swallow.delete(e.pointerId);
|
|
3460
|
+
if (editingMade) {
|
|
3461
|
+
editingMade = false;
|
|
3462
|
+
madeEdit?.onPointerCancel();
|
|
3463
|
+
} else current?.onPointerCancel?.(e);
|
|
3464
|
+
};
|
|
3465
|
+
const onKey = (e) => {
|
|
3466
|
+
if (inCustomContent(e.target)) return;
|
|
3467
|
+
if (made && e.key === "Escape" && !e.defaultPrevented) {
|
|
3468
|
+
e.preventDefault();
|
|
3469
|
+
ctx.done();
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
if (made && (e.key === "Delete" || e.key === "Backspace")) {
|
|
3473
|
+
madeEdit?.onKeyDown(e);
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
current?.onKeyDown?.(e);
|
|
3477
|
+
if (isViewportEnter(e, ctx.root)) editSelectedText(e);
|
|
3478
|
+
};
|
|
3479
|
+
/**
|
|
3480
|
+
* Escape in an editor on a made text box, however it was opened (a press inside, Enter, a
|
|
3481
|
+
* double-click): commits and ends the tool. Captured, as the editor keeps its keys to itself.
|
|
3482
|
+
*/
|
|
3483
|
+
const onEditorEscape = (e) => {
|
|
3484
|
+
if (!made || e.key !== "Escape" || e.isComposing || e.defaultPrevented) return;
|
|
3485
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
3486
|
+
if (!target?.closest("[data-ink-editor]")) return;
|
|
3487
|
+
const el = target.closest("[data-annotation]");
|
|
3488
|
+
const id = el?.getAttribute("data-annotation");
|
|
3489
|
+
if (!el || !id || !made.includes(id)) return;
|
|
3490
|
+
e.preventDefault();
|
|
3491
|
+
e.stopPropagation();
|
|
3492
|
+
editorFor(el)?.close(true);
|
|
3493
|
+
ctx.done();
|
|
3494
|
+
};
|
|
3495
|
+
function editSelectedText(e) {
|
|
3496
|
+
const selected = ctx.store.selection.get();
|
|
3497
|
+
if (selected.length !== 1) return;
|
|
3498
|
+
const id = selected[0];
|
|
3499
|
+
const a = ctx.store.get(id);
|
|
3500
|
+
const el = ctx.root.querySelector(`[data-annotation="${CSS.escape(id)}"]`);
|
|
3501
|
+
if (!canEditFreeText(a, el) || !el) return;
|
|
3502
|
+
e.preventDefault();
|
|
3503
|
+
editFreeTextIn(ctx.store, el, id, ctx.viewport.transformFor(a.page));
|
|
3504
|
+
}
|
|
3505
|
+
function capture(id) {
|
|
3506
|
+
try {
|
|
3507
|
+
ctx.root.setPointerCapture?.(id);
|
|
3508
|
+
} catch {}
|
|
3509
|
+
}
|
|
3510
|
+
function release(id) {
|
|
3511
|
+
try {
|
|
3512
|
+
if (ctx.root.hasPointerCapture?.(id)) ctx.root.releasePointerCapture(id);
|
|
3513
|
+
} catch {}
|
|
3514
|
+
}
|
|
3515
|
+
const doc = ctx.root.ownerDocument;
|
|
3516
|
+
doc.defaultView?.addEventListener("pointerdown", readFirst, { capture: true });
|
|
3517
|
+
doc.addEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
3518
|
+
ctx.root.addEventListener("pointerdown", onDown, { capture: true });
|
|
3519
|
+
ctx.root.addEventListener("pointermove", onMove);
|
|
3520
|
+
ctx.root.addEventListener("pointerup", onUp);
|
|
3521
|
+
ctx.root.addEventListener("pointercancel", onCancel);
|
|
3522
|
+
ctx.root.addEventListener("lostpointercapture", onCancel);
|
|
3523
|
+
ctx.root.addEventListener("keydown", onKey);
|
|
3524
|
+
ctx.root.addEventListener("keydown", onEditorEscape, { capture: true });
|
|
3525
|
+
function set(id, options) {
|
|
2784
3526
|
const next = byId.get(id);
|
|
2785
3527
|
if (!next) throw new RangeError(`Unknown tool ${id}`);
|
|
2786
3528
|
if (options) registry.merge(id, options);
|
|
2787
3529
|
if (pressed !== void 0) {
|
|
2788
|
-
|
|
3530
|
+
if (editingMade) madeEdit?.onPointerCancel();
|
|
3531
|
+
else current?.onPointerCancel?.(new PointerEvent("pointercancel", {
|
|
2789
3532
|
pointerId: pressed,
|
|
2790
3533
|
isPrimary: true
|
|
2791
3534
|
}));
|
|
2792
3535
|
swallow.add(pressed);
|
|
2793
3536
|
pressed = void 0;
|
|
2794
3537
|
}
|
|
3538
|
+
editingMade = false;
|
|
3539
|
+
madeEdit?.reset();
|
|
2795
3540
|
current?.deactivate();
|
|
3541
|
+
clearMade();
|
|
2796
3542
|
current = next;
|
|
3543
|
+
madeEdit = next.kind === "one-shot" ? createSelectionEdit(ctx) : void 0;
|
|
2797
3544
|
next.activate(ctx);
|
|
2798
3545
|
ctx.root.dataset.inkTool = id;
|
|
2799
3546
|
ctx.root.style.cursor = next.cursor;
|
|
@@ -2812,14 +3559,20 @@ function createToolController$1(opts) {
|
|
|
2812
3559
|
return registry.readable(id);
|
|
2813
3560
|
},
|
|
2814
3561
|
destroy() {
|
|
3562
|
+
madeEdit?.reset();
|
|
3563
|
+
madeEdit = void 0;
|
|
2815
3564
|
current?.deactivate();
|
|
3565
|
+
clearMade();
|
|
2816
3566
|
current = void 0;
|
|
3567
|
+
doc.defaultView?.removeEventListener("pointerdown", readFirst, { capture: true });
|
|
3568
|
+
doc.removeEventListener("pointerdown", readAfterEditor, { capture: true });
|
|
2817
3569
|
ctx.root.removeEventListener("pointerdown", onDown, { capture: true });
|
|
2818
3570
|
ctx.root.removeEventListener("pointermove", onMove);
|
|
2819
3571
|
ctx.root.removeEventListener("pointerup", onUp);
|
|
2820
3572
|
ctx.root.removeEventListener("pointercancel", onCancel);
|
|
2821
3573
|
ctx.root.removeEventListener("lostpointercapture", onCancel);
|
|
2822
3574
|
ctx.root.removeEventListener("keydown", onKey);
|
|
3575
|
+
ctx.root.removeEventListener("keydown", onEditorEscape, { capture: true });
|
|
2823
3576
|
delete ctx.root.dataset.inkTool;
|
|
2824
3577
|
ctx.root.style.cursor = originalCursor;
|
|
2825
3578
|
}
|
|
@@ -2841,6 +3594,17 @@ function isViewportEnter(e, root) {
|
|
|
2841
3594
|
return target.closest("[data-ink-text-layer], [data-ink-canvas-layer]") !== null;
|
|
2842
3595
|
}
|
|
2843
3596
|
/**
|
|
3597
|
+
* True for a press on `root` itself outside its client area: on a scroll bar (or its border).
|
|
3598
|
+
* Client coordinates only, so a synthetic press gives the same answer as a real one.
|
|
3599
|
+
*/
|
|
3600
|
+
function onScrollBar(e, root) {
|
|
3601
|
+
if (e.target !== root) return false;
|
|
3602
|
+
const r = root.getBoundingClientRect();
|
|
3603
|
+
const x = e.clientX - r.left - root.clientLeft;
|
|
3604
|
+
const y = e.clientY - r.top - root.clientTop;
|
|
3605
|
+
return x < 0 || y < 0 || x >= root.clientWidth || y >= root.clientHeight;
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
2844
3608
|
* True for a target inside a custom annotation element (`data-custom`) but not that element
|
|
2845
3609
|
* itself: the host's buttons and inputs there own their presses and keys, as in the overlay.
|
|
2846
3610
|
*/
|
|
@@ -2917,6 +3681,7 @@ function freehandTool(opts) {
|
|
|
2917
3681
|
};
|
|
2918
3682
|
return {
|
|
2919
3683
|
id: opts.id,
|
|
3684
|
+
kind: "sticky",
|
|
2920
3685
|
cursor: "crosshair",
|
|
2921
3686
|
styleGroup: "ink",
|
|
2922
3687
|
activate(c) {
|
|
@@ -3123,7 +3888,7 @@ function freetextTool() {
|
|
|
3123
3888
|
};
|
|
3124
3889
|
const editor = editFreeText(el, draftBox, {
|
|
3125
3890
|
transform: t,
|
|
3126
|
-
commit(contents, rect) {
|
|
3891
|
+
commit(contents, rect, _grown, how) {
|
|
3127
3892
|
done();
|
|
3128
3893
|
if (contents.trim() === "") return refocus();
|
|
3129
3894
|
const created = c.store.add({
|
|
@@ -3133,7 +3898,9 @@ function freetextTool() {
|
|
|
3133
3898
|
rect
|
|
3134
3899
|
});
|
|
3135
3900
|
c.store.select([created.id]);
|
|
3901
|
+
c.made([created.id]);
|
|
3136
3902
|
refocus(created.id);
|
|
3903
|
+
if (how === "escape") c.done();
|
|
3137
3904
|
},
|
|
3138
3905
|
cancel() {
|
|
3139
3906
|
done();
|
|
@@ -3159,6 +3926,7 @@ function freetextTool() {
|
|
|
3159
3926
|
};
|
|
3160
3927
|
return {
|
|
3161
3928
|
id: "freetext",
|
|
3929
|
+
kind: "one-shot",
|
|
3162
3930
|
cursor: "text",
|
|
3163
3931
|
styleGroup: "text",
|
|
3164
3932
|
defaultOptions: {},
|
|
@@ -3263,6 +4031,7 @@ function highlightTool() {
|
|
|
3263
4031
|
};
|
|
3264
4032
|
return {
|
|
3265
4033
|
id: "highlight",
|
|
4034
|
+
kind: "sticky",
|
|
3266
4035
|
cursor: "text",
|
|
3267
4036
|
styleGroup: "markup",
|
|
3268
4037
|
defaultOptions: { ...DEFAULT_HIGHLIGHT_OPTIONS },
|
|
@@ -3306,10 +4075,12 @@ function noteTool() {
|
|
|
3306
4075
|
}
|
|
3307
4076
|
});
|
|
3308
4077
|
c.store.select([a.id]);
|
|
4078
|
+
c.made([a.id]);
|
|
3309
4079
|
return a.id;
|
|
3310
4080
|
};
|
|
3311
4081
|
return {
|
|
3312
4082
|
id: "note",
|
|
4083
|
+
kind: "one-shot",
|
|
3313
4084
|
cursor: "copy",
|
|
3314
4085
|
activate(c) {
|
|
3315
4086
|
ctx = c;
|
|
@@ -3363,487 +4134,122 @@ function focusWhenDrawn(ctx, id) {
|
|
|
3363
4134
|
let frames = 0;
|
|
3364
4135
|
const view = doc.defaultView;
|
|
3365
4136
|
const tick = () => {
|
|
3366
|
-
if (doc.activeElement !== ctx.root) return;
|
|
3367
|
-
const el = find();
|
|
3368
|
-
if (el) el.focus({ preventScroll: true });
|
|
3369
|
-
else if (++frames < FOCUS_FRAMES) view?.requestAnimationFrame(tick);
|
|
3370
|
-
};
|
|
3371
|
-
view?.requestAnimationFrame(tick);
|
|
3372
|
-
}
|
|
3373
|
-
//#endregion
|
|
3374
|
-
//#region src/dom/tools/redact-area.ts
|
|
3375
|
-
/** Smallest drag, in CSS px on each axis, that draws a box rather than counting as a click. */
|
|
3376
|
-
const MIN_CSS$1 = 3;
|
|
3377
|
-
function redactAreaTool() {
|
|
3378
|
-
let ctx;
|
|
3379
|
-
let start;
|
|
3380
|
-
let draft;
|
|
3381
|
-
/** Text redactions from the live selection, one per page. False when nothing is selected. */
|
|
3382
|
-
const fromSelection = () => {
|
|
3383
|
-
if (!ctx) return false;
|
|
3384
|
-
const selections = ctx.selectionQuads();
|
|
3385
|
-
if (selections.length === 0) return false;
|
|
3386
|
-
for (const s of selections) ctx.store.add(ctx.toolStyle.resolve("redact", {
|
|
3387
|
-
type: "redact",
|
|
3388
|
-
page: s.page,
|
|
3389
|
-
quads: s.quads
|
|
3390
|
-
}));
|
|
3391
|
-
ctx.clearSelection();
|
|
3392
|
-
return true;
|
|
3393
|
-
};
|
|
3394
|
-
const clear = () => {
|
|
3395
|
-
draft?.remove();
|
|
3396
|
-
draft = void 0;
|
|
3397
|
-
start = void 0;
|
|
3398
|
-
};
|
|
3399
|
-
return {
|
|
3400
|
-
id: "redact-area",
|
|
3401
|
-
cursor: "crosshair",
|
|
3402
|
-
styleGroup: "redact",
|
|
3403
|
-
activate(c) {
|
|
3404
|
-
ctx = c;
|
|
3405
|
-
},
|
|
3406
|
-
deactivate() {
|
|
3407
|
-
clear();
|
|
3408
|
-
ctx = void 0;
|
|
3409
|
-
},
|
|
3410
|
-
onPointerDown(e, hit, annotationId) {
|
|
3411
|
-
if (!hit) return;
|
|
3412
|
-
if (annotationId !== void 0) {
|
|
3413
|
-
const a = ctx?.store.get(annotationId);
|
|
3414
|
-
if (!a || !isTextMarkup(a)) return;
|
|
3415
|
-
e.stopPropagation();
|
|
3416
|
-
}
|
|
3417
|
-
start = hit;
|
|
3418
|
-
e.preventDefault();
|
|
3419
|
-
return true;
|
|
3420
|
-
},
|
|
3421
|
-
onPointerMove(e) {
|
|
3422
|
-
if (!start) return;
|
|
3423
|
-
if (e.buttons === 0 || !start.host.isConnected) {
|
|
3424
|
-
clear();
|
|
3425
|
-
return;
|
|
3426
|
-
}
|
|
3427
|
-
const css = clampedCssIn(start.host, e);
|
|
3428
|
-
if (!draft) {
|
|
3429
|
-
draft = start.host.ownerDocument.createElement("div");
|
|
3430
|
-
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;
|
|
4137
|
+
if (doc.activeElement !== ctx.root) return;
|
|
4138
|
+
const el = find();
|
|
4139
|
+
if (el) el.focus({ preventScroll: true });
|
|
4140
|
+
else if (++frames < FOCUS_FRAMES) view?.requestAnimationFrame(tick);
|
|
3511
4141
|
};
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
4142
|
+
view?.requestAnimationFrame(tick);
|
|
4143
|
+
}
|
|
4144
|
+
//#endregion
|
|
4145
|
+
//#region src/dom/tools/redact-area.ts
|
|
4146
|
+
/** Smallest drag, in CSS px on each axis, that draws a box rather than counting as a click. */
|
|
4147
|
+
const MIN_CSS$1 = 3;
|
|
4148
|
+
function redactAreaTool() {
|
|
4149
|
+
let ctx;
|
|
4150
|
+
let start;
|
|
4151
|
+
let draft;
|
|
4152
|
+
/** Selects what the tool just added, together, and reports it made. */
|
|
4153
|
+
const made = (c, ids) => {
|
|
4154
|
+
c.store.select(ids);
|
|
4155
|
+
c.made(ids);
|
|
3517
4156
|
};
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
if (
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
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();
|
|
4157
|
+
/** Text redactions from the live selection, one per page. False when nothing is selected. */
|
|
4158
|
+
const fromSelection = () => {
|
|
4159
|
+
if (!ctx) return false;
|
|
4160
|
+
const selections = ctx.selectionQuads();
|
|
4161
|
+
if (selections.length === 0) return false;
|
|
4162
|
+
const c = ctx;
|
|
4163
|
+
const ids = selections.map((s) => c.store.add(c.toolStyle.resolve("redact", {
|
|
4164
|
+
type: "redact",
|
|
4165
|
+
page: s.page,
|
|
4166
|
+
quads: s.quads
|
|
4167
|
+
})).id);
|
|
4168
|
+
c.clearSelection();
|
|
4169
|
+
made(c, ids);
|
|
3546
4170
|
return true;
|
|
3547
|
-
}
|
|
4171
|
+
};
|
|
4172
|
+
const clear = () => {
|
|
4173
|
+
draft?.remove();
|
|
4174
|
+
draft = void 0;
|
|
4175
|
+
start = void 0;
|
|
4176
|
+
};
|
|
3548
4177
|
return {
|
|
3549
|
-
|
|
4178
|
+
id: "redact-area",
|
|
4179
|
+
kind: "one-shot",
|
|
4180
|
+
cursor: "crosshair",
|
|
4181
|
+
styleGroup: "redact",
|
|
4182
|
+
activate(c) {
|
|
4183
|
+
ctx = c;
|
|
4184
|
+
},
|
|
4185
|
+
deactivate() {
|
|
4186
|
+
clear();
|
|
4187
|
+
ctx = void 0;
|
|
4188
|
+
},
|
|
3550
4189
|
onPointerDown(e, hit, annotationId) {
|
|
3551
4190
|
if (!hit) return;
|
|
3552
|
-
reset();
|
|
3553
4191
|
if (annotationId !== void 0) {
|
|
3554
|
-
const a = ctx
|
|
3555
|
-
if (!a) return;
|
|
3556
|
-
|
|
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;
|
|
4192
|
+
const a = ctx?.store.get(annotationId);
|
|
4193
|
+
if (!a || !isTextMarkup(a)) return;
|
|
4194
|
+
e.stopPropagation();
|
|
3581
4195
|
}
|
|
3582
|
-
|
|
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([]);
|
|
4196
|
+
start = hit;
|
|
3595
4197
|
e.preventDefault();
|
|
3596
4198
|
return true;
|
|
3597
4199
|
},
|
|
3598
4200
|
onPointerMove(e) {
|
|
3599
|
-
if (
|
|
3600
|
-
if (e.buttons === 0 || !
|
|
3601
|
-
|
|
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);
|
|
4201
|
+
if (!start) return;
|
|
4202
|
+
if (e.buttons === 0 || !start.host.isConnected) {
|
|
4203
|
+
clear();
|
|
3634
4204
|
return;
|
|
3635
4205
|
}
|
|
3636
|
-
const css =
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
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;
|
|
4206
|
+
const css = clampedCssIn(start.host, e);
|
|
4207
|
+
if (!draft) {
|
|
4208
|
+
draft = start.host.ownerDocument.createElement("div");
|
|
4209
|
+
draft.className = "ink-draft";
|
|
4210
|
+
draft.dataset.inkDraft = "redact";
|
|
4211
|
+
draft.style.position = "absolute";
|
|
4212
|
+
draft.style.pointerEvents = "none";
|
|
4213
|
+
start.host.append(draft);
|
|
3662
4214
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4215
|
+
draft.style.left = `${Math.min(start.css.x, css.x)}px`;
|
|
4216
|
+
draft.style.top = `${Math.min(start.css.y, css.y)}px`;
|
|
4217
|
+
draft.style.width = `${Math.abs(css.x - start.css.x)}px`;
|
|
4218
|
+
draft.style.height = `${Math.abs(css.y - start.css.y)}px`;
|
|
3667
4219
|
},
|
|
3668
4220
|
onPointerCancel() {
|
|
3669
|
-
|
|
4221
|
+
clear();
|
|
3670
4222
|
},
|
|
3671
4223
|
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
|
-
}
|
|
4224
|
+
if (!ctx || !start) return;
|
|
4225
|
+
if (!start.host.isConnected) {
|
|
4226
|
+
clear();
|
|
4227
|
+
return;
|
|
3730
4228
|
}
|
|
4229
|
+
const css = clampedCssIn(start.host, e);
|
|
4230
|
+
const width = Math.abs(css.x - start.css.x);
|
|
4231
|
+
const height = Math.abs(css.y - start.css.y);
|
|
4232
|
+
if (width >= MIN_CSS$1 && height >= MIN_CSS$1) {
|
|
4233
|
+
const rect = start.transform.fromCssRect({
|
|
4234
|
+
left: Math.min(start.css.x, css.x),
|
|
4235
|
+
top: Math.min(start.css.y, css.y),
|
|
4236
|
+
width,
|
|
4237
|
+
height
|
|
4238
|
+
});
|
|
4239
|
+
const a = ctx.store.add(ctx.toolStyle.resolve("redact", {
|
|
4240
|
+
type: "redact",
|
|
4241
|
+
page: start.page,
|
|
4242
|
+
rect
|
|
4243
|
+
}));
|
|
4244
|
+
made(ctx, [a.id]);
|
|
4245
|
+
} else fromSelection();
|
|
4246
|
+
clear();
|
|
3731
4247
|
},
|
|
3732
4248
|
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);
|
|
4249
|
+
if (!!ctx && isViewportEnter(e, ctx.root) && fromSelection()) e.preventDefault();
|
|
3742
4250
|
}
|
|
3743
4251
|
};
|
|
3744
4252
|
}
|
|
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
4253
|
//#endregion
|
|
3848
4254
|
//#region src/dom/tools/select.ts
|
|
3849
4255
|
/**
|
|
@@ -3921,6 +4327,15 @@ function shapeTool() {
|
|
|
3921
4327
|
};
|
|
3922
4328
|
/** The input with the shape group's style and, for a bubble, the text group's text style. */
|
|
3923
4329
|
const styled = (c, input) => {
|
|
4330
|
+
if (input.type === "line") {
|
|
4331
|
+
const line = c.toolStyle.resolve("shape", input);
|
|
4332
|
+
const arrow = input.intent === "LineArrow";
|
|
4333
|
+
return {
|
|
4334
|
+
...line,
|
|
4335
|
+
head: line.head ?? "None",
|
|
4336
|
+
tail: line.tail ?? (arrow ? "OpenArrow" : "None")
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
3924
4339
|
if (input.type !== "freetext") return c.toolStyle.resolve("shape", input);
|
|
3925
4340
|
const shape = c.toolStyle.resolve("shape", {
|
|
3926
4341
|
...input,
|
|
@@ -3955,7 +4370,9 @@ function shapeTool() {
|
|
|
3955
4370
|
let waiting;
|
|
3956
4371
|
const openEditor = (c, a) => {
|
|
3957
4372
|
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))
|
|
4373
|
+
return el !== null && editFreeTextIn(c.store, el, a.id, c.viewport.transformFor(a.page), { onClose: (how) => {
|
|
4374
|
+
if (how === "escape" && ctx === c) c.done();
|
|
4375
|
+
} }) !== void 0;
|
|
3959
4376
|
};
|
|
3960
4377
|
/**
|
|
3961
4378
|
* Opens the in-place editor on a new bubble. When a controlled host has not applied it yet,
|
|
@@ -3977,6 +4394,7 @@ function shapeTool() {
|
|
|
3977
4394
|
if (!input) return;
|
|
3978
4395
|
const created = c.store.add(styled(c, input));
|
|
3979
4396
|
c.store.select([created.id]);
|
|
4397
|
+
c.made([created.id]);
|
|
3980
4398
|
if (isBubble(created)) edit(c, created);
|
|
3981
4399
|
};
|
|
3982
4400
|
const paint = (c, from, e) => {
|
|
@@ -4045,6 +4463,7 @@ function shapeTool() {
|
|
|
4045
4463
|
};
|
|
4046
4464
|
return {
|
|
4047
4465
|
id: "shape",
|
|
4466
|
+
kind: "one-shot",
|
|
4048
4467
|
cursor: "crosshair",
|
|
4049
4468
|
styleGroup: "shape",
|
|
4050
4469
|
defaultOptions: { ...DEFAULT_SHAPE_OPTIONS },
|
|
@@ -4114,6 +4533,7 @@ function signatureTool() {
|
|
|
4114
4533
|
const input = placeSignature(t, page, at, 150, rotation);
|
|
4115
4534
|
const a = c.store.add(input.type === "ink" ? c.toolStyle.resolve("ink", input) : input);
|
|
4116
4535
|
c.store.select([a.id]);
|
|
4536
|
+
c.made([a.id]);
|
|
4117
4537
|
};
|
|
4118
4538
|
/**
|
|
4119
4539
|
* Places the selected template, from the library at once, or after reloading it when the
|
|
@@ -4137,6 +4557,7 @@ function signatureTool() {
|
|
|
4137
4557
|
};
|
|
4138
4558
|
return {
|
|
4139
4559
|
id: "signature",
|
|
4560
|
+
kind: "one-shot",
|
|
4140
4561
|
cursor: "copy",
|
|
4141
4562
|
defaultOptions: {},
|
|
4142
4563
|
activate(c) {
|
|
@@ -4647,9 +5068,11 @@ const COLOR_LABELS = {
|
|
|
4647
5068
|
const COLOR_COLUMNS = 12;
|
|
4648
5069
|
/**
|
|
4649
5070
|
* 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.
|
|
5071
|
+
* for `fill`, and the trigger's swatch. Shared by the bindings and the vanilla menu. `fallback`
|
|
5072
|
+
* is the colour new annotations get while the style leaves it unset: the trigger shows it
|
|
5073
|
+
* rather than an empty swatch.
|
|
4651
5074
|
*/
|
|
4652
|
-
function colorMenuModel(property, current, supported) {
|
|
5075
|
+
function colorMenuModel(property, current, supported, fallback) {
|
|
4653
5076
|
const value = current[property];
|
|
4654
5077
|
const enabled = supported.has(property);
|
|
4655
5078
|
const color = typeof value === "string" && value !== "mixed" ? value.toUpperCase() : void 0;
|
|
@@ -4662,6 +5085,9 @@ function colorMenuModel(property, current, supported) {
|
|
|
4662
5085
|
const trigger = !enabled ? { kind: "empty" } : value === "mixed" ? { kind: "mixed" } : value === null ? { kind: "none" } : typeof value === "string" ? {
|
|
4663
5086
|
kind: "color",
|
|
4664
5087
|
color: value
|
|
5088
|
+
} : fallback !== void 0 ? {
|
|
5089
|
+
kind: "color",
|
|
5090
|
+
color: fallback
|
|
4665
5091
|
} : { kind: "empty" };
|
|
4666
5092
|
return {
|
|
4667
5093
|
label: COLOR_LABELS[property],
|
|
@@ -4679,7 +5105,7 @@ function colorMenuModel(property, current, supported) {
|
|
|
4679
5105
|
checked: value === null,
|
|
4680
5106
|
patch: { fill: null }
|
|
4681
5107
|
} : null,
|
|
4682
|
-
customValue: color ?? "#000000",
|
|
5108
|
+
customValue: color ?? fallback?.toUpperCase() ?? "#000000",
|
|
4683
5109
|
custom: (hex) => patchFor(hex.toUpperCase())
|
|
4684
5110
|
};
|
|
4685
5111
|
}
|
|
@@ -5031,35 +5457,190 @@ function createFocusReturn(opts) {
|
|
|
5031
5457
|
/** Set by `beforeRemove`: the close comes from removing the anchor's annotation. */
|
|
5032
5458
|
let removing = false;
|
|
5033
5459
|
return {
|
|
5034
|
-
beforeRemove(anchor, prefer) {
|
|
5035
|
-
fallbacks = [];
|
|
5036
|
-
removing = true;
|
|
5037
|
-
const siblings = anchor?.parentElement ? [...anchor.parentElement.children].filter((el) => el instanceof HTMLElement && el.hasAttribute("data-annotation")) : [];
|
|
5038
|
-
const i = anchor ? siblings.indexOf(anchor) : -1;
|
|
5039
|
-
if (i >= 0) {
|
|
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
|
-
}
|
|
5460
|
+
beforeRemove(anchor, prefer) {
|
|
5461
|
+
fallbacks = [];
|
|
5462
|
+
removing = true;
|
|
5463
|
+
const siblings = anchor?.parentElement ? [...anchor.parentElement.children].filter((el) => el instanceof HTMLElement && el.hasAttribute("data-annotation")) : [];
|
|
5464
|
+
const i = anchor ? siblings.indexOf(anchor) : -1;
|
|
5465
|
+
if (i >= 0) {
|
|
5466
|
+
const after = siblings.slice(i + 1);
|
|
5467
|
+
const preferred = prefer ? after.filter((el) => el.matches(prefer)) : [];
|
|
5468
|
+
fallbacks.push(...preferred, ...after, ...siblings.slice(0, i).reverse());
|
|
5469
|
+
}
|
|
5470
|
+
},
|
|
5471
|
+
onCloseAutoFocus(event, anchor) {
|
|
5472
|
+
event.preventDefault();
|
|
5473
|
+
const plan = fallbacks;
|
|
5474
|
+
const removed = removing;
|
|
5475
|
+
fallbacks = [];
|
|
5476
|
+
removing = false;
|
|
5477
|
+
const root = opts.root();
|
|
5478
|
+
if (!root) return;
|
|
5479
|
+
const activated = opts.activated();
|
|
5480
|
+
if (activated !== null && annotationElementFor(root, activated)) return;
|
|
5481
|
+
const active = root.ownerDocument.activeElement;
|
|
5482
|
+
if (active && active !== root && root.contains(active)) return;
|
|
5483
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5484
|
+
(removed ? [...plan, scroll] : [
|
|
5485
|
+
anchor,
|
|
5486
|
+
...plan,
|
|
5487
|
+
scroll
|
|
5488
|
+
]).find((el) => el?.isConnected)?.focus({ preventScroll: true });
|
|
5489
|
+
}
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
//#endregion
|
|
5493
|
+
//#region src/dom/viewer/dom.ts
|
|
5494
|
+
/** `document.createElement` with attributes and children. */
|
|
5495
|
+
function h(tag, attrs = {}, ...children) {
|
|
5496
|
+
const el = document.createElement(tag);
|
|
5497
|
+
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
5498
|
+
el.append(...children);
|
|
5499
|
+
return el;
|
|
5500
|
+
}
|
|
5501
|
+
function button$1(label, onClick) {
|
|
5502
|
+
const b = h("button", { type: "button" }, label);
|
|
5503
|
+
b.addEventListener("click", onClick);
|
|
5504
|
+
return b;
|
|
5505
|
+
}
|
|
5506
|
+
//#endregion
|
|
5507
|
+
//#region src/dom/viewer/popover.ts
|
|
5508
|
+
/** Space between the anchor and the panel, and between the panel and the window edge. */
|
|
5509
|
+
const GAP = 6;
|
|
5510
|
+
const MARGIN = 8;
|
|
5511
|
+
const TABBABLE = "button, input, select, textarea, a[href], [tabindex]";
|
|
5512
|
+
/**
|
|
5513
|
+
* Positions `panel` (fixed) below the `anchor` rectangle, or above it when only that fits,
|
|
5514
|
+
* kept inside the window. For panels anchored to something other than an annotation.
|
|
5515
|
+
*/
|
|
5516
|
+
function placePanel(panel, anchor) {
|
|
5517
|
+
const { innerWidth: vw, innerHeight: vh } = window;
|
|
5518
|
+
panel.style.position = "fixed";
|
|
5519
|
+
panel.style.left = "0px";
|
|
5520
|
+
panel.style.top = "0px";
|
|
5521
|
+
const w = panel.offsetWidth;
|
|
5522
|
+
const ht = panel.offsetHeight;
|
|
5523
|
+
let top = anchor.bottom + GAP;
|
|
5524
|
+
if (top + ht > vh - MARGIN && anchor.top - GAP - ht >= MARGIN) top = anchor.top - GAP - ht;
|
|
5525
|
+
top = Math.max(MARGIN, Math.min(top, vh - ht - MARGIN));
|
|
5526
|
+
const left = Math.max(MARGIN, Math.min(anchor.left, vw - w - MARGIN));
|
|
5527
|
+
panel.style.left = `${left}px`;
|
|
5528
|
+
panel.style.top = `${top}px`;
|
|
5529
|
+
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Creates a non-modal `role="dialog"` panel on `<body>`, marked `data-ink-popover` so the theme
|
|
5532
|
+
* gives it its tokens, that an app anchors to an annotation with `show(id)` (for instance on
|
|
5533
|
+
* `viewer.activated`). The panel follows the annotation's element as pages mount and unmount:
|
|
5534
|
+
* it hides while the element is gone and comes back with it. Tab loops inside the panel, and
|
|
5535
|
+
* Escape or a press outside calls `onDismiss`. Call `destroy` when done.
|
|
5536
|
+
*/
|
|
5537
|
+
function createPopover(opts) {
|
|
5538
|
+
const { root, focusReturn } = opts;
|
|
5539
|
+
const panel = h("div", {
|
|
5540
|
+
...opts.attrs,
|
|
5541
|
+
role: "dialog",
|
|
5542
|
+
"data-ink-popover": "",
|
|
5543
|
+
hidden: ""
|
|
5544
|
+
});
|
|
5545
|
+
document.body.append(panel);
|
|
5546
|
+
let anchoredId = null;
|
|
5547
|
+
let anchor = null;
|
|
5548
|
+
let stopObserving = () => void 0;
|
|
5549
|
+
/** The element that had focus in the panel when the anchor went away. */
|
|
5550
|
+
let stranded = null;
|
|
5551
|
+
const place = () => {
|
|
5552
|
+
if (!anchor || panel.hidden) return;
|
|
5553
|
+
placePanel(panel, anchor.getBoundingClientRect());
|
|
5554
|
+
};
|
|
5555
|
+
const resizes = new ResizeObserver(place);
|
|
5556
|
+
resizes.observe(panel);
|
|
5557
|
+
const onAnchor = (el, closing = false) => {
|
|
5558
|
+
if (anchor) resizes.unobserve(anchor);
|
|
5559
|
+
anchor = el;
|
|
5560
|
+
if (!el) {
|
|
5561
|
+
const active = document.activeElement;
|
|
5562
|
+
if (!closing && !panel.hidden && active instanceof HTMLElement && panel.contains(active)) {
|
|
5563
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5564
|
+
if (scroll) {
|
|
5565
|
+
stranded = active;
|
|
5566
|
+
scroll.focus({ preventScroll: true });
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
panel.hidden = true;
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
resizes.observe(el);
|
|
5573
|
+
const wasHidden = panel.hidden;
|
|
5574
|
+
panel.hidden = false;
|
|
5575
|
+
place();
|
|
5576
|
+
if (wasHidden && stranded) {
|
|
5577
|
+
const back = stranded;
|
|
5578
|
+
stranded = null;
|
|
5579
|
+
const scroll = root.querySelector("[data-ink-scroll]");
|
|
5580
|
+
if (panel.contains(back) && document.activeElement === scroll) back.focus({ preventScroll: true });
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
const onKey = (e) => {
|
|
5584
|
+
if (e.key !== "Escape" || panel.hidden || e.defaultPrevented) return;
|
|
5585
|
+
const target = e.target;
|
|
5586
|
+
if (!(target instanceof Node) || !(panel.contains(target) || root.contains(target))) return;
|
|
5587
|
+
e.preventDefault();
|
|
5588
|
+
opts.onDismiss();
|
|
5589
|
+
};
|
|
5590
|
+
const onPress = (e) => {
|
|
5591
|
+
if (panel.hidden || panel.contains(e.target)) return;
|
|
5592
|
+
if (shouldKeepOpenOnAnnotation(e.target)) return;
|
|
5593
|
+
opts.onDismiss();
|
|
5594
|
+
};
|
|
5595
|
+
const onTab = (e) => {
|
|
5596
|
+
if (e.key !== "Tab" || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
5597
|
+
const tabbable = [...panel.querySelectorAll(TABBABLE)].filter((el) => !el.disabled && el.tabIndex >= 0);
|
|
5598
|
+
const first = tabbable[0];
|
|
5599
|
+
const last = tabbable.at(-1);
|
|
5600
|
+
if (!first || !last) return;
|
|
5601
|
+
const active = document.activeElement;
|
|
5602
|
+
if (e.shiftKey ? active !== first : active !== last) return;
|
|
5603
|
+
e.preventDefault();
|
|
5604
|
+
(e.shiftKey ? last : first).focus();
|
|
5605
|
+
};
|
|
5606
|
+
panel.addEventListener("keydown", onTab);
|
|
5607
|
+
document.addEventListener("keydown", onKey);
|
|
5608
|
+
document.addEventListener("pointerdown", onPress, true);
|
|
5609
|
+
document.addEventListener("scroll", place, true);
|
|
5610
|
+
window.addEventListener("resize", place);
|
|
5611
|
+
return {
|
|
5612
|
+
panel,
|
|
5613
|
+
get anchor() {
|
|
5614
|
+
return anchor;
|
|
5044
5615
|
},
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
if (
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
(
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5616
|
+
show(id) {
|
|
5617
|
+
if (id === anchoredId) return;
|
|
5618
|
+
stopObserving();
|
|
5619
|
+
anchoredId = id;
|
|
5620
|
+
stopObserving = observeAnnotationElement(root, id, onAnchor);
|
|
5621
|
+
},
|
|
5622
|
+
hide() {
|
|
5623
|
+
if (anchoredId === null) return;
|
|
5624
|
+
stopObserving();
|
|
5625
|
+
stopObserving = () => void 0;
|
|
5626
|
+
anchoredId = null;
|
|
5627
|
+
const back = anchor;
|
|
5628
|
+
stranded = null;
|
|
5629
|
+
onAnchor(null, true);
|
|
5630
|
+
queueMicrotask(() => {
|
|
5631
|
+
if (anchoredId !== null || !panel.isConnected) return;
|
|
5632
|
+
focusReturn.onCloseAutoFocus(new Event("closeautofocus", { cancelable: true }), back);
|
|
5633
|
+
});
|
|
5634
|
+
},
|
|
5635
|
+
destroy() {
|
|
5636
|
+
stopObserving();
|
|
5637
|
+
resizes.disconnect();
|
|
5638
|
+
document.removeEventListener("keydown", onKey);
|
|
5639
|
+
document.removeEventListener("pointerdown", onPress, true);
|
|
5640
|
+
panel.removeEventListener("keydown", onTab);
|
|
5641
|
+
document.removeEventListener("scroll", place, true);
|
|
5642
|
+
window.removeEventListener("resize", place);
|
|
5643
|
+
panel.remove();
|
|
5063
5644
|
}
|
|
5064
5645
|
};
|
|
5065
5646
|
}
|
|
@@ -5105,6 +5686,7 @@ function mountViewer(opts) {
|
|
|
5105
5686
|
store: doc.annotations,
|
|
5106
5687
|
onActivate: (id) => opts.onActivate?.(id),
|
|
5107
5688
|
isCustom: () => isCustom,
|
|
5689
|
+
decorate: (a) => opts.decorateAnnotation?.(a),
|
|
5108
5690
|
onError: (cause, layer) => {
|
|
5109
5691
|
failed.add(layer);
|
|
5110
5692
|
if (error) return;
|
|
@@ -5281,6 +5863,10 @@ function mountViewer(opts) {
|
|
|
5281
5863
|
if (next.customAnnotations !== void 0) custom = next.customAnnotations;
|
|
5282
5864
|
build(state);
|
|
5283
5865
|
},
|
|
5866
|
+
redecorate() {
|
|
5867
|
+
if (destroyed) return;
|
|
5868
|
+
for (const mount of pages.values()) mount.overlay.redecorate();
|
|
5869
|
+
},
|
|
5284
5870
|
destroy() {
|
|
5285
5871
|
if (destroyed) return;
|
|
5286
5872
|
destroyed = true;
|
|
@@ -5289,224 +5875,6 @@ function mountViewer(opts) {
|
|
|
5289
5875
|
};
|
|
5290
5876
|
}
|
|
5291
5877
|
//#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
5878
|
//#region src/dom/viewer/thread-popover.ts
|
|
5511
5879
|
function createThreadPopover(opts) {
|
|
5512
5880
|
const { root, store, activated, focusReturn } = opts;
|
|
@@ -5676,7 +6044,7 @@ function createThreadForm(n, actions) {
|
|
|
5676
6044
|
//#endregion
|
|
5677
6045
|
//#region src/dom/viewer/create-viewer.ts
|
|
5678
6046
|
function createViewer(el, opts) {
|
|
5679
|
-
const { source: initial, assets, password, defaults, history, threads: withThreads = true,
|
|
6047
|
+
const { source: initial, assets, password, defaults, history, threads: withThreads = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, toolStyle: _toolStyle, toolOptions: _toolOptions, signatureLibrary: _signatureLibrary, signatures: signatureStore, ...view } = opts;
|
|
5680
6048
|
/** One library for the viewer's life, so templates and the menu outlive each document. */
|
|
5681
6049
|
const signatures = createSignatureLibrary(signatureStore ?? createMemorySignatureStore());
|
|
5682
6050
|
/** Tool options persist for the viewer's life; the tool style is per document. */
|
|
@@ -5686,7 +6054,7 @@ function createViewer(el, opts) {
|
|
|
5686
6054
|
const engine = writable(null);
|
|
5687
6055
|
const viewport = writable(null);
|
|
5688
6056
|
const tools = writable(null);
|
|
5689
|
-
/** The activated annotation: the note thread
|
|
6057
|
+
/** The activated annotation: the note thread opens from it. */
|
|
5690
6058
|
const activated = writable(null);
|
|
5691
6059
|
const root = h("div", {
|
|
5692
6060
|
"data-ink-root": "",
|
|
@@ -5734,20 +6102,15 @@ function createViewer(el, opts) {
|
|
|
5734
6102
|
renderStatus(s);
|
|
5735
6103
|
status.set(s);
|
|
5736
6104
|
}
|
|
5737
|
-
/** Mounts the engine and the
|
|
6105
|
+
/** Mounts the engine and the thread popover for a ready document; returns their teardown. */
|
|
5738
6106
|
function start(doc) {
|
|
5739
6107
|
const store = doc.annotations;
|
|
5740
|
-
const
|
|
5741
|
-
root,
|
|
5742
|
-
store,
|
|
5743
|
-
activated,
|
|
5744
|
-
focusReturn
|
|
5745
|
-
}) : null, withSuggestions ? createSuggestionCard({
|
|
6108
|
+
const threads = withThreads ? createThreadPopover({
|
|
5746
6109
|
root,
|
|
5747
6110
|
store,
|
|
5748
6111
|
activated,
|
|
5749
6112
|
focusReturn
|
|
5750
|
-
}) : null
|
|
6113
|
+
}) : null;
|
|
5751
6114
|
const stopList = store.list.subscribe(() => {
|
|
5752
6115
|
const id = activated.get();
|
|
5753
6116
|
if (id !== null && !store.get(id)) activated.set(null);
|
|
@@ -5800,7 +6163,7 @@ function createViewer(el, opts) {
|
|
|
5800
6163
|
engine.set(mounted);
|
|
5801
6164
|
return () => {
|
|
5802
6165
|
stopList();
|
|
5803
|
-
|
|
6166
|
+
threads?.destroy();
|
|
5804
6167
|
activated.set(null);
|
|
5805
6168
|
engine.set(null);
|
|
5806
6169
|
mounted.destroy();
|
|
@@ -5863,6 +6226,10 @@ function createViewer(el, opts) {
|
|
|
5863
6226
|
engine: readonly(engine),
|
|
5864
6227
|
viewport: readonly(viewport),
|
|
5865
6228
|
tools: readonly(tools),
|
|
6229
|
+
activated: readonly(activated),
|
|
6230
|
+
setActivated(id) {
|
|
6231
|
+
if (id === null || current.get()?.annotations.get(id)) activated.set(id);
|
|
6232
|
+
},
|
|
5866
6233
|
open,
|
|
5867
6234
|
destroy() {
|
|
5868
6235
|
if (destroyed) return;
|
|
@@ -6188,7 +6555,7 @@ function createMenu(trigger, opts) {
|
|
|
6188
6555
|
let panel = null;
|
|
6189
6556
|
let teardown = null;
|
|
6190
6557
|
const place = () => {
|
|
6191
|
-
if (panel) placePanel(panel, trigger.getBoundingClientRect());
|
|
6558
|
+
if (panel) placePanel(panel, (opts.anchor ?? trigger).getBoundingClientRect());
|
|
6192
6559
|
};
|
|
6193
6560
|
function close(restoreFocus = false) {
|
|
6194
6561
|
if (!panel) return;
|
|
@@ -6404,7 +6771,7 @@ function createColorMenu(viewer, opts) {
|
|
|
6404
6771
|
/** What the menu reads and writes: the whole tool style, or one group's view of it. */
|
|
6405
6772
|
let style = null;
|
|
6406
6773
|
let stops = [];
|
|
6407
|
-
const model = () => colorMenuModel(property, style?.current.get() ?? {}, style?.supported.get() ?? /* @__PURE__ */ new Set());
|
|
6774
|
+
const model = () => colorMenuModel(property, style?.current.get() ?? {}, style?.supported.get() ?? /* @__PURE__ */ new Set(), opts.fallback?.get());
|
|
6408
6775
|
function refresh() {
|
|
6409
6776
|
const m = model();
|
|
6410
6777
|
trigger.disabled = !m.enabled;
|
|
@@ -6518,6 +6885,7 @@ function createColorMenu(viewer, opts) {
|
|
|
6518
6885
|
};
|
|
6519
6886
|
follow(viewer.engine.get());
|
|
6520
6887
|
viewer.engine.subscribe(follow);
|
|
6888
|
+
opts.fallback?.subscribe(refresh);
|
|
6521
6889
|
return trigger;
|
|
6522
6890
|
}
|
|
6523
6891
|
//#endregion
|
|
@@ -6801,6 +7169,7 @@ function createShapeMenu(viewer) {
|
|
|
6801
7169
|
}
|
|
6802
7170
|
const menu = createMenu(trigger, {
|
|
6803
7171
|
label: "Shapes",
|
|
7172
|
+
anchor: root,
|
|
6804
7173
|
attrs: { "data-ink-menu": "shape" },
|
|
6805
7174
|
render: (panel) => paint(panel, model())
|
|
6806
7175
|
});
|
|
@@ -6859,10 +7228,13 @@ function createHighlightMenu(viewer) {
|
|
|
6859
7228
|
}, main, trigger);
|
|
6860
7229
|
main.disabled = true;
|
|
6861
7230
|
trigger.disabled = true;
|
|
7231
|
+
/** The colour the chosen kind gets while the markup style leaves it unset. */
|
|
7232
|
+
const defaultColour = writable(void 0);
|
|
6862
7233
|
const colour = createColorMenu(viewer, {
|
|
6863
7234
|
property: "border",
|
|
6864
7235
|
group: "markup",
|
|
6865
|
-
label: COLOUR_LABEL
|
|
7236
|
+
label: COLOUR_LABEL,
|
|
7237
|
+
fallback: defaultColour
|
|
6866
7238
|
});
|
|
6867
7239
|
colour.setAttribute("role", "menuitem");
|
|
6868
7240
|
colour.tabIndex = -1;
|
|
@@ -6883,6 +7255,7 @@ function createHighlightMenu(viewer) {
|
|
|
6883
7255
|
const on = tools?.active.get() === "highlight";
|
|
6884
7256
|
main.dataset.state = on ? "on" : "off";
|
|
6885
7257
|
main.setAttribute("aria-pressed", String(on));
|
|
7258
|
+
defaultColour.set(viewer.document.get()?.annotations.defaults.color(m.kind));
|
|
6886
7259
|
menu.refresh();
|
|
6887
7260
|
}
|
|
6888
7261
|
function paint(panel, m) {
|
|
@@ -6929,6 +7302,7 @@ function createHighlightMenu(viewer) {
|
|
|
6929
7302
|
}
|
|
6930
7303
|
const menu = createMenu(trigger, {
|
|
6931
7304
|
label: "Highlights",
|
|
7305
|
+
anchor: root,
|
|
6932
7306
|
attrs: { "data-ink-menu": "highlight" },
|
|
6933
7307
|
owns: (t) => t instanceof Element && t.closest("[data-ink-menu=\"color-border\"]") !== null,
|
|
6934
7308
|
render: (panel) => paint(panel, model())
|
|
@@ -6938,7 +7312,11 @@ function createHighlightMenu(viewer) {
|
|
|
6938
7312
|
for (const stop of stops) stop();
|
|
6939
7313
|
stops = [];
|
|
6940
7314
|
tools = next;
|
|
6941
|
-
if (tools) stops = [
|
|
7315
|
+
if (tools) stops = [
|
|
7316
|
+
tools.active.subscribe(refresh),
|
|
7317
|
+
tools.options("highlight").subscribe(refresh),
|
|
7318
|
+
viewer.document.subscribe(refresh)
|
|
7319
|
+
];
|
|
6942
7320
|
if (!tools) menu.close();
|
|
6943
7321
|
refresh();
|
|
6944
7322
|
};
|
|
@@ -7695,6 +8073,7 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
7695
8073
|
}
|
|
7696
8074
|
const menu = createMenu(trigger, {
|
|
7697
8075
|
label: SIGNATURE_LABELS.menu,
|
|
8076
|
+
anchor: root,
|
|
7698
8077
|
attrs: { "data-ink-menu": "signature" },
|
|
7699
8078
|
render: (panel) => paint(panel, model())
|
|
7700
8079
|
});
|
|
@@ -7731,6 +8110,6 @@ function createSignatureMenu(viewer, opts = {}) {
|
|
|
7731
8110
|
return root;
|
|
7732
8111
|
}
|
|
7733
8112
|
//#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,
|
|
8113
|
+
export { ARROWHEAD_CHOICES, ARROWHEAD_ENDING, COLOR_COLUMNS, COLOR_LABELS, DASH_PATTERN, DEFAULT_CORNER_RADIUS, DEFAULT_FREETEXT_WIDTH, DEFAULT_HIGHLIGHT_OPTIONS, DEFAULT_SHAPE_SIZE, FONT_FAMILIES, LINE_WEIGHTS, MARKUP_ICONS, MARKUP_KINDS, MARKUP_LABELS, PALETTE, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SHAPE_LABELS, SIGNATURE_FONTS, SIGNATURE_IMAGE_MAX_LENGTH, SIGNATURE_IMAGE_MAX_SIDE, SIGNATURE_IMAGE_TYPES, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, arrowheadChoice, attachViewportKeys, builtinTools, clampFontSize, colorMenuModel, computeCssLayout, createColorMenu, createFocusReturn, createHighlightMenu, createIcon, createImageSignature, createLineStyleMenu, createMenu, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPopover, createPrevPage, createRedo, createShapeMenu, createSignatureDialog, createSignatureDraft, createSignatureMenu, createSignaturePad, createSignaturePreview, createTextLayer, createTextStyleMenu, createToolButton, createToolController, createToolOptions, createToolbar, createToolbarSeparator, createTypedSignature, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, editFreeText, fitZoomCss, focusInitialMenuItem, fontFamilyValue, freetextTool, getSelectionQuads, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, highlightTool, hitFor, lineStyleMenuModel, menuItems, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, placePanel, redactAreaTool, renderAnnotationContent, renderTypedSignature, selectTextTool, selectTool, shapeMenuModel, shapeTool, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, signatureTool, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, trapTab, visibleRangeCss, withHostTools };
|
|
7735
8114
|
|
|
7736
8115
|
//# sourceMappingURL=dom.mjs.map
|