@inkbar/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dom.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { B as mergeLineRects, F as confidenceBucket, H as readonly, K as invertMatrix, L as cssVariableDefaults, Q as rectFromPoints, U as writable, W as applyMatrix, a as acceptSuggestion, at as InkError, et as rectsIntersect, i as threadRoot, nt as translateRect, o as openDocument, ot as isInkError, r as threadOf, t as isAbandonedNote, tt as translateQuad } from "./threads-BtvtrAhL.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) {
@@ -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;
@@ -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, toolbar: withToolbar = true, notes: withNotes = true, suggestions: withSuggestions = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, ...view } = opts;
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: (viewport, tools) => {
3259
- toolbar?.bind(doc, viewport, tools);
3260
- onBuild?.(viewport, tools);
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
- toolbar?.unbind();
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
- toolbar: toolbar?.element ?? null,
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
- export { TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, attachViewportKeys, builtinTools, computeCssLayout, confidenceAttr, createFocusReturn, createOverlay, createPageMount, createPageRenderer, createPageTransform, createTextLayer, createToolController, createViewer, createViewport, cssRectsToUserRects, cssVariableDefaults, fitZoomCss, getSelectionQuads, highlightTool, hitFor, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, redactAreaTool, renderAnnotationContent, selectTool, shouldKeepOpenOnAnnotation, viewTool, visibleRangeCss };
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