@inkbar/core 0.1.1 → 0.3.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/LICENSE +0 -49
- package/README.md +37 -9
- package/dist/{document-Cywnv8Sz.d.mts → document-BftlPZKU.d.mts} +343 -71
- package/dist/document-BftlPZKU.d.mts.map +1 -0
- package/dist/dom.d.mts +48 -5
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +339 -339
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/{threads-BtvtrAhL.mjs → threads-2USvyGpX.mjs} +70 -47
- package/dist/threads-2USvyGpX.mjs.map +1 -0
- package/package.json +2 -1
- package/schema.json +122 -91
- package/dist/document-Cywnv8Sz.d.mts.map +0 -1
- package/dist/threads-BtvtrAhL.mjs.map +0 -1
package/dist/dom.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as rectFromPoints, G as applyMatrix, I as confidenceBucket, R as cssVariableDefaults, U as readonly, V as mergeLineRects, W as writable, a as acceptSuggestion, i as threadRoot, nt as translateQuad, o as openDocument, ot as InkError, q as invertMatrix, r as threadOf, rt as translateRect, st as isInkError, t as isAbandonedNote, tt as rectsIntersect } from "./threads-2USvyGpX.mjs";
|
|
2
|
+
import { ChevronLeft, ChevronRight, Hand, Highlighter, ICON_ATTRIBUTES, MessageSquare, MousePointer2, Redo2, Square, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
2
3
|
//#region src/dom/transform.ts
|
|
3
4
|
/** Folds any integer multiple of 90 onto 0, 90, 180 or 270. Throws `RangeError` otherwise. */
|
|
4
5
|
function normalizeRotation(deg) {
|
|
@@ -924,7 +925,7 @@ function translated(a, dx, dy) {
|
|
|
924
925
|
case "polygon":
|
|
925
926
|
patch.vertices = a.vertices.map((p) => shift(p, dx, dy));
|
|
926
927
|
break;
|
|
927
|
-
case "ink": patch.
|
|
928
|
+
case "ink": patch.inklist = a.inklist.map((path) => path.map((p) => shift(p, dx, dy)));
|
|
928
929
|
}
|
|
929
930
|
return patch;
|
|
930
931
|
}
|
|
@@ -936,7 +937,7 @@ function translated(a, dx, dy) {
|
|
|
936
937
|
function hasRectGeometry(a) {
|
|
937
938
|
switch (a.type) {
|
|
938
939
|
case "redact": return a.quads === void 0 || a.quads.length === 0;
|
|
939
|
-
case "
|
|
940
|
+
case "freetext":
|
|
940
941
|
case "square":
|
|
941
942
|
case "circle": return true;
|
|
942
943
|
default: return false;
|
|
@@ -970,10 +971,10 @@ const SVG$1 = "http://www.w3.org/2000/svg";
|
|
|
970
971
|
* `toCssRect(a.rect)`.
|
|
971
972
|
*/
|
|
972
973
|
function annotationBox(a, t) {
|
|
973
|
-
const points = a.type === "line" ? [a.start, a.end] : a.type === "polyline" || a.type === "polygon" ? a.vertices : a.type === "ink" ? a.
|
|
974
|
-
if (!points || points.length === 0 || !("
|
|
974
|
+
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;
|
|
975
|
+
if (!points || points.length === 0 || !("width" in a)) return t.toCssRect(a.rect);
|
|
975
976
|
const box = t.toCssRect(pointBounds(points));
|
|
976
|
-
const pad = Math.max(t.toCssLength(a.
|
|
977
|
+
const pad = Math.max(t.toCssLength(a.width), 1) / 2;
|
|
977
978
|
return {
|
|
978
979
|
left: box.left - pad,
|
|
979
980
|
top: box.top - pad,
|
|
@@ -1011,7 +1012,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1011
1012
|
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, String(v));
|
|
1012
1013
|
return n;
|
|
1013
1014
|
};
|
|
1014
|
-
if (a.type === "
|
|
1015
|
+
if (a.type === "text") {
|
|
1015
1016
|
const svg = boxSvg(doc, box.width, box.height);
|
|
1016
1017
|
const hit = node("rect", {
|
|
1017
1018
|
x: 0,
|
|
@@ -1030,10 +1031,10 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1030
1031
|
el.append(svg, icon);
|
|
1031
1032
|
return;
|
|
1032
1033
|
}
|
|
1033
|
-
if (a.type === "
|
|
1034
|
+
if (a.type === "freetext") {
|
|
1034
1035
|
const text = doc.createElement("div");
|
|
1035
1036
|
text.className = "ink-free-text";
|
|
1036
|
-
text.textContent = a.
|
|
1037
|
+
text.textContent = a.contents;
|
|
1037
1038
|
text.dataset.align = a.align;
|
|
1038
1039
|
text.style.setProperty("--ink-font-size", `${t.toCssLength(a.fontSize)}px`);
|
|
1039
1040
|
text.style.setProperty("--ink-text-color", a.color);
|
|
@@ -1075,7 +1076,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1075
1076
|
height: box.height
|
|
1076
1077
|
})];
|
|
1077
1078
|
for (const s of shapes) {
|
|
1078
|
-
s.setAttribute("fill", a.
|
|
1079
|
+
s.setAttribute("fill", a.interiorColor);
|
|
1079
1080
|
s.setAttribute("fill-opacity", "0.15");
|
|
1080
1081
|
s.setAttribute("stroke", a.color);
|
|
1081
1082
|
s.setAttribute("stroke-width", String(t.toCssLength(1)));
|
|
@@ -1086,7 +1087,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1086
1087
|
}
|
|
1087
1088
|
case "square":
|
|
1088
1089
|
case "circle": {
|
|
1089
|
-
const sw = t.toCssLength(a.
|
|
1090
|
+
const sw = t.toCssLength(a.width);
|
|
1090
1091
|
const shape = a.type === "square" ? node("rect", {
|
|
1091
1092
|
x: sw / 2,
|
|
1092
1093
|
y: sw / 2,
|
|
@@ -1098,7 +1099,7 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1098
1099
|
rx: Math.max(0, box.width / 2 - sw / 2),
|
|
1099
1100
|
ry: Math.max(0, box.height / 2 - sw / 2)
|
|
1100
1101
|
});
|
|
1101
|
-
shape.setAttribute("fill", a.
|
|
1102
|
+
shape.setAttribute("fill", a.interiorColor ?? "none");
|
|
1102
1103
|
shape.setAttribute("stroke", a.color);
|
|
1103
1104
|
shape.setAttribute("stroke-width", String(sw));
|
|
1104
1105
|
svg.append(shape);
|
|
@@ -1111,20 +1112,20 @@ function renderAnnotationContent(el, a, t) {
|
|
|
1111
1112
|
const pts = a.type === "line" ? [a.start, a.end] : a.vertices;
|
|
1112
1113
|
svg.append(node(a.type === "polygon" ? "polygon" : "polyline", {
|
|
1113
1114
|
points: pts.map(local).join(" "),
|
|
1114
|
-
fill: a.type === "polygon" ? a.
|
|
1115
|
+
fill: a.type === "polygon" ? a.interiorColor ?? "none" : "none",
|
|
1115
1116
|
stroke: a.color,
|
|
1116
|
-
"stroke-width": t.toCssLength(a.
|
|
1117
|
+
"stroke-width": t.toCssLength(a.width),
|
|
1117
1118
|
"stroke-linejoin": "round"
|
|
1118
1119
|
}));
|
|
1119
1120
|
svg.setAttribute("opacity", String(a.opacity));
|
|
1120
1121
|
break;
|
|
1121
1122
|
}
|
|
1122
1123
|
case "ink":
|
|
1123
|
-
for (const path of a.
|
|
1124
|
+
for (const path of a.inklist) svg.append(node("polyline", {
|
|
1124
1125
|
points: path.map(local).join(" "),
|
|
1125
1126
|
fill: "none",
|
|
1126
1127
|
stroke: a.color,
|
|
1127
|
-
"stroke-width": t.toCssLength(a.
|
|
1128
|
+
"stroke-width": t.toCssLength(a.width),
|
|
1128
1129
|
"stroke-linecap": "round",
|
|
1129
1130
|
"stroke-linejoin": "round"
|
|
1130
1131
|
}));
|
|
@@ -1257,8 +1258,8 @@ const LABELS = {
|
|
|
1257
1258
|
underline: "Underline",
|
|
1258
1259
|
strikeout: "Strikeout",
|
|
1259
1260
|
squiggly: "Squiggly underline",
|
|
1260
|
-
|
|
1261
|
-
|
|
1261
|
+
text: "Note",
|
|
1262
|
+
freetext: "Text box",
|
|
1262
1263
|
square: "Rectangle",
|
|
1263
1264
|
circle: "Ellipse",
|
|
1264
1265
|
line: "Line",
|
|
@@ -1789,7 +1790,7 @@ function noteTool() {
|
|
|
1789
1790
|
let down;
|
|
1790
1791
|
const place = (c, page, at) => {
|
|
1791
1792
|
const a = c.store.add({
|
|
1792
|
-
type: "
|
|
1793
|
+
type: "text",
|
|
1793
1794
|
page,
|
|
1794
1795
|
rect: {
|
|
1795
1796
|
x: at.x - SIZE / 2,
|
|
@@ -2497,7 +2498,7 @@ function h(tag, attrs = {}, ...children) {
|
|
|
2497
2498
|
el.append(...children);
|
|
2498
2499
|
return el;
|
|
2499
2500
|
}
|
|
2500
|
-
function button(label, onClick) {
|
|
2501
|
+
function button$1(label, onClick) {
|
|
2501
2502
|
const b = h("button", { type: "button" }, label);
|
|
2502
2503
|
b.addEventListener("click", onClick);
|
|
2503
2504
|
return b;
|
|
@@ -2654,13 +2655,13 @@ function createNotePopover(opts) {
|
|
|
2654
2655
|
const dropped = /* @__PURE__ */ new Set();
|
|
2655
2656
|
function shown() {
|
|
2656
2657
|
const a = current ? store.get(current) : void 0;
|
|
2657
|
-
return a?.type === "
|
|
2658
|
+
return a?.type === "text" ? a : null;
|
|
2658
2659
|
}
|
|
2659
2660
|
function target() {
|
|
2660
2661
|
const list = store.list.get();
|
|
2661
2662
|
const id = activated.get();
|
|
2662
2663
|
const a = id === null ? void 0 : list.find((x) => x.id === id);
|
|
2663
|
-
return a?.type === "
|
|
2664
|
+
return a?.type === "text" ? threadRoot(list, a) : null;
|
|
2664
2665
|
}
|
|
2665
2666
|
const editingRoot = (n) => !n.locked && !n.contents?.trim();
|
|
2666
2667
|
function isEmptyRoot(n) {
|
|
@@ -2716,7 +2717,7 @@ function createNotePopover(opts) {
|
|
|
2716
2717
|
written.add(n.id);
|
|
2717
2718
|
if (editingRoot(n)) store.update(n.id, { contents: text });
|
|
2718
2719
|
else store.add({
|
|
2719
|
-
type: "
|
|
2720
|
+
type: "text",
|
|
2720
2721
|
page: n.page,
|
|
2721
2722
|
rect: n.rect,
|
|
2722
2723
|
contents: text,
|
|
@@ -2757,11 +2758,11 @@ function threadItem(a) {
|
|
|
2757
2758
|
function createThreadForm(n, actions) {
|
|
2758
2759
|
const textarea = h("textarea", { rows: "2" });
|
|
2759
2760
|
const send = h("button", { type: "submit" });
|
|
2760
|
-
const remove = button("Delete", actions.remove);
|
|
2761
|
+
const remove = button$1("Delete", actions.remove);
|
|
2761
2762
|
const element = h("form", {
|
|
2762
2763
|
class: "ink-thread-reply",
|
|
2763
2764
|
"data-ink-thread-reply": ""
|
|
2764
|
-
}, textarea, h("div", { class: "ink-thread-actions" }, send, remove, button("Close", actions.close)));
|
|
2765
|
+
}, textarea, h("div", { class: "ink-thread-actions" }, send, remove, button$1("Close", actions.close)));
|
|
2765
2766
|
const syncSend = () => {
|
|
2766
2767
|
send.disabled = !textarea.value.trim();
|
|
2767
2768
|
};
|
|
@@ -2835,7 +2836,7 @@ function createSuggestionCard(opts) {
|
|
|
2835
2836
|
popover.panel.replaceChildren(h("p", { class: "ink-suggestion-reason" }, h("strong", {}, s?.reason ?? "Suggested redaction"), ...bucket ? [h("span", {
|
|
2836
2837
|
class: "ink-suggestion-confidence",
|
|
2837
2838
|
"data-confidence": bucket
|
|
2838
|
-
}, `${bucket} confidence`)] : []), ...s?.explanation ? [h("p", { class: "ink-suggestion-explanation" }, s.explanation)] : [], h("div", { class: "ink-suggestion-actions" }, button("Accept", accept), ...a.locked ? [] : [button("Dismiss", dismiss)]));
|
|
2839
|
+
}, `${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)]));
|
|
2839
2840
|
}
|
|
2840
2841
|
function close() {
|
|
2841
2842
|
activated.set(null);
|
|
@@ -2864,315 +2865,14 @@ function createSuggestionCard(opts) {
|
|
|
2864
2865
|
} };
|
|
2865
2866
|
}
|
|
2866
2867
|
//#endregion
|
|
2867
|
-
//#region src/dom/viewer/icons.ts
|
|
2868
|
-
const ICONS = {
|
|
2869
|
-
hand: [
|
|
2870
|
-
["path", { d: "M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" }],
|
|
2871
|
-
["path", { d: "M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" }],
|
|
2872
|
-
["path", { d: "M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" }],
|
|
2873
|
-
["path", { d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" }]
|
|
2874
|
-
],
|
|
2875
|
-
"mouse-pointer-2": [["path", { d: "M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" }]],
|
|
2876
|
-
highlighter: [["path", { d: "m9 11-6 6v3h9l3-3" }], ["path", { d: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" }]],
|
|
2877
|
-
square: [["rect", {
|
|
2878
|
-
width: "18",
|
|
2879
|
-
height: "18",
|
|
2880
|
-
x: "3",
|
|
2881
|
-
y: "3",
|
|
2882
|
-
rx: "2"
|
|
2883
|
-
}]],
|
|
2884
|
-
"message-square": [["path", { d: "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" }]],
|
|
2885
|
-
"zoom-in": [
|
|
2886
|
-
["circle", {
|
|
2887
|
-
cx: "11",
|
|
2888
|
-
cy: "11",
|
|
2889
|
-
r: "8"
|
|
2890
|
-
}],
|
|
2891
|
-
["line", {
|
|
2892
|
-
x1: "21",
|
|
2893
|
-
x2: "16.65",
|
|
2894
|
-
y1: "21",
|
|
2895
|
-
y2: "16.65"
|
|
2896
|
-
}],
|
|
2897
|
-
["line", {
|
|
2898
|
-
x1: "11",
|
|
2899
|
-
x2: "11",
|
|
2900
|
-
y1: "8",
|
|
2901
|
-
y2: "14"
|
|
2902
|
-
}],
|
|
2903
|
-
["line", {
|
|
2904
|
-
x1: "8",
|
|
2905
|
-
x2: "14",
|
|
2906
|
-
y1: "11",
|
|
2907
|
-
y2: "11"
|
|
2908
|
-
}]
|
|
2909
|
-
],
|
|
2910
|
-
"zoom-out": [
|
|
2911
|
-
["circle", {
|
|
2912
|
-
cx: "11",
|
|
2913
|
-
cy: "11",
|
|
2914
|
-
r: "8"
|
|
2915
|
-
}],
|
|
2916
|
-
["line", {
|
|
2917
|
-
x1: "21",
|
|
2918
|
-
x2: "16.65",
|
|
2919
|
-
y1: "21",
|
|
2920
|
-
y2: "16.65"
|
|
2921
|
-
}],
|
|
2922
|
-
["line", {
|
|
2923
|
-
x1: "8",
|
|
2924
|
-
x2: "14",
|
|
2925
|
-
y1: "11",
|
|
2926
|
-
y2: "11"
|
|
2927
|
-
}]
|
|
2928
|
-
]
|
|
2929
|
-
};
|
|
2930
|
-
const SVG = "http://www.w3.org/2000/svg";
|
|
2931
|
-
/** An 18px Lucide icon, hidden from assistive technology: its button carries the name. */
|
|
2932
|
-
function icon(name) {
|
|
2933
|
-
const svg = document.createElementNS(SVG, "svg");
|
|
2934
|
-
const attrs = {
|
|
2935
|
-
xmlns: SVG,
|
|
2936
|
-
width: "18",
|
|
2937
|
-
height: "18",
|
|
2938
|
-
viewBox: "0 0 24 24",
|
|
2939
|
-
fill: "none",
|
|
2940
|
-
stroke: "currentColor",
|
|
2941
|
-
"stroke-width": "2",
|
|
2942
|
-
"stroke-linecap": "round",
|
|
2943
|
-
"stroke-linejoin": "round",
|
|
2944
|
-
"aria-hidden": "true"
|
|
2945
|
-
};
|
|
2946
|
-
for (const [k, v] of Object.entries(attrs)) svg.setAttribute(k, v);
|
|
2947
|
-
for (const [tag, shape] of ICONS[name]) {
|
|
2948
|
-
const child = document.createElementNS(SVG, tag);
|
|
2949
|
-
for (const [k, v] of Object.entries(shape)) child.setAttribute(k, v);
|
|
2950
|
-
svg.append(child);
|
|
2951
|
-
}
|
|
2952
|
-
return svg;
|
|
2953
|
-
}
|
|
2954
|
-
//#endregion
|
|
2955
|
-
//#region src/dom/viewer/toolbar.ts
|
|
2956
|
-
const TOOLS = [
|
|
2957
|
-
["view", "hand"],
|
|
2958
|
-
["select", "mouse-pointer-2"],
|
|
2959
|
-
["highlight", "highlighter"],
|
|
2960
|
-
["redact-area", "square"],
|
|
2961
|
-
["note", "message-square"]
|
|
2962
|
-
];
|
|
2963
|
-
/** A toolbar button with an icon; the label is its accessible name and tooltip. */
|
|
2964
|
-
function iconButton(label, attrs, name, onClick) {
|
|
2965
|
-
const b = h("button", {
|
|
2966
|
-
type: "button",
|
|
2967
|
-
...attrs,
|
|
2968
|
-
"aria-label": label,
|
|
2969
|
-
title: label
|
|
2970
|
-
});
|
|
2971
|
-
b.append(icon(name));
|
|
2972
|
-
b.addEventListener("click", onClick);
|
|
2973
|
-
return b;
|
|
2974
|
-
}
|
|
2975
|
-
const group = (name, label, ...children) => h("div", {
|
|
2976
|
-
class: "ink-toolbar-group",
|
|
2977
|
-
"data-ink-toolbar-group": name,
|
|
2978
|
-
role: "group",
|
|
2979
|
-
"aria-label": label
|
|
2980
|
-
}, ...children);
|
|
2981
|
-
function createToolbar() {
|
|
2982
|
-
let stops = [];
|
|
2983
|
-
let bound = null;
|
|
2984
|
-
const watch = (r, fn) => {
|
|
2985
|
-
fn(r.get());
|
|
2986
|
-
stops.push(r.subscribe(fn));
|
|
2987
|
-
};
|
|
2988
|
-
const toolButtons = TOOLS.map(([id, name]) => iconButton(TOOL_LABELS[id], {
|
|
2989
|
-
class: "ink-tool-button",
|
|
2990
|
-
"data-ink-tool-button": "",
|
|
2991
|
-
"data-tool": id
|
|
2992
|
-
}, name, () => bound?.tools.set(id)));
|
|
2993
|
-
const showTool = (active) => {
|
|
2994
|
-
for (const b of toolButtons) {
|
|
2995
|
-
const on = b.dataset.tool === active;
|
|
2996
|
-
b.dataset.state = on ? "on" : "off";
|
|
2997
|
-
b.setAttribute("aria-pressed", String(on));
|
|
2998
|
-
}
|
|
2999
|
-
};
|
|
3000
|
-
const zoomOut = iconButton("Zoom out", {
|
|
3001
|
-
class: "ink-zoom-out",
|
|
3002
|
-
"data-ink-zoom-out": ""
|
|
3003
|
-
}, "zoom-out", () => bound?.viewport.zoomBy(.8));
|
|
3004
|
-
const zoomIn = iconButton("Zoom in", {
|
|
3005
|
-
class: "ink-zoom-in",
|
|
3006
|
-
"data-ink-zoom-in": ""
|
|
3007
|
-
}, "zoom-in", () => bound?.viewport.zoomBy(1.25));
|
|
3008
|
-
const custom = h("option", { value: "custom" });
|
|
3009
|
-
const zoomSelect = h("select", {
|
|
3010
|
-
class: "ink-zoom-select",
|
|
3011
|
-
"data-ink-zoom-select": "",
|
|
3012
|
-
"aria-label": "Zoom"
|
|
3013
|
-
}, h("option", { value: "fit-width" }, "Fit width"), h("option", { value: "fit-page" }, "Fit page"), ...ZOOM_LEVELS.map((l) => h("option", { value: String(l) }, `${Math.round(l * 100)}%`)));
|
|
3014
|
-
zoomSelect.addEventListener("change", () => {
|
|
3015
|
-
const v = zoomSelect.value;
|
|
3016
|
-
if (v === "fit-width" || v === "fit-page") bound?.viewport.setZoom(v);
|
|
3017
|
-
else if (v !== "custom") bound?.viewport.setZoom(Number(v));
|
|
3018
|
-
});
|
|
3019
|
-
const pageInput = h("input", {
|
|
3020
|
-
class: "ink-page-input",
|
|
3021
|
-
"data-ink-page-input": "",
|
|
3022
|
-
type: "number",
|
|
3023
|
-
inputmode: "numeric",
|
|
3024
|
-
"aria-label": "Page",
|
|
3025
|
-
min: "1"
|
|
3026
|
-
});
|
|
3027
|
-
const commitPage = () => {
|
|
3028
|
-
if (!bound) return;
|
|
3029
|
-
const { viewport } = bound;
|
|
3030
|
-
const n = Number(pageInput.value);
|
|
3031
|
-
const current = viewport.currentPage.get();
|
|
3032
|
-
if (Number.isInteger(n) && n >= 1 && n <= bound.doc.pages.length) {
|
|
3033
|
-
if (n - 1 !== current) viewport.scrollToPage(n - 1);
|
|
3034
|
-
} else pageInput.value = String(current + 1);
|
|
3035
|
-
};
|
|
3036
|
-
pageInput.addEventListener("change", commitPage);
|
|
3037
|
-
pageInput.addEventListener("keydown", (e) => {
|
|
3038
|
-
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
3039
|
-
e.preventDefault();
|
|
3040
|
-
commitPage();
|
|
3041
|
-
});
|
|
3042
|
-
const pageCount = h("span", {
|
|
3043
|
-
class: "ink-page-count",
|
|
3044
|
-
"data-ink-page-count": "",
|
|
3045
|
-
"aria-live": "polite"
|
|
3046
|
-
});
|
|
3047
|
-
const controls = [
|
|
3048
|
-
...toolButtons,
|
|
3049
|
-
zoomOut,
|
|
3050
|
-
zoomSelect,
|
|
3051
|
-
zoomIn,
|
|
3052
|
-
pageInput
|
|
3053
|
-
];
|
|
3054
|
-
const element = h("div", {
|
|
3055
|
-
class: "ink-toolbar ink-default-toolbar",
|
|
3056
|
-
"data-ink-toolbar": "",
|
|
3057
|
-
"data-ink-default-toolbar": "",
|
|
3058
|
-
role: "toolbar",
|
|
3059
|
-
"aria-label": "Document tools",
|
|
3060
|
-
"aria-orientation": "horizontal"
|
|
3061
|
-
}, group("tools", "Tools", ...toolButtons), group("zoom", "Zoom", zoomOut, zoomSelect, zoomIn), group("page", "Page", pageInput, pageCount));
|
|
3062
|
-
const roving = rovingTabindex(element);
|
|
3063
|
-
function stop() {
|
|
3064
|
-
for (const s of stops) s();
|
|
3065
|
-
stops = [];
|
|
3066
|
-
bound = null;
|
|
3067
|
-
}
|
|
3068
|
-
function unbind() {
|
|
3069
|
-
stop();
|
|
3070
|
-
for (const c of controls) c.disabled = true;
|
|
3071
|
-
showTool(null);
|
|
3072
|
-
custom.remove();
|
|
3073
|
-
zoomSelect.value = "fit-width";
|
|
3074
|
-
pageInput.value = "";
|
|
3075
|
-
pageCount.textContent = "";
|
|
3076
|
-
roving.sync();
|
|
3077
|
-
}
|
|
3078
|
-
unbind();
|
|
3079
|
-
return {
|
|
3080
|
-
element,
|
|
3081
|
-
bind(doc, viewport, tools) {
|
|
3082
|
-
stop();
|
|
3083
|
-
bound = {
|
|
3084
|
-
doc,
|
|
3085
|
-
viewport,
|
|
3086
|
-
tools
|
|
3087
|
-
};
|
|
3088
|
-
for (const c of controls) c.disabled = false;
|
|
3089
|
-
watch(tools.active, showTool);
|
|
3090
|
-
watch(viewport.zoom, (z) => {
|
|
3091
|
-
const level = typeof z === "number" ? ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001) : void 0;
|
|
3092
|
-
if (typeof z === "number" && level === void 0) {
|
|
3093
|
-
custom.textContent = `${Math.round(z * 100)}%`;
|
|
3094
|
-
zoomSelect.append(custom);
|
|
3095
|
-
zoomSelect.value = "custom";
|
|
3096
|
-
} else {
|
|
3097
|
-
custom.remove();
|
|
3098
|
-
zoomSelect.value = typeof z === "number" ? String(level) : z;
|
|
3099
|
-
}
|
|
3100
|
-
});
|
|
3101
|
-
const count = doc.pages.length;
|
|
3102
|
-
pageInput.max = String(count);
|
|
3103
|
-
pageCount.textContent = `of ${count}`;
|
|
3104
|
-
watch(viewport.currentPage, (p) => {
|
|
3105
|
-
pageInput.value = String(p + 1);
|
|
3106
|
-
});
|
|
3107
|
-
roving.sync();
|
|
3108
|
-
},
|
|
3109
|
-
unbind,
|
|
3110
|
-
destroy() {
|
|
3111
|
-
stop();
|
|
3112
|
-
roving.destroy();
|
|
3113
|
-
element.remove();
|
|
3114
|
-
}
|
|
3115
|
-
};
|
|
3116
|
-
}
|
|
3117
|
-
/** Roving items, as in the bindings' Radix toolbar: buttons only. */
|
|
3118
|
-
const ITEMS = "button";
|
|
3119
|
-
/**
|
|
3120
|
-
* One Tab stop across the enabled buttons in `bar`: the current one has `tabindex="0"`, the
|
|
3121
|
-
* rest `-1`. ArrowLeft and ArrowRight move between them and wrap, Home and End go to the ends.
|
|
3122
|
-
* Inputs, selects and textareas, built in or added by the host, keep their own tabindex (a
|
|
3123
|
-
* Tab stop of their own by default) and their own arrow, Home and End keys.
|
|
3124
|
-
*/
|
|
3125
|
-
function rovingTabindex(bar) {
|
|
3126
|
-
let current = null;
|
|
3127
|
-
const items = () => [...bar.querySelectorAll(ITEMS)].filter((el) => !el.disabled && !el.closest("[hidden]"));
|
|
3128
|
-
const sync = () => {
|
|
3129
|
-
const list = items();
|
|
3130
|
-
if (!current || !list.includes(current)) current = list[0] ?? null;
|
|
3131
|
-
for (const el of bar.querySelectorAll(ITEMS)) el.tabIndex = el === current ? 0 : -1;
|
|
3132
|
-
};
|
|
3133
|
-
const onFocusIn = (e) => {
|
|
3134
|
-
const target = e.target;
|
|
3135
|
-
if (!items().includes(target)) return;
|
|
3136
|
-
current = target;
|
|
3137
|
-
sync();
|
|
3138
|
-
};
|
|
3139
|
-
const onKeyDown = (e) => {
|
|
3140
|
-
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3141
|
-
const list = items();
|
|
3142
|
-
const i = list.indexOf(e.target);
|
|
3143
|
-
if (i < 0) return;
|
|
3144
|
-
const last = list.length - 1;
|
|
3145
|
-
const next = e.key === "ArrowRight" ? i === last ? 0 : i + 1 : e.key === "ArrowLeft" ? i === 0 ? last : i - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
|
|
3146
|
-
if (next < 0) return;
|
|
3147
|
-
e.preventDefault();
|
|
3148
|
-
list[next].focus();
|
|
3149
|
-
};
|
|
3150
|
-
const observer = new MutationObserver(sync);
|
|
3151
|
-
observer.observe(bar, {
|
|
3152
|
-
childList: true,
|
|
3153
|
-
subtree: true,
|
|
3154
|
-
attributes: true,
|
|
3155
|
-
attributeFilter: ["disabled", "hidden"]
|
|
3156
|
-
});
|
|
3157
|
-
bar.addEventListener("focusin", onFocusIn);
|
|
3158
|
-
bar.addEventListener("keydown", onKeyDown);
|
|
3159
|
-
sync();
|
|
3160
|
-
return {
|
|
3161
|
-
sync,
|
|
3162
|
-
destroy() {
|
|
3163
|
-
observer.disconnect();
|
|
3164
|
-
bar.removeEventListener("focusin", onFocusIn);
|
|
3165
|
-
bar.removeEventListener("keydown", onKeyDown);
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3168
|
-
}
|
|
3169
|
-
//#endregion
|
|
3170
2868
|
//#region src/dom/viewer/create-viewer.ts
|
|
3171
2869
|
function createViewer(el, opts) {
|
|
3172
|
-
const { source: initial, assets, password, defaults, history,
|
|
2870
|
+
const { source: initial, assets, password, defaults, history, notes: withNotes = true, suggestions: withSuggestions = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, ...view } = opts;
|
|
3173
2871
|
const status = writable({ state: "idle" });
|
|
3174
2872
|
const current = writable(null);
|
|
3175
2873
|
const engine = writable(null);
|
|
2874
|
+
const viewport = writable(null);
|
|
2875
|
+
const tools = writable(null);
|
|
3176
2876
|
/** The activated annotation: the note thread and the suggestion card open from it. */
|
|
3177
2877
|
const activated = writable(null);
|
|
3178
2878
|
const root = h("div", {
|
|
@@ -3191,8 +2891,6 @@ function createViewer(el, opts) {
|
|
|
3191
2891
|
"data-ink-viewport-root": "",
|
|
3192
2892
|
"data-state": "idle"
|
|
3193
2893
|
}, scroll);
|
|
3194
|
-
const toolbar = withToolbar ? createToolbar() : null;
|
|
3195
|
-
if (toolbar) root.append(toolbar.element);
|
|
3196
2894
|
root.append(viewportRoot);
|
|
3197
2895
|
el.append(root);
|
|
3198
2896
|
const focusReturn = createFocusReturn({
|
|
@@ -3255,9 +2953,10 @@ function createViewer(el, opts) {
|
|
|
3255
2953
|
activated.set(id);
|
|
3256
2954
|
onActivate?.(id);
|
|
3257
2955
|
},
|
|
3258
|
-
onBuild: (
|
|
3259
|
-
|
|
3260
|
-
|
|
2956
|
+
onBuild: (vp, tc) => {
|
|
2957
|
+
tools.set(tc);
|
|
2958
|
+
viewport.set(vp);
|
|
2959
|
+
onBuild?.(vp, tc);
|
|
3261
2960
|
},
|
|
3262
2961
|
onPageMount: (page, mount, host) => {
|
|
3263
2962
|
hosts.set(page, host);
|
|
@@ -3290,7 +2989,8 @@ function createViewer(el, opts) {
|
|
|
3290
2989
|
activated.set(null);
|
|
3291
2990
|
engine.set(null);
|
|
3292
2991
|
mounted.destroy();
|
|
3293
|
-
|
|
2992
|
+
viewport.set(null);
|
|
2993
|
+
tools.set(null);
|
|
3294
2994
|
};
|
|
3295
2995
|
}
|
|
3296
2996
|
let loads = 0;
|
|
@@ -3346,19 +3046,319 @@ function createViewer(el, opts) {
|
|
|
3346
3046
|
status: readonly(status),
|
|
3347
3047
|
document: readonly(current),
|
|
3348
3048
|
engine: readonly(engine),
|
|
3349
|
-
|
|
3049
|
+
viewport: readonly(viewport),
|
|
3050
|
+
tools: readonly(tools),
|
|
3350
3051
|
open,
|
|
3351
3052
|
destroy() {
|
|
3352
3053
|
if (destroyed) return;
|
|
3353
3054
|
destroyed = true;
|
|
3354
3055
|
loads++;
|
|
3355
3056
|
close();
|
|
3356
|
-
toolbar?.destroy();
|
|
3357
3057
|
root.remove();
|
|
3358
3058
|
}
|
|
3359
3059
|
};
|
|
3360
3060
|
}
|
|
3361
3061
|
//#endregion
|
|
3362
|
-
|
|
3062
|
+
//#region src/dom/viewer/controls.ts
|
|
3063
|
+
/**
|
|
3064
|
+
* Runs `connect` for every value `source` takes while it is set, undoing the previous run's
|
|
3065
|
+
* subscriptions first, and disables `control` while it is `null`.
|
|
3066
|
+
*/
|
|
3067
|
+
function follow(source, control, connect, reset) {
|
|
3068
|
+
let stops = [];
|
|
3069
|
+
const watch = (r, fn) => {
|
|
3070
|
+
fn(r.get());
|
|
3071
|
+
stops.push(r.subscribe(fn));
|
|
3072
|
+
};
|
|
3073
|
+
const run = (value) => {
|
|
3074
|
+
for (const s of stops) s();
|
|
3075
|
+
stops = [];
|
|
3076
|
+
control.disabled = value === null;
|
|
3077
|
+
if (value === null) reset?.();
|
|
3078
|
+
else connect(value, watch);
|
|
3079
|
+
};
|
|
3080
|
+
run(source.get());
|
|
3081
|
+
source.subscribe(run);
|
|
3082
|
+
}
|
|
3083
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
3084
|
+
const TOOL_ICONS = {
|
|
3085
|
+
view: Hand,
|
|
3086
|
+
select: MousePointer2,
|
|
3087
|
+
highlight: Highlighter,
|
|
3088
|
+
"redact-area": Square,
|
|
3089
|
+
note: MessageSquare
|
|
3090
|
+
};
|
|
3091
|
+
/**
|
|
3092
|
+
* An `<svg>` drawing `icon` from `@inkbar/icons`, hidden from assistive technology: the
|
|
3093
|
+
* control it sits in carries the name. The theme sizes it with `--ink-icon-size`.
|
|
3094
|
+
*/
|
|
3095
|
+
function createIcon(icon) {
|
|
3096
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
3097
|
+
const attrs = {
|
|
3098
|
+
...ICON_ATTRIBUTES,
|
|
3099
|
+
width: "18",
|
|
3100
|
+
height: "18",
|
|
3101
|
+
class: "ink-icon",
|
|
3102
|
+
"aria-hidden": "true"
|
|
3103
|
+
};
|
|
3104
|
+
for (const [k, v] of Object.entries(attrs)) svg.setAttribute(k, v);
|
|
3105
|
+
for (const [tag, shape] of icon) {
|
|
3106
|
+
const child = document.createElementNS(SVG, tag);
|
|
3107
|
+
for (const [k, v] of Object.entries(shape)) child.setAttribute(k, v);
|
|
3108
|
+
svg.append(child);
|
|
3109
|
+
}
|
|
3110
|
+
return svg;
|
|
3111
|
+
}
|
|
3112
|
+
function button(label, attrs, icon) {
|
|
3113
|
+
const b = h("button", {
|
|
3114
|
+
type: "button",
|
|
3115
|
+
...attrs,
|
|
3116
|
+
"aria-label": label,
|
|
3117
|
+
title: label
|
|
3118
|
+
});
|
|
3119
|
+
b.append(createIcon(icon));
|
|
3120
|
+
return b;
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* A toolbar following the WAI-ARIA toolbar pattern: its buttons share one Tab stop, with
|
|
3124
|
+
* arrow keys, Home and End to move between them. Fields and selects are Tab stops of their
|
|
3125
|
+
* own and keep their keys. Buttons added later join in.
|
|
3126
|
+
*/
|
|
3127
|
+
function createToolbar(opts = {}) {
|
|
3128
|
+
const bar = h("div", {
|
|
3129
|
+
class: "ink-toolbar",
|
|
3130
|
+
"data-ink-toolbar": "",
|
|
3131
|
+
role: "toolbar",
|
|
3132
|
+
"aria-label": opts.label ?? "Document tools",
|
|
3133
|
+
"aria-orientation": "horizontal"
|
|
3134
|
+
});
|
|
3135
|
+
rovingTabindex(bar);
|
|
3136
|
+
return bar;
|
|
3137
|
+
}
|
|
3138
|
+
/** A divider between groups of controls in a horizontal toolbar. Not focusable. */
|
|
3139
|
+
function createToolbarSeparator() {
|
|
3140
|
+
return h("div", {
|
|
3141
|
+
role: "separator",
|
|
3142
|
+
"aria-orientation": "vertical",
|
|
3143
|
+
class: "ink-toolbar-separator",
|
|
3144
|
+
"data-ink-toolbar-separator": ""
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
/** Undoes the last change to the document's annotations; disabled when there is none. */
|
|
3148
|
+
function createUndo(viewer) {
|
|
3149
|
+
return historyButton(viewer, "undo");
|
|
3150
|
+
}
|
|
3151
|
+
/** Redoes the last undone change; disabled when there is none. */
|
|
3152
|
+
function createRedo(viewer) {
|
|
3153
|
+
return historyButton(viewer, "redo");
|
|
3154
|
+
}
|
|
3155
|
+
function historyButton(viewer, action) {
|
|
3156
|
+
const b = button(action === "undo" ? "Undo" : "Redo", {
|
|
3157
|
+
class: `ink-${action}`,
|
|
3158
|
+
[`data-ink-${action}`]: ""
|
|
3159
|
+
}, action === "undo" ? Undo2 : Redo2);
|
|
3160
|
+
b.addEventListener("click", () => viewer.document.get()?.history[action]());
|
|
3161
|
+
follow(viewer.document, b, (doc, watch) => watch(action === "undo" ? doc.history.canUndo : doc.history.canRedo, (can) => {
|
|
3162
|
+
b.disabled = !can;
|
|
3163
|
+
}));
|
|
3164
|
+
return b;
|
|
3165
|
+
}
|
|
3166
|
+
/** Sets the tool; pressed while it is the active one. */
|
|
3167
|
+
function createToolButton(viewer, tool, opts = {}) {
|
|
3168
|
+
const b = button(opts.label ?? TOOL_LABELS[tool], {
|
|
3169
|
+
class: "ink-tool-button",
|
|
3170
|
+
"data-ink-tool-button": "",
|
|
3171
|
+
"data-tool": tool
|
|
3172
|
+
}, TOOL_ICONS[tool]);
|
|
3173
|
+
const show = (active) => {
|
|
3174
|
+
const on = active === tool;
|
|
3175
|
+
b.dataset.state = on ? "on" : "off";
|
|
3176
|
+
b.setAttribute("aria-pressed", String(on));
|
|
3177
|
+
};
|
|
3178
|
+
b.addEventListener("click", () => viewer.tools.get()?.set(tool));
|
|
3179
|
+
follow(viewer.tools, b, (tools, watch) => watch(tools.active, show), () => show(null));
|
|
3180
|
+
return b;
|
|
3181
|
+
}
|
|
3182
|
+
function createZoomIn(viewer) {
|
|
3183
|
+
const b = button("Zoom in", {
|
|
3184
|
+
class: "ink-zoom-in",
|
|
3185
|
+
"data-ink-zoom-in": ""
|
|
3186
|
+
}, ZoomIn);
|
|
3187
|
+
b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(1.25));
|
|
3188
|
+
follow(viewer.viewport, b, () => void 0);
|
|
3189
|
+
return b;
|
|
3190
|
+
}
|
|
3191
|
+
function createZoomOut(viewer) {
|
|
3192
|
+
const b = button("Zoom out", {
|
|
3193
|
+
class: "ink-zoom-out",
|
|
3194
|
+
"data-ink-zoom-out": ""
|
|
3195
|
+
}, ZoomOut);
|
|
3196
|
+
b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(.8));
|
|
3197
|
+
follow(viewer.viewport, b, () => void 0);
|
|
3198
|
+
return b;
|
|
3199
|
+
}
|
|
3200
|
+
/** Fit width, fit page and the zoom levels; a zoom between levels gets an option of its own. */
|
|
3201
|
+
function createZoomSelect(viewer) {
|
|
3202
|
+
const custom = h("option", { value: "custom" });
|
|
3203
|
+
const select = h("select", {
|
|
3204
|
+
class: "ink-zoom-select",
|
|
3205
|
+
"data-ink-zoom-select": "",
|
|
3206
|
+
"aria-label": "Zoom"
|
|
3207
|
+
}, h("option", { value: "fit-width" }, "Fit width"), h("option", { value: "fit-page" }, "Fit page"), ...ZOOM_LEVELS.map((l) => h("option", { value: String(l) }, `${Math.round(l * 100)}%`)));
|
|
3208
|
+
select.addEventListener("change", () => {
|
|
3209
|
+
const v = select.value;
|
|
3210
|
+
const viewport = viewer.viewport.get();
|
|
3211
|
+
if (v === "fit-width" || v === "fit-page") viewport?.setZoom(v);
|
|
3212
|
+
else if (v !== "custom") viewport?.setZoom(Number(v));
|
|
3213
|
+
});
|
|
3214
|
+
follow(viewer.viewport, select, (viewport, watch) => watch(viewport.zoom, (z) => {
|
|
3215
|
+
const level = typeof z === "number" ? ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001) : void 0;
|
|
3216
|
+
if (typeof z === "number" && level === void 0) {
|
|
3217
|
+
custom.textContent = `${Math.round(z * 100)}%`;
|
|
3218
|
+
const after = [...select.options].find((o) => o !== custom && Number(o.value) > z);
|
|
3219
|
+
select.insertBefore(custom, after ?? null);
|
|
3220
|
+
select.value = "custom";
|
|
3221
|
+
} else {
|
|
3222
|
+
custom.remove();
|
|
3223
|
+
select.value = typeof z === "number" ? String(level) : z;
|
|
3224
|
+
}
|
|
3225
|
+
}), () => {
|
|
3226
|
+
custom.remove();
|
|
3227
|
+
select.value = "fit-width";
|
|
3228
|
+
});
|
|
3229
|
+
return select;
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* The current page, 1-based. Enter or leaving the field goes to the page typed; a number out
|
|
3233
|
+
* of range puts the current page back.
|
|
3234
|
+
*/
|
|
3235
|
+
function createPageInput(viewer) {
|
|
3236
|
+
const input = h("input", {
|
|
3237
|
+
class: "ink-page-input",
|
|
3238
|
+
"data-ink-page-input": "",
|
|
3239
|
+
type: "number",
|
|
3240
|
+
inputmode: "numeric",
|
|
3241
|
+
"aria-label": "Page",
|
|
3242
|
+
min: "1"
|
|
3243
|
+
});
|
|
3244
|
+
const commit = () => {
|
|
3245
|
+
const viewport = viewer.viewport.get();
|
|
3246
|
+
const doc = viewer.document.get();
|
|
3247
|
+
if (!viewport || !doc) return;
|
|
3248
|
+
const n = Number(input.value);
|
|
3249
|
+
const current = viewport.currentPage.get();
|
|
3250
|
+
if (Number.isInteger(n) && n >= 1 && n <= doc.pages.length) {
|
|
3251
|
+
if (n - 1 !== current) viewport.scrollToPage(n - 1);
|
|
3252
|
+
} else input.value = String(current + 1);
|
|
3253
|
+
};
|
|
3254
|
+
input.addEventListener("change", commit);
|
|
3255
|
+
input.addEventListener("keydown", (e) => {
|
|
3256
|
+
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
3257
|
+
e.preventDefault();
|
|
3258
|
+
commit();
|
|
3259
|
+
});
|
|
3260
|
+
follow(viewer.viewport, input, (viewport, watch) => {
|
|
3261
|
+
input.max = String(viewer.document.get()?.pages.length ?? 1);
|
|
3262
|
+
watch(viewport.currentPage, (p) => input.value = String(p + 1));
|
|
3263
|
+
}, () => input.value = "");
|
|
3264
|
+
return input;
|
|
3265
|
+
}
|
|
3266
|
+
/** "of 12": the page count, announced when it changes. */
|
|
3267
|
+
function createPageCount(viewer) {
|
|
3268
|
+
const span = h("span", {
|
|
3269
|
+
class: "ink-page-count",
|
|
3270
|
+
"data-ink-page-count": "",
|
|
3271
|
+
"aria-live": "polite"
|
|
3272
|
+
});
|
|
3273
|
+
const show = () => {
|
|
3274
|
+
const doc = viewer.document.get();
|
|
3275
|
+
span.textContent = doc && viewer.viewport.get() ? `of ${doc.pages.length}` : "";
|
|
3276
|
+
};
|
|
3277
|
+
show();
|
|
3278
|
+
viewer.viewport.subscribe(show);
|
|
3279
|
+
return span;
|
|
3280
|
+
}
|
|
3281
|
+
/** Disabled on the first page. */
|
|
3282
|
+
function createPrevPage(viewer) {
|
|
3283
|
+
return pageButton(viewer, -1);
|
|
3284
|
+
}
|
|
3285
|
+
/** Disabled on the last page. */
|
|
3286
|
+
function createNextPage(viewer) {
|
|
3287
|
+
return pageButton(viewer, 1);
|
|
3288
|
+
}
|
|
3289
|
+
function pageButton(viewer, step) {
|
|
3290
|
+
const b = step < 0 ? button("Previous page", {
|
|
3291
|
+
class: "ink-prev-page",
|
|
3292
|
+
"data-ink-prev-page": ""
|
|
3293
|
+
}, ChevronLeft) : button("Next page", {
|
|
3294
|
+
class: "ink-next-page",
|
|
3295
|
+
"data-ink-next-page": ""
|
|
3296
|
+
}, ChevronRight);
|
|
3297
|
+
b.addEventListener("click", () => {
|
|
3298
|
+
const viewport = viewer.viewport.get();
|
|
3299
|
+
viewport?.scrollToPage(viewport.currentPage.get() + step);
|
|
3300
|
+
});
|
|
3301
|
+
follow(viewer.viewport, b, (viewport, watch) => {
|
|
3302
|
+
const last = (viewer.document.get()?.pages.length ?? 1) - 1;
|
|
3303
|
+
watch(viewport.currentPage, (p) => {
|
|
3304
|
+
b.disabled = step < 0 ? p <= 0 : p >= last;
|
|
3305
|
+
});
|
|
3306
|
+
});
|
|
3307
|
+
return b;
|
|
3308
|
+
}
|
|
3309
|
+
/** Roving items, as in the bindings' Radix toolbar: buttons only. */
|
|
3310
|
+
const ITEMS = "button";
|
|
3311
|
+
/**
|
|
3312
|
+
* One Tab stop across the enabled buttons in `bar`: the current one has `tabindex="0"`, the
|
|
3313
|
+
* rest `-1`. ArrowLeft and ArrowRight move between them and wrap, Home and End go to the ends.
|
|
3314
|
+
* Inputs, selects and textareas, built in or added by the host, keep their own tabindex (a
|
|
3315
|
+
* Tab stop of their own by default) and their own arrow, Home and End keys.
|
|
3316
|
+
*/
|
|
3317
|
+
function rovingTabindex(bar) {
|
|
3318
|
+
let current = null;
|
|
3319
|
+
const items = () => [...bar.querySelectorAll(ITEMS)].filter((el) => !el.disabled && !el.closest("[hidden]"));
|
|
3320
|
+
const sync = () => {
|
|
3321
|
+
const list = items();
|
|
3322
|
+
if (!current || !list.includes(current)) current = list[0] ?? null;
|
|
3323
|
+
for (const el of bar.querySelectorAll(ITEMS)) el.tabIndex = el === current ? 0 : -1;
|
|
3324
|
+
};
|
|
3325
|
+
const onFocusIn = (e) => {
|
|
3326
|
+
const target = e.target;
|
|
3327
|
+
if (!items().includes(target)) return;
|
|
3328
|
+
current = target;
|
|
3329
|
+
sync();
|
|
3330
|
+
};
|
|
3331
|
+
const onKeyDown = (e) => {
|
|
3332
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3333
|
+
const list = items();
|
|
3334
|
+
const i = list.indexOf(e.target);
|
|
3335
|
+
if (i < 0) return;
|
|
3336
|
+
const last = list.length - 1;
|
|
3337
|
+
const next = e.key === "ArrowRight" ? i === last ? 0 : i + 1 : e.key === "ArrowLeft" ? i === 0 ? last : i - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
|
|
3338
|
+
if (next < 0) return;
|
|
3339
|
+
e.preventDefault();
|
|
3340
|
+
list[next].focus();
|
|
3341
|
+
};
|
|
3342
|
+
const observer = new MutationObserver(sync);
|
|
3343
|
+
observer.observe(bar, {
|
|
3344
|
+
childList: true,
|
|
3345
|
+
subtree: true,
|
|
3346
|
+
attributes: true,
|
|
3347
|
+
attributeFilter: ["disabled", "hidden"]
|
|
3348
|
+
});
|
|
3349
|
+
bar.addEventListener("focusin", onFocusIn);
|
|
3350
|
+
bar.addEventListener("keydown", onKeyDown);
|
|
3351
|
+
sync();
|
|
3352
|
+
return {
|
|
3353
|
+
sync,
|
|
3354
|
+
destroy() {
|
|
3355
|
+
observer.disconnect();
|
|
3356
|
+
bar.removeEventListener("focusin", onFocusIn);
|
|
3357
|
+
bar.removeEventListener("keydown", onKeyDown);
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
//#endregion
|
|
3362
|
+
export { TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, attachViewportKeys, builtinTools, computeCssLayout, confidenceAttr, createFocusReturn, createIcon, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPrevPage, createRedo, createTextLayer, createToolButton, createToolController, createToolbar, createToolbarSeparator, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, fitZoomCss, getSelectionQuads, highlightTool, hitFor, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, redactAreaTool, renderAnnotationContent, selectTool, shouldKeepOpenOnAnnotation, viewTool, visibleRangeCss };
|
|
3363
3363
|
|
|
3364
3364
|
//# sourceMappingURL=dom.mjs.map
|