@embedpdf/web 3.0.0-next.4 → 3.0.0-next.6
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/index.cjs +182 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -1
- package/dist/index.d.ts +181 -1
- package/dist/index.js +177 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,136 @@ function saveFile(bytes, name, mimeType) {
|
|
|
60
60
|
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
61
61
|
}
|
|
62
62
|
//#endregion
|
|
63
|
+
//#region src/clipboard.ts
|
|
64
|
+
const PREFETCH_DEBOUNCE_MS = 150;
|
|
65
|
+
/**
|
|
66
|
+
* Wire clipboard copy for a selection. Two delivery paths, both fed by the
|
|
67
|
+
* prefetched text:
|
|
68
|
+
*
|
|
69
|
+
* - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page
|
|
70
|
+
* has a DOM selection or focused editable — e.g. the viewer shell's
|
|
71
|
+
* focus sink): answered SYNCHRONOUSLY from the cache;
|
|
72
|
+
* - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually
|
|
73
|
+
* have none, and browsers don't dispatch `copy` then): a keydown
|
|
74
|
+
* fallback writes via the async Clipboard API inside the keystroke's
|
|
75
|
+
* user-activation window.
|
|
76
|
+
*
|
|
77
|
+
* Returns the unwire function. Mount once per document view (not per page).
|
|
78
|
+
*/
|
|
79
|
+
function wireSelectionClipboard(selection, options = {}) {
|
|
80
|
+
const target = options.target ?? document;
|
|
81
|
+
const prefetch = options.prefetch ?? true;
|
|
82
|
+
let cached = null;
|
|
83
|
+
let generation = 0;
|
|
84
|
+
let timer = null;
|
|
85
|
+
const refresh = () => {
|
|
86
|
+
cached = null;
|
|
87
|
+
if (!prefetch || !selection.hasSelection() || !selection.canCopy()) return;
|
|
88
|
+
const gen = ++generation;
|
|
89
|
+
selection.readText().then((text) => {
|
|
90
|
+
if (gen === generation) cached = text;
|
|
91
|
+
}, () => {});
|
|
92
|
+
};
|
|
93
|
+
const offCommit = selection.onCommit(() => {
|
|
94
|
+
if (timer) clearTimeout(timer);
|
|
95
|
+
timer = null;
|
|
96
|
+
refresh();
|
|
97
|
+
});
|
|
98
|
+
const offChange = selection.onChange(() => {
|
|
99
|
+
generation++;
|
|
100
|
+
cached = null;
|
|
101
|
+
if (timer) clearTimeout(timer);
|
|
102
|
+
timer = setTimeout(refresh, PREFETCH_DEBOUNCE_MS);
|
|
103
|
+
});
|
|
104
|
+
const onCopy = (e) => {
|
|
105
|
+
if (cached == null || cached === "" || !selection.hasSelection()) return;
|
|
106
|
+
e.clipboardData?.setData("text/plain", cached);
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
};
|
|
109
|
+
const onKeyDown = (e) => {
|
|
110
|
+
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "c" || e.shiftKey || e.altKey) return;
|
|
111
|
+
if (!selection.hasSelection() || !selection.canCopy()) return;
|
|
112
|
+
const dom = target.getSelection?.();
|
|
113
|
+
if (dom && !dom.isCollapsed) return;
|
|
114
|
+
copySelection(selection);
|
|
115
|
+
};
|
|
116
|
+
target.addEventListener("copy", onCopy);
|
|
117
|
+
target.addEventListener("keydown", onKeyDown);
|
|
118
|
+
return () => {
|
|
119
|
+
offCommit();
|
|
120
|
+
offChange();
|
|
121
|
+
if (timer) clearTimeout(timer);
|
|
122
|
+
target.removeEventListener("copy", onCopy);
|
|
123
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Toolbar/menu path: read the selected text and write it to the clipboard
|
|
128
|
+
* via the async Clipboard API. Call from a user-gesture handler (browsers
|
|
129
|
+
* require a secure context + user activation for clipboard writes). Resolves
|
|
130
|
+
* with what was copied ('' when nothing was — nothing is written then, so an
|
|
131
|
+
* empty selection never clobbers the user's clipboard).
|
|
132
|
+
*/
|
|
133
|
+
async function copySelection(selection) {
|
|
134
|
+
const text = await selection.readText();
|
|
135
|
+
if (text !== "") await navigator.clipboard.writeText(text);
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/anchored-position.ts
|
|
140
|
+
/**
|
|
141
|
+
* THE anchored projection: anchor → screen position, in one pure call —
|
|
142
|
+
* projection, avoid-point transform, and the shared placement policy.
|
|
143
|
+
* Frameworks recompute this inside their native reactive primitive
|
|
144
|
+
* (a React render, a Vue `computed`, an Angular signal, a Svelte
|
|
145
|
+
* `$derived`); nothing here schedules anything.
|
|
146
|
+
*/
|
|
147
|
+
function projectAnchoredTarget(projector, anchor, placement, gap) {
|
|
148
|
+
const box = projector.toScreen(anchor.pon, anchor.bounds);
|
|
149
|
+
if (!box) return null;
|
|
150
|
+
return positionAnchoredRect(box, placement, gap, anchor.avoid?.length ? projector.toScreenPoint(anchor.pon, anchor.avoid[0]) : null);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The one genuinely BROWSER-driven invalidation: an element in normal
|
|
154
|
+
* document flow moves when the document scrolls or the window resizes —
|
|
155
|
+
* no state change announces it. `<PageView>`'s binding registers this;
|
|
156
|
+
* the Stage never does (its camera is state, delivered through render).
|
|
157
|
+
*/
|
|
158
|
+
function observeClientGeometry(callback) {
|
|
159
|
+
window.addEventListener("scroll", callback, true);
|
|
160
|
+
window.addEventListener("resize", callback);
|
|
161
|
+
return () => {
|
|
162
|
+
window.removeEventListener("scroll", callback, true);
|
|
163
|
+
window.removeEventListener("resize", callback);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function positionAnchoredRect(box, placement, gap, avoid) {
|
|
167
|
+
const cx = box.x + box.width / 2;
|
|
168
|
+
const cy = box.y + box.height / 2;
|
|
169
|
+
switch (placement) {
|
|
170
|
+
case "bottom": return {
|
|
171
|
+
left: cx,
|
|
172
|
+
top: Math.max(box.y + box.height, avoid ? avoid.y : -Infinity) + gap,
|
|
173
|
+
transform: "translate(-50%, 0)"
|
|
174
|
+
};
|
|
175
|
+
case "left": return {
|
|
176
|
+
left: Math.min(box.x, avoid ? avoid.x : Infinity) - gap,
|
|
177
|
+
top: cy,
|
|
178
|
+
transform: "translate(-100%, -50%)"
|
|
179
|
+
};
|
|
180
|
+
case "right": return {
|
|
181
|
+
left: Math.max(box.x + box.width, avoid ? avoid.x : -Infinity) + gap,
|
|
182
|
+
top: cy,
|
|
183
|
+
transform: "translate(0, -50%)"
|
|
184
|
+
};
|
|
185
|
+
default: return {
|
|
186
|
+
left: cx,
|
|
187
|
+
top: Math.min(box.y, avoid ? avoid.y : Infinity) - gap,
|
|
188
|
+
transform: "translate(-50%, -100%)"
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
63
193
|
//#region src/cursor.ts
|
|
64
194
|
/** Build the CSS cursor value: `url("data:image/svg+xml,…") x y, fallback`. */
|
|
65
195
|
function svgCursor({ svg, hotspot, fallback = "crosshair" }) {
|
|
@@ -105,10 +235,62 @@ function sanitizeExternalUri(uri, allowedSchemes = DEFAULT_ALLOWED_SCHEMES) {
|
|
|
105
235
|
}
|
|
106
236
|
}
|
|
107
237
|
//#endregion
|
|
238
|
+
//#region src/painted-image.ts
|
|
239
|
+
/**
|
|
240
|
+
* Bind a raster source to an <img> without ever painting its incomplete-image
|
|
241
|
+
* placeholder. The source becomes visible on load; retained coverage is
|
|
242
|
+
* released only after the browser has had one full frame to present it.
|
|
243
|
+
*
|
|
244
|
+
* Call this from the framework's pre-paint mount hook and invoke the returned
|
|
245
|
+
* cleanup on unmount. Keeping this DOM lifecycle here lets every framework
|
|
246
|
+
* adapter remain a ref plus one bind/unbind hook.
|
|
247
|
+
*/
|
|
248
|
+
function bindPaintedImage(image, source, callbacks) {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
let revoke;
|
|
251
|
+
let frame = 0;
|
|
252
|
+
let painted = false;
|
|
253
|
+
image.style.visibility = "hidden";
|
|
254
|
+
const onLoad = () => {
|
|
255
|
+
image.style.visibility = "visible";
|
|
256
|
+
frame = requestAnimationFrame(() => {
|
|
257
|
+
frame = requestAnimationFrame(() => {
|
|
258
|
+
frame = 0;
|
|
259
|
+
painted = true;
|
|
260
|
+
callbacks.onPainted();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
image.addEventListener("load", onLoad, { once: true });
|
|
265
|
+
source.objectUrl(controller.signal).then((objectUrl) => {
|
|
266
|
+
if (controller.signal.aborted) {
|
|
267
|
+
objectUrl.revoke();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
revoke = objectUrl.revoke;
|
|
271
|
+
image.src = objectUrl.url;
|
|
272
|
+
}, () => {});
|
|
273
|
+
return () => {
|
|
274
|
+
controller.abort();
|
|
275
|
+
if (frame) cancelAnimationFrame(frame);
|
|
276
|
+
image.removeEventListener("load", onLoad);
|
|
277
|
+
image.style.visibility = "hidden";
|
|
278
|
+
image.removeAttribute("src");
|
|
279
|
+
revoke?.();
|
|
280
|
+
if (painted) callbacks.onUnpainted();
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
exports.bindPaintedImage = bindPaintedImage;
|
|
285
|
+
exports.copySelection = copySelection;
|
|
286
|
+
exports.observeClientGeometry = observeClientGeometry;
|
|
108
287
|
exports.pickFile = pickFile;
|
|
109
288
|
exports.pickImageFile = pickImageFile;
|
|
289
|
+
exports.positionAnchoredRect = positionAnchoredRect;
|
|
290
|
+
exports.projectAnchoredTarget = projectAnchoredTarget;
|
|
110
291
|
exports.sanitizeExternalUri = sanitizeExternalUri;
|
|
111
292
|
exports.saveFile = saveFile;
|
|
112
293
|
exports.svgCursor = svgCursor;
|
|
294
|
+
exports.wireSelectionClipboard = wireSelectionClipboard;
|
|
113
295
|
|
|
114
296
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/file-picker.ts","../src/cursor.ts","../src/external-uri.ts"],"sourcesContent":["/**\n * A one-shot file dialog. Opens a throwaway `<input type=file>`, resolves the\n * chosen `File`, or `null` when the dialog is dismissed. MUST be called\n * synchronously from a user gesture (a click) or the browser refuses to open it.\n *\n * A `File` is a `Blob`, which is a `BinarySource` everywhere in the stack, so the\n * caller can hand the result straight to the engine — this module stays a pure\n * DOM utility with no EmbedPDF types.\n */\nconst IMAGE_ACCEPT = 'image/png,image/jpeg,application/pdf';\n\nexport interface PickFileOptions {\n /** The dialog's `accept` filter. Defaults to PNG / JPEG / PDF. */\n accept?: string;\n /** Allow multiple selection (returns the first file). Defaults to false. */\n multiple?: boolean;\n}\n\n/** Open the file dialog; resolve the picked file, or null if cancelled. */\nexport function pickImageFile(options: PickFileOptions = {}): Promise<File | null> {\n const { accept = IMAGE_ACCEPT, multiple = false } = options;\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.multiple = multiple;\n input.style.display = 'none';\n\n let settled = false;\n const finish = (value: File | null): void => {\n if (settled) return;\n settled = true;\n window.removeEventListener('focus', onFocus);\n input.remove();\n resolve(value);\n };\n\n // A window refocus means the dialog closed. Give a queued `change` a tick to\n // win, then treat a bare refocus as a cancel — the fallback for browsers that\n // predate the `cancel` event.\n const onFocus = (): void => {\n setTimeout(() => finish(null), 300);\n };\n\n input.addEventListener('change', () => finish(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => finish(null), { once: true });\n window.addEventListener('focus', onFocus, { once: true });\n\n document.body.appendChild(input);\n input.click();\n });\n}\n\n/** The same dialog with NO type filter — any file (attachments). */\nexport const pickFile = (options: PickFileOptions = {}): Promise<File | null> =>\n pickImageFile({ accept: '*/*', ...options });\n\n/**\n * Hand bytes to the user as a browser download (a one-shot `<a download>`).\n * The DOM half of \"download this attachment\" — pair with the annotation\n * capability's `downloadAttachment` or the document `attachments.download`.\n */\nexport function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void {\n const blob =\n bytes instanceof Blob\n ? bytes\n : new Blob([bytes as BlobPart], { type: mimeType ?? 'application/octet-stream' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = name;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n","/**\n * SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the\n * only zero-latency pointer-locked pixel the web has — the OS composites it),\n * so an app hands its toolbar icon here and assigns the result via the\n * interaction hub's `setToolCursor`.\n *\n * Browser rules baked into this module:\n * - The SVG must carry explicit `width`/`height` attributes (not just a\n * viewBox) or Chromium/WebKit ignore it — enforced here.\n * - The hard ceiling is 128×128 CSS px (bigger falls back to the keyword).\n * Above 32×32, Chromium temporarily resets the cursor to default while it\n * would overlap browser UI (anti-spoofing) — fine mid-viewport, so ~40px\n * compositions are common; stay ≤128.\n * - The keyword fallback covers browsers that refuse SVG cursor images.\n *\n * Pure string work with no EmbedPDF types — like the file picker, this module\n * is a plain web utility.\n */\n\nexport interface SvgCursorOptions {\n /** Complete `<svg>` markup, colors baked in (no `currentColor` — a cursor\n * image has no cascade to inherit from). */\n svg: string;\n /** The click point inside the image, CSS px from the top-left. Default 0,0\n * (arrow-style: the image's corner is the tip). */\n hotspot?: { x: number; y: number };\n /** Keyword cursor when the image can't render. Default `'crosshair'`. */\n fallback?: string;\n}\n\n/** Build the CSS cursor value: `url(\"data:image/svg+xml,…\") x y, fallback`. */\nexport function svgCursor({ svg, hotspot, fallback = 'crosshair' }: SvgCursorOptions): string {\n // Scan only the opening `<svg …>` tag (bounded slice) rather than running\n // `/<svg[^>]*\\swidth\\s*=/` over the whole string — the unbounded form is\n // quadratic on pathological inputs (many `<svg` fragments with no `>`),\n // while accepting exactly the same markup: `[^>]*` could never cross a\n // `>` either.\n const tagStart = svg.indexOf('<svg');\n const tagEnd = tagStart >= 0 ? svg.indexOf('>', tagStart) : -1;\n const openTag = tagEnd >= 0 ? svg.slice(tagStart, tagEnd + 1) : '';\n if (!/\\swidth\\s*=/.test(openTag) || !/\\sheight\\s*=/.test(openTag)) {\n throw new Error(\n '[web] svgCursor: the <svg> needs explicit width/height attributes — ' +\n 'browsers ignore cursor images without an intrinsic size',\n );\n }\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n const spot = hotspot ? ` ${Math.round(hotspot.x)} ${Math.round(hotspot.y)}` : '';\n return `url(\"${uri}\")${spot}, ${fallback}`;\n}\n","/**\n * External-URI hygiene for PDF link annotations — written ONCE here so every\n * framework adapter (react, vue, svelte, angular) shares the same policy.\n *\n * PDFs are untrusted input: a link's `/URI` can carry anything, including\n * `javascript:` payloads. The default allowlist covers what a document link\n * legitimately opens; everything else (javascript:, file:, data:, vbscript:,\n * custom app schemes) resolves to `null` and the adapters render the link\n * WITHOUT an href — activation still reports through the plugin, so an\n * embedder that needs a custom scheme can handle it in `onActivate`.\n */\nconst DEFAULT_ALLOWED_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * Validate a link URI for opening in the browser. Returns the URI when its\n * scheme is allowed (scheme-relative and bare URIs resolve as https), else\n * `null`. Never throws — a malformed URI is just an unopenable one.\n */\nexport function sanitizeExternalUri(\n uri: string,\n allowedSchemes: Iterable<string> = DEFAULT_ALLOWED_SCHEMES,\n): string | null {\n const trimmed = uri.trim();\n if (!trimmed) return null;\n const allowed = new Set(\n [...allowedSchemes].map((s) => (s.endsWith(':') ? s.toLowerCase() : `${s.toLowerCase()}:`)),\n );\n try {\n // Base resolves scheme-relative (\"//host\") and bare (\"example.com/x\" is a\n // relative path → https) forms; an absolute scheme wins over the base.\n const url = new URL(trimmed, 'https://invalid.example/');\n return allowed.has(url.protocol) ? trimmed : null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;AASA,MAAM,eAAe;;AAUrB,SAAgB,cAAc,UAA2B,CAAC,GAAyB;CACjF,MAAM,EAAE,SAAS,cAAc,WAAW,UAAU;CACpD,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,OAAO;EACb,MAAM,SAAS;EACf,MAAM,WAAW;EACjB,MAAM,MAAM,UAAU;EAEtB,IAAI,UAAU;EACd,MAAM,UAAU,UAA6B;GAC3C,IAAI,SAAS;GACb,UAAU;GACV,OAAO,oBAAoB,SAAS,OAAO;GAC3C,MAAM,OAAO;GACb,QAAQ,KAAK;EACf;EAKA,MAAM,gBAAsB;GAC1B,iBAAiB,OAAO,IAAI,GAAG,GAAG;EACpC;EAEA,MAAM,iBAAiB,gBAAgB,OAAO,MAAM,QAAQ,MAAM,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACvF,MAAM,iBAAiB,gBAAgB,OAAO,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACnE,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAExD,SAAS,KAAK,YAAY,KAAK;EAC/B,MAAM,MAAM;CACd,CAAC;AACH;;AAGA,MAAa,YAAY,UAA2B,CAAC,MACnD,cAAc;CAAE,QAAQ;CAAO,GAAG;AAAQ,CAAC;;;;;;AAO7C,SAAgB,SAAS,OAA0B,MAAc,UAAyB;CACxF,MAAM,OACJ,iBAAiB,OACb,QACA,IAAI,KAAK,CAAC,KAAiB,GAAG,EAAE,MAAM,YAAY,2BAA2B,CAAC;CACpF,MAAM,MAAM,IAAI,gBAAgB,IAAI;CACpC,MAAM,IAAI,SAAS,cAAc,GAAG;CACpC,EAAE,OAAO;CACT,EAAE,WAAW;CACb,EAAE,MAAM,UAAU;CAClB,SAAS,KAAK,YAAY,CAAC;CAC3B,EAAE,MAAM;CACR,EAAE,OAAO;CACT,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,GAAI;AACjD;;;;AC7CA,SAAgB,UAAU,EAAE,KAAK,SAAS,WAAW,eAAyC;CAM5F,MAAM,WAAW,IAAI,QAAQ,MAAM;CACnC,MAAM,SAAS,YAAY,IAAI,IAAI,QAAQ,KAAK,QAAQ,IAAI;CAC5D,MAAM,UAAU,UAAU,IAAI,IAAI,MAAM,UAAU,SAAS,CAAC,IAAI;CAChE,IAAI,CAAC,cAAc,KAAK,OAAO,KAAK,CAAC,eAAe,KAAK,OAAO,GAC9D,MAAM,IAAI,MACR,6HAEF;CAIF,OAAO,QAAQ,sBAFmB,mBAAmB,GAAG,IAErC,IADN,UAAU,IAAI,KAAK,MAAM,QAAQ,CAAC,EAAE,GAAG,KAAK,MAAM,QAAQ,CAAC,MAAM,GAClD,IAAI;AAClC;;;;;;;;;;;;;;ACtCA,MAAM,0CAA0B,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;AAO9E,SAAgB,oBACd,KACA,iBAAmC,yBACpB;CACf,MAAM,UAAU,IAAI,KAAK;CACzB,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,UAAU,IAAI,IAClB,CAAC,GAAG,cAAc,CAAC,CAAC,KAAK,MAAO,EAAE,SAAS,GAAG,IAAI,EAAE,YAAY,IAAI,GAAG,EAAE,YAAY,EAAE,EAAG,CAC5F;CACA,IAAI;EAGF,MAAM,MAAM,IAAI,IAAI,SAAS,0BAA0B;EACvD,OAAO,QAAQ,IAAI,IAAI,QAAQ,IAAI,UAAU;CAC/C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/file-picker.ts","../src/clipboard.ts","../src/anchored-position.ts","../src/cursor.ts","../src/external-uri.ts","../src/painted-image.ts"],"sourcesContent":["/**\n * A one-shot file dialog. Opens a throwaway `<input type=file>`, resolves the\n * chosen `File`, or `null` when the dialog is dismissed. MUST be called\n * synchronously from a user gesture (a click) or the browser refuses to open it.\n *\n * A `File` is a `Blob`, which is a `BinarySource` everywhere in the stack, so the\n * caller can hand the result straight to the engine — this module stays a pure\n * DOM utility with no EmbedPDF types.\n */\nconst IMAGE_ACCEPT = 'image/png,image/jpeg,application/pdf';\n\nexport interface PickFileOptions {\n /** The dialog's `accept` filter. Defaults to PNG / JPEG / PDF. */\n accept?: string;\n /** Allow multiple selection (returns the first file). Defaults to false. */\n multiple?: boolean;\n}\n\n/** Open the file dialog; resolve the picked file, or null if cancelled. */\nexport function pickImageFile(options: PickFileOptions = {}): Promise<File | null> {\n const { accept = IMAGE_ACCEPT, multiple = false } = options;\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.multiple = multiple;\n input.style.display = 'none';\n\n let settled = false;\n const finish = (value: File | null): void => {\n if (settled) return;\n settled = true;\n window.removeEventListener('focus', onFocus);\n input.remove();\n resolve(value);\n };\n\n // A window refocus means the dialog closed. Give a queued `change` a tick to\n // win, then treat a bare refocus as a cancel — the fallback for browsers that\n // predate the `cancel` event.\n const onFocus = (): void => {\n setTimeout(() => finish(null), 300);\n };\n\n input.addEventListener('change', () => finish(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => finish(null), { once: true });\n window.addEventListener('focus', onFocus, { once: true });\n\n document.body.appendChild(input);\n input.click();\n });\n}\n\n/** The same dialog with NO type filter — any file (attachments). */\nexport const pickFile = (options: PickFileOptions = {}): Promise<File | null> =>\n pickImageFile({ accept: '*/*', ...options });\n\n/**\n * Hand bytes to the user as a browser download (a one-shot `<a download>`).\n * The DOM half of \"download this attachment\" — pair with the annotation\n * capability's `downloadAttachment` or the document `attachments.download`.\n */\nexport function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void {\n const blob =\n bytes instanceof Blob\n ? bytes\n : new Blob([bytes as BlobPart], { type: mimeType ?? 'application/octet-stream' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = name;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n","/**\n * Clipboard wiring for text selection — the environmental half of copy.\n *\n * The selection plugin is deliberately DOM-free: its public capability ends\n * at `readText()` (data). Everything that touches `navigator.clipboard` or\n * the native `copy` event lives HERE, structurally typed so this module\n * stays a pure DOM utility with no EmbedPDF dependencies (the selection\n * capability satisfies {@link ClipboardSelectionSource} as-is).\n */\n\n/** The slice of the selection capability clipboard wiring needs. */\nexport interface ClipboardSelectionSource {\n hasSelection(): boolean;\n canCopy(): boolean;\n readText(): Promise<string>;\n onChange(cb: () => void): () => void;\n onCommit(cb: () => void): () => void;\n}\n\nexport interface SelectionClipboardOptions {\n /**\n * Prefetch the selected text when the selection settles (default true).\n * This is what makes the NATIVE copy path work: a `copy` event handler\n * must call `clipboardData.setData` synchronously — awaiting a page-text\n * read inside it is too late — and it also keeps the async path instant.\n * The fetch is one versioned, cached, permission-gated read per page;\n * pass false to make copying strictly on-demand.\n */\n prefetch?: boolean;\n /** Event target to listen on. Defaults to `document`. */\n target?: Document;\n}\n\nconst PREFETCH_DEBOUNCE_MS = 150;\n\n/**\n * Wire clipboard copy for a selection. Two delivery paths, both fed by the\n * prefetched text:\n *\n * - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page\n * has a DOM selection or focused editable — e.g. the viewer shell's\n * focus sink): answered SYNCHRONOUSLY from the cache;\n * - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually\n * have none, and browsers don't dispatch `copy` then): a keydown\n * fallback writes via the async Clipboard API inside the keystroke's\n * user-activation window.\n *\n * Returns the unwire function. Mount once per document view (not per page).\n */\nexport function wireSelectionClipboard(\n selection: ClipboardSelectionSource,\n options: SelectionClipboardOptions = {},\n): () => void {\n const target = options.target ?? document;\n const prefetch = options.prefetch ?? true;\n let cached: string | null = null;\n let generation = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const refresh = (): void => {\n cached = null;\n if (!prefetch || !selection.hasSelection() || !selection.canCopy()) return;\n const gen = ++generation;\n selection.readText().then(\n (text) => {\n if (gen === generation) cached = text;\n },\n () => {}, // denied / failed — the async path will surface it if used\n );\n };\n\n // Commit (gesture end) refreshes immediately; change (drag, programmatic\n // select) debounces so a drag doesn't fetch per pointer move.\n const offCommit = selection.onCommit(() => {\n if (timer) clearTimeout(timer);\n timer = null;\n refresh();\n });\n const offChange = selection.onChange(() => {\n generation++;\n cached = null;\n if (timer) clearTimeout(timer);\n timer = setTimeout(refresh, PREFETCH_DEBOUNCE_MS);\n });\n\n const onCopy = (e: ClipboardEvent): void => {\n if (cached == null || cached === '' || !selection.hasSelection()) return;\n e.clipboardData?.setData('text/plain', cached);\n e.preventDefault();\n };\n\n const onKeyDown = (e: KeyboardEvent): void => {\n if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'c' || e.shiftKey || e.altKey) {\n return;\n }\n if (!selection.hasSelection() || !selection.canCopy()) return;\n // A live DOM selection means the browser will dispatch `copy` itself and\n // our synchronous handler answers — don't double-write.\n const dom = target.getSelection?.();\n if (dom && !dom.isCollapsed) return;\n void copySelection(selection); // instant when prefetched; still inside user activation\n };\n\n target.addEventListener('copy', onCopy);\n target.addEventListener('keydown', onKeyDown);\n return () => {\n offCommit();\n offChange();\n if (timer) clearTimeout(timer);\n target.removeEventListener('copy', onCopy);\n target.removeEventListener('keydown', onKeyDown);\n };\n}\n\n/**\n * Toolbar/menu path: read the selected text and write it to the clipboard\n * via the async Clipboard API. Call from a user-gesture handler (browsers\n * require a secure context + user activation for clipboard writes). Resolves\n * with what was copied ('' when nothing was — nothing is written then, so an\n * empty selection never clobbers the user's clipboard).\n */\nexport async function copySelection(selection: ClipboardSelectionSource): Promise<string> {\n const text = await selection.readText();\n if (text !== '') await navigator.clipboard.writeText(text);\n return text;\n}\n","/**\n * Anchored-overlay geometry for UI floating over page content (selection\n * menus, draft menus, popovers): the projector SNAPSHOT contract, the\n * anchor shape, and the placement math. Pure values and pure functions over\n * structural rect/point shapes — no EmbedPDF types, no framework, no\n * reactive lifecycle — so every framework adapter (React, Vue, Svelte,\n * Angular) shares ONE implementation and binds it with its NATIVE\n * reactivity (context revision, signals, computed, $derived).\n *\n * The boundary laws:\n * - Placement policy lives HERE, above the projector seam: a projector\n * only knows geometry — never what is being positioned nor where it\n * prefers to sit.\n * - A projector describes the CURRENT projection; it never implements a\n * framework-style reactive lifecycle. State-driven changes (the Stage\n * camera) must reach consumers through the framework's own render\n * cycle so surface and overlay commit TOGETHER; only genuinely\n * browser-driven changes (a PageView moving because the document\n * scrolled) use an external listener ({@link observeClientGeometry}).\n */\n\nexport interface AnchoredRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface AnchoredPoint {\n x: number;\n y: number;\n}\n\nexport type AnchoredPlacement = 'top' | 'right' | 'bottom' | 'left';\n\n/** CSS-ready output: absolute/fixed `left`/`top` plus a centering transform. */\nexport interface AnchoredPosition {\n left: number;\n top: number;\n transform: string;\n}\n\n/**\n * Place an upright element around `box` (screen px). `avoid` is a screen\n * point the element must clear (e.g. the rotate knob): the element extends\n * ONLY the edge it sits on, and ONLY when the point protrudes past that\n * edge — so it clears the obstacle without ever shifting off-centre on the\n * other axis. When the point is on another side (e.g. a 90° shape, knob at\n * mid-height for a `top` placement) the edge is untouched and the element\n * stays centred on `box`.\n */\n/** What anchored UI attaches to: a content-space rect on a page, plus\n * optional content-space points the UI must clear (e.g. a rotate knob).\n * Structural — plugin anchor reads satisfy it without importing this\n * package. */\nexport interface AnchorTarget {\n pon: number;\n bounds: AnchoredRect;\n avoid?: AnchoredPoint[];\n}\n\n/**\n * A page surface's PROJECTION SNAPSHOT: how a content-space rect on a page\n * becomes screen coordinates, right now. Provided by `<Stage>`\n * (camera-driven, pure state, no DOM reads) and `<PageView>` (DOM-measured).\n * Deliberately NO subscribe here — when projection changes is a framework\n * binding concern (see the module doc), not part of the snapshot.\n */\nexport interface ViewProjector {\n /**\n * Which coordinate space `toScreen` speaks, and therefore how anchored\n * content mounts:\n * - 'overlay': coords are relative to the surface's own positioned\n * container → rendered in place, position:absolute. (Stage)\n * - 'client': coords are viewport px → portalled to the document body,\n * position:fixed, so no ancestor overflow can clip. (PageView)\n */\n space: 'overlay' | 'client';\n /** Content-space rect on a page → coords in `space`. Null: not projectable\n * right now (page not shown / not measurable yet). */\n toScreen(pon: number, rect: AnchoredRect): AnchoredRect | null;\n toScreenPoint(pon: number, at: AnchoredPoint): AnchoredPoint | null;\n /** The page's live view facts (for anchor reads that need them — e.g. the\n * screen-constant rotate-knob stalk), or null when the page isn't shown. */\n viewEnv(pon: number): {\n scale: number;\n rotation: 0 | 90 | 180 | 270;\n zoom: number;\n } | null;\n}\n\n/**\n * THE anchored projection: anchor → screen position, in one pure call —\n * projection, avoid-point transform, and the shared placement policy.\n * Frameworks recompute this inside their native reactive primitive\n * (a React render, a Vue `computed`, an Angular signal, a Svelte\n * `$derived`); nothing here schedules anything.\n */\nexport function projectAnchoredTarget(\n projector: ViewProjector,\n anchor: AnchorTarget,\n placement: AnchoredPlacement,\n gap: number,\n): AnchoredPosition | null {\n const box = projector.toScreen(anchor.pon, anchor.bounds);\n if (!box) return null;\n const avoid = anchor.avoid?.length\n ? projector.toScreenPoint(anchor.pon, anchor.avoid[0])\n : null;\n return positionAnchoredRect(box, placement, gap, avoid);\n}\n\n/**\n * The one genuinely BROWSER-driven invalidation: an element in normal\n * document flow moves when the document scrolls or the window resizes —\n * no state change announces it. `<PageView>`'s binding registers this;\n * the Stage never does (its camera is state, delivered through render).\n */\nexport function observeClientGeometry(callback: () => void): () => void {\n window.addEventListener('scroll', callback, true);\n window.addEventListener('resize', callback);\n return () => {\n window.removeEventListener('scroll', callback, true);\n window.removeEventListener('resize', callback);\n };\n}\n\nexport function positionAnchoredRect(\n box: AnchoredRect,\n placement: AnchoredPlacement,\n gap: number,\n avoid?: AnchoredPoint | null,\n): AnchoredPosition {\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n\n switch (placement) {\n case 'bottom': {\n const edge = Math.max(box.y + box.height, avoid ? avoid.y : -Infinity);\n return { left: cx, top: edge + gap, transform: 'translate(-50%, 0)' };\n }\n case 'left': {\n const edge = Math.min(box.x, avoid ? avoid.x : Infinity);\n return { left: edge - gap, top: cy, transform: 'translate(-100%, -50%)' };\n }\n case 'right': {\n const edge = Math.max(box.x + box.width, avoid ? avoid.x : -Infinity);\n return { left: edge + gap, top: cy, transform: 'translate(0, -50%)' };\n }\n case 'top':\n default: {\n const edge = Math.min(box.y, avoid ? avoid.y : Infinity);\n return { left: cx, top: edge - gap, transform: 'translate(-50%, -100%)' };\n }\n }\n}\n","/**\n * SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the\n * only zero-latency pointer-locked pixel the web has — the OS composites it),\n * so an app hands its toolbar icon here and assigns the result via the\n * interaction hub's `setToolCursor`.\n *\n * Browser rules baked into this module:\n * - The SVG must carry explicit `width`/`height` attributes (not just a\n * viewBox) or Chromium/WebKit ignore it — enforced here.\n * - The hard ceiling is 128×128 CSS px (bigger falls back to the keyword).\n * Above 32×32, Chromium temporarily resets the cursor to default while it\n * would overlap browser UI (anti-spoofing) — fine mid-viewport, so ~40px\n * compositions are common; stay ≤128.\n * - The keyword fallback covers browsers that refuse SVG cursor images.\n *\n * Pure string work with no EmbedPDF types — like the file picker, this module\n * is a plain web utility.\n */\n\nexport interface SvgCursorOptions {\n /** Complete `<svg>` markup, colors baked in (no `currentColor` — a cursor\n * image has no cascade to inherit from). */\n svg: string;\n /** The click point inside the image, CSS px from the top-left. Default 0,0\n * (arrow-style: the image's corner is the tip). */\n hotspot?: { x: number; y: number };\n /** Keyword cursor when the image can't render. Default `'crosshair'`. */\n fallback?: string;\n}\n\n/** Build the CSS cursor value: `url(\"data:image/svg+xml,…\") x y, fallback`. */\nexport function svgCursor({ svg, hotspot, fallback = 'crosshair' }: SvgCursorOptions): string {\n // Scan only the opening `<svg …>` tag (bounded slice) rather than running\n // `/<svg[^>]*\\swidth\\s*=/` over the whole string — the unbounded form is\n // quadratic on pathological inputs (many `<svg` fragments with no `>`),\n // while accepting exactly the same markup: `[^>]*` could never cross a\n // `>` either.\n const tagStart = svg.indexOf('<svg');\n const tagEnd = tagStart >= 0 ? svg.indexOf('>', tagStart) : -1;\n const openTag = tagEnd >= 0 ? svg.slice(tagStart, tagEnd + 1) : '';\n if (!/\\swidth\\s*=/.test(openTag) || !/\\sheight\\s*=/.test(openTag)) {\n throw new Error(\n '[web] svgCursor: the <svg> needs explicit width/height attributes — ' +\n 'browsers ignore cursor images without an intrinsic size',\n );\n }\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n const spot = hotspot ? ` ${Math.round(hotspot.x)} ${Math.round(hotspot.y)}` : '';\n return `url(\"${uri}\")${spot}, ${fallback}`;\n}\n","/**\n * External-URI hygiene for PDF link annotations — written ONCE here so every\n * framework adapter (react, vue, svelte, angular) shares the same policy.\n *\n * PDFs are untrusted input: a link's `/URI` can carry anything, including\n * `javascript:` payloads. The default allowlist covers what a document link\n * legitimately opens; everything else (javascript:, file:, data:, vbscript:,\n * custom app schemes) resolves to `null` and the adapters render the link\n * WITHOUT an href — activation still reports through the plugin, so an\n * embedder that needs a custom scheme can handle it in `onActivate`.\n */\nconst DEFAULT_ALLOWED_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * Validate a link URI for opening in the browser. Returns the URI when its\n * scheme is allowed (scheme-relative and bare URIs resolve as https), else\n * `null`. Never throws — a malformed URI is just an unopenable one.\n */\nexport function sanitizeExternalUri(\n uri: string,\n allowedSchemes: Iterable<string> = DEFAULT_ALLOWED_SCHEMES,\n): string | null {\n const trimmed = uri.trim();\n if (!trimmed) return null;\n const allowed = new Set(\n [...allowedSchemes].map((s) => (s.endsWith(':') ? s.toLowerCase() : `${s.toLowerCase()}:`)),\n );\n try {\n // Base resolves scheme-relative (\"//host\") and bare (\"example.com/x\" is a\n // relative path → https) forms; an absolute scheme wins over the base.\n const url = new URL(trimmed, 'https://invalid.example/');\n return allowed.has(url.protocol) ? trimmed : null;\n } catch {\n return null;\n }\n}\n","/** A browser image source whose object URL has an explicit lifetime. */\nexport interface ObjectUrlImageSource {\n objectUrl(signal?: AbortSignal): Promise<{ url: string; revoke(): void }>;\n}\n\nexport interface PaintedImageCallbacks {\n onPainted(): void;\n onUnpainted(): void;\n}\n\n/**\n * Bind a raster source to an <img> without ever painting its incomplete-image\n * placeholder. The source becomes visible on load; retained coverage is\n * released only after the browser has had one full frame to present it.\n *\n * Call this from the framework's pre-paint mount hook and invoke the returned\n * cleanup on unmount. Keeping this DOM lifecycle here lets every framework\n * adapter remain a ref plus one bind/unbind hook.\n */\nexport function bindPaintedImage(\n image: HTMLImageElement,\n source: ObjectUrlImageSource,\n callbacks: PaintedImageCallbacks,\n): () => void {\n const controller = new AbortController();\n let revoke: (() => void) | undefined;\n let frame = 0;\n let painted = false;\n\n // The element is already committed when a mount hook runs. Hide it before\n // that commit can paint, then reveal only once the bitmap has loaded.\n image.style.visibility = 'hidden';\n\n const onLoad = () => {\n image.style.visibility = 'visible';\n\n // rAF callbacks run before a frame paints. The first frame presents the\n // newly visible image while retained coverage remains; the second reports\n // that presentation opportunity and permits the retained source to leave.\n frame = requestAnimationFrame(() => {\n frame = requestAnimationFrame(() => {\n frame = 0;\n painted = true;\n callbacks.onPainted();\n });\n });\n };\n\n image.addEventListener('load', onLoad, { once: true });\n\n void source.objectUrl(controller.signal).then(\n (objectUrl) => {\n if (controller.signal.aborted) {\n objectUrl.revoke();\n return;\n }\n revoke = objectUrl.revoke;\n image.src = objectUrl.url;\n },\n () => {\n // Aborted or unavailable: remain hidden and retain fallback coverage.\n },\n );\n\n return () => {\n controller.abort();\n if (frame) cancelAnimationFrame(frame);\n image.removeEventListener('load', onLoad);\n image.style.visibility = 'hidden';\n image.removeAttribute('src');\n revoke?.();\n if (painted) callbacks.onUnpainted();\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -16,6 +16,163 @@ declare const pickFile: (options?: PickFileOptions) => Promise<File | null>;
|
|
|
16
16
|
*/
|
|
17
17
|
declare function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void;
|
|
18
18
|
//#endregion
|
|
19
|
+
//#region src/clipboard.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Clipboard wiring for text selection — the environmental half of copy.
|
|
22
|
+
*
|
|
23
|
+
* The selection plugin is deliberately DOM-free: its public capability ends
|
|
24
|
+
* at `readText()` (data). Everything that touches `navigator.clipboard` or
|
|
25
|
+
* the native `copy` event lives HERE, structurally typed so this module
|
|
26
|
+
* stays a pure DOM utility with no EmbedPDF dependencies (the selection
|
|
27
|
+
* capability satisfies {@link ClipboardSelectionSource} as-is).
|
|
28
|
+
*/
|
|
29
|
+
/** The slice of the selection capability clipboard wiring needs. */
|
|
30
|
+
interface ClipboardSelectionSource {
|
|
31
|
+
hasSelection(): boolean;
|
|
32
|
+
canCopy(): boolean;
|
|
33
|
+
readText(): Promise<string>;
|
|
34
|
+
onChange(cb: () => void): () => void;
|
|
35
|
+
onCommit(cb: () => void): () => void;
|
|
36
|
+
}
|
|
37
|
+
interface SelectionClipboardOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Prefetch the selected text when the selection settles (default true).
|
|
40
|
+
* This is what makes the NATIVE copy path work: a `copy` event handler
|
|
41
|
+
* must call `clipboardData.setData` synchronously — awaiting a page-text
|
|
42
|
+
* read inside it is too late — and it also keeps the async path instant.
|
|
43
|
+
* The fetch is one versioned, cached, permission-gated read per page;
|
|
44
|
+
* pass false to make copying strictly on-demand.
|
|
45
|
+
*/
|
|
46
|
+
prefetch?: boolean;
|
|
47
|
+
/** Event target to listen on. Defaults to `document`. */
|
|
48
|
+
target?: Document;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wire clipboard copy for a selection. Two delivery paths, both fed by the
|
|
52
|
+
* prefetched text:
|
|
53
|
+
*
|
|
54
|
+
* - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page
|
|
55
|
+
* has a DOM selection or focused editable — e.g. the viewer shell's
|
|
56
|
+
* focus sink): answered SYNCHRONOUSLY from the cache;
|
|
57
|
+
* - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually
|
|
58
|
+
* have none, and browsers don't dispatch `copy` then): a keydown
|
|
59
|
+
* fallback writes via the async Clipboard API inside the keystroke's
|
|
60
|
+
* user-activation window.
|
|
61
|
+
*
|
|
62
|
+
* Returns the unwire function. Mount once per document view (not per page).
|
|
63
|
+
*/
|
|
64
|
+
declare function wireSelectionClipboard(selection: ClipboardSelectionSource, options?: SelectionClipboardOptions): () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Toolbar/menu path: read the selected text and write it to the clipboard
|
|
67
|
+
* via the async Clipboard API. Call from a user-gesture handler (browsers
|
|
68
|
+
* require a secure context + user activation for clipboard writes). Resolves
|
|
69
|
+
* with what was copied ('' when nothing was — nothing is written then, so an
|
|
70
|
+
* empty selection never clobbers the user's clipboard).
|
|
71
|
+
*/
|
|
72
|
+
declare function copySelection(selection: ClipboardSelectionSource): Promise<string>;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/anchored-position.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* Anchored-overlay geometry for UI floating over page content (selection
|
|
77
|
+
* menus, draft menus, popovers): the projector SNAPSHOT contract, the
|
|
78
|
+
* anchor shape, and the placement math. Pure values and pure functions over
|
|
79
|
+
* structural rect/point shapes — no EmbedPDF types, no framework, no
|
|
80
|
+
* reactive lifecycle — so every framework adapter (React, Vue, Svelte,
|
|
81
|
+
* Angular) shares ONE implementation and binds it with its NATIVE
|
|
82
|
+
* reactivity (context revision, signals, computed, $derived).
|
|
83
|
+
*
|
|
84
|
+
* The boundary laws:
|
|
85
|
+
* - Placement policy lives HERE, above the projector seam: a projector
|
|
86
|
+
* only knows geometry — never what is being positioned nor where it
|
|
87
|
+
* prefers to sit.
|
|
88
|
+
* - A projector describes the CURRENT projection; it never implements a
|
|
89
|
+
* framework-style reactive lifecycle. State-driven changes (the Stage
|
|
90
|
+
* camera) must reach consumers through the framework's own render
|
|
91
|
+
* cycle so surface and overlay commit TOGETHER; only genuinely
|
|
92
|
+
* browser-driven changes (a PageView moving because the document
|
|
93
|
+
* scrolled) use an external listener ({@link observeClientGeometry}).
|
|
94
|
+
*/
|
|
95
|
+
interface AnchoredRect {
|
|
96
|
+
x: number;
|
|
97
|
+
y: number;
|
|
98
|
+
width: number;
|
|
99
|
+
height: number;
|
|
100
|
+
}
|
|
101
|
+
interface AnchoredPoint {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
}
|
|
105
|
+
type AnchoredPlacement = 'top' | 'right' | 'bottom' | 'left';
|
|
106
|
+
/** CSS-ready output: absolute/fixed `left`/`top` plus a centering transform. */
|
|
107
|
+
interface AnchoredPosition {
|
|
108
|
+
left: number;
|
|
109
|
+
top: number;
|
|
110
|
+
transform: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Place an upright element around `box` (screen px). `avoid` is a screen
|
|
114
|
+
* point the element must clear (e.g. the rotate knob): the element extends
|
|
115
|
+
* ONLY the edge it sits on, and ONLY when the point protrudes past that
|
|
116
|
+
* edge — so it clears the obstacle without ever shifting off-centre on the
|
|
117
|
+
* other axis. When the point is on another side (e.g. a 90° shape, knob at
|
|
118
|
+
* mid-height for a `top` placement) the edge is untouched and the element
|
|
119
|
+
* stays centred on `box`.
|
|
120
|
+
*/
|
|
121
|
+
/** What anchored UI attaches to: a content-space rect on a page, plus
|
|
122
|
+
* optional content-space points the UI must clear (e.g. a rotate knob).
|
|
123
|
+
* Structural — plugin anchor reads satisfy it without importing this
|
|
124
|
+
* package. */
|
|
125
|
+
interface AnchorTarget {
|
|
126
|
+
pon: number;
|
|
127
|
+
bounds: AnchoredRect;
|
|
128
|
+
avoid?: AnchoredPoint[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A page surface's PROJECTION SNAPSHOT: how a content-space rect on a page
|
|
132
|
+
* becomes screen coordinates, right now. Provided by `<Stage>`
|
|
133
|
+
* (camera-driven, pure state, no DOM reads) and `<PageView>` (DOM-measured).
|
|
134
|
+
* Deliberately NO subscribe here — when projection changes is a framework
|
|
135
|
+
* binding concern (see the module doc), not part of the snapshot.
|
|
136
|
+
*/
|
|
137
|
+
interface ViewProjector {
|
|
138
|
+
/**
|
|
139
|
+
* Which coordinate space `toScreen` speaks, and therefore how anchored
|
|
140
|
+
* content mounts:
|
|
141
|
+
* - 'overlay': coords are relative to the surface's own positioned
|
|
142
|
+
* container → rendered in place, position:absolute. (Stage)
|
|
143
|
+
* - 'client': coords are viewport px → portalled to the document body,
|
|
144
|
+
* position:fixed, so no ancestor overflow can clip. (PageView)
|
|
145
|
+
*/
|
|
146
|
+
space: 'overlay' | 'client';
|
|
147
|
+
/** Content-space rect on a page → coords in `space`. Null: not projectable
|
|
148
|
+
* right now (page not shown / not measurable yet). */
|
|
149
|
+
toScreen(pon: number, rect: AnchoredRect): AnchoredRect | null;
|
|
150
|
+
toScreenPoint(pon: number, at: AnchoredPoint): AnchoredPoint | null;
|
|
151
|
+
/** The page's live view facts (for anchor reads that need them — e.g. the
|
|
152
|
+
* screen-constant rotate-knob stalk), or null when the page isn't shown. */
|
|
153
|
+
viewEnv(pon: number): {
|
|
154
|
+
scale: number;
|
|
155
|
+
rotation: 0 | 90 | 180 | 270;
|
|
156
|
+
zoom: number;
|
|
157
|
+
} | null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* THE anchored projection: anchor → screen position, in one pure call —
|
|
161
|
+
* projection, avoid-point transform, and the shared placement policy.
|
|
162
|
+
* Frameworks recompute this inside their native reactive primitive
|
|
163
|
+
* (a React render, a Vue `computed`, an Angular signal, a Svelte
|
|
164
|
+
* `$derived`); nothing here schedules anything.
|
|
165
|
+
*/
|
|
166
|
+
declare function projectAnchoredTarget(projector: ViewProjector, anchor: AnchorTarget, placement: AnchoredPlacement, gap: number): AnchoredPosition | null;
|
|
167
|
+
/**
|
|
168
|
+
* The one genuinely BROWSER-driven invalidation: an element in normal
|
|
169
|
+
* document flow moves when the document scrolls or the window resizes —
|
|
170
|
+
* no state change announces it. `<PageView>`'s binding registers this;
|
|
171
|
+
* the Stage never does (its camera is state, delivered through render).
|
|
172
|
+
*/
|
|
173
|
+
declare function observeClientGeometry(callback: () => void): () => void;
|
|
174
|
+
declare function positionAnchoredRect(box: AnchoredRect, placement: AnchoredPlacement, gap: number, avoid?: AnchoredPoint | null): AnchoredPosition;
|
|
175
|
+
//#endregion
|
|
19
176
|
//#region src/cursor.d.ts
|
|
20
177
|
/**
|
|
21
178
|
* SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the
|
|
@@ -59,5 +216,28 @@ declare function svgCursor({ svg, hotspot, fallback }: SvgCursorOptions): string
|
|
|
59
216
|
*/
|
|
60
217
|
declare function sanitizeExternalUri(uri: string, allowedSchemes?: Iterable<string>): string | null;
|
|
61
218
|
//#endregion
|
|
62
|
-
|
|
219
|
+
//#region src/painted-image.d.ts
|
|
220
|
+
/** A browser image source whose object URL has an explicit lifetime. */
|
|
221
|
+
interface ObjectUrlImageSource {
|
|
222
|
+
objectUrl(signal?: AbortSignal): Promise<{
|
|
223
|
+
url: string;
|
|
224
|
+
revoke(): void;
|
|
225
|
+
}>;
|
|
226
|
+
}
|
|
227
|
+
interface PaintedImageCallbacks {
|
|
228
|
+
onPainted(): void;
|
|
229
|
+
onUnpainted(): void;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Bind a raster source to an <img> without ever painting its incomplete-image
|
|
233
|
+
* placeholder. The source becomes visible on load; retained coverage is
|
|
234
|
+
* released only after the browser has had one full frame to present it.
|
|
235
|
+
*
|
|
236
|
+
* Call this from the framework's pre-paint mount hook and invoke the returned
|
|
237
|
+
* cleanup on unmount. Keeping this DOM lifecycle here lets every framework
|
|
238
|
+
* adapter remain a ref plus one bind/unbind hook.
|
|
239
|
+
*/
|
|
240
|
+
declare function bindPaintedImage(image: HTMLImageElement, source: ObjectUrlImageSource, callbacks: PaintedImageCallbacks): () => void;
|
|
241
|
+
//#endregion
|
|
242
|
+
export { type AnchorTarget, type AnchoredPlacement, type AnchoredPoint, type AnchoredPosition, type AnchoredRect, type ClipboardSelectionSource, type ObjectUrlImageSource, type PaintedImageCallbacks, type PickFileOptions, type SelectionClipboardOptions, type SvgCursorOptions, type ViewProjector, bindPaintedImage, copySelection, observeClientGeometry, pickFile, pickImageFile, positionAnchoredRect, projectAnchoredTarget, sanitizeExternalUri, saveFile, svgCursor, wireSelectionClipboard };
|
|
63
243
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,163 @@ declare const pickFile: (options?: PickFileOptions) => Promise<File | null>;
|
|
|
16
16
|
*/
|
|
17
17
|
declare function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void;
|
|
18
18
|
//#endregion
|
|
19
|
+
//#region src/clipboard.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Clipboard wiring for text selection — the environmental half of copy.
|
|
22
|
+
*
|
|
23
|
+
* The selection plugin is deliberately DOM-free: its public capability ends
|
|
24
|
+
* at `readText()` (data). Everything that touches `navigator.clipboard` or
|
|
25
|
+
* the native `copy` event lives HERE, structurally typed so this module
|
|
26
|
+
* stays a pure DOM utility with no EmbedPDF dependencies (the selection
|
|
27
|
+
* capability satisfies {@link ClipboardSelectionSource} as-is).
|
|
28
|
+
*/
|
|
29
|
+
/** The slice of the selection capability clipboard wiring needs. */
|
|
30
|
+
interface ClipboardSelectionSource {
|
|
31
|
+
hasSelection(): boolean;
|
|
32
|
+
canCopy(): boolean;
|
|
33
|
+
readText(): Promise<string>;
|
|
34
|
+
onChange(cb: () => void): () => void;
|
|
35
|
+
onCommit(cb: () => void): () => void;
|
|
36
|
+
}
|
|
37
|
+
interface SelectionClipboardOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Prefetch the selected text when the selection settles (default true).
|
|
40
|
+
* This is what makes the NATIVE copy path work: a `copy` event handler
|
|
41
|
+
* must call `clipboardData.setData` synchronously — awaiting a page-text
|
|
42
|
+
* read inside it is too late — and it also keeps the async path instant.
|
|
43
|
+
* The fetch is one versioned, cached, permission-gated read per page;
|
|
44
|
+
* pass false to make copying strictly on-demand.
|
|
45
|
+
*/
|
|
46
|
+
prefetch?: boolean;
|
|
47
|
+
/** Event target to listen on. Defaults to `document`. */
|
|
48
|
+
target?: Document;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wire clipboard copy for a selection. Two delivery paths, both fed by the
|
|
52
|
+
* prefetched text:
|
|
53
|
+
*
|
|
54
|
+
* - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page
|
|
55
|
+
* has a DOM selection or focused editable — e.g. the viewer shell's
|
|
56
|
+
* focus sink): answered SYNCHRONOUSLY from the cache;
|
|
57
|
+
* - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually
|
|
58
|
+
* have none, and browsers don't dispatch `copy` then): a keydown
|
|
59
|
+
* fallback writes via the async Clipboard API inside the keystroke's
|
|
60
|
+
* user-activation window.
|
|
61
|
+
*
|
|
62
|
+
* Returns the unwire function. Mount once per document view (not per page).
|
|
63
|
+
*/
|
|
64
|
+
declare function wireSelectionClipboard(selection: ClipboardSelectionSource, options?: SelectionClipboardOptions): () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Toolbar/menu path: read the selected text and write it to the clipboard
|
|
67
|
+
* via the async Clipboard API. Call from a user-gesture handler (browsers
|
|
68
|
+
* require a secure context + user activation for clipboard writes). Resolves
|
|
69
|
+
* with what was copied ('' when nothing was — nothing is written then, so an
|
|
70
|
+
* empty selection never clobbers the user's clipboard).
|
|
71
|
+
*/
|
|
72
|
+
declare function copySelection(selection: ClipboardSelectionSource): Promise<string>;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/anchored-position.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* Anchored-overlay geometry for UI floating over page content (selection
|
|
77
|
+
* menus, draft menus, popovers): the projector SNAPSHOT contract, the
|
|
78
|
+
* anchor shape, and the placement math. Pure values and pure functions over
|
|
79
|
+
* structural rect/point shapes — no EmbedPDF types, no framework, no
|
|
80
|
+
* reactive lifecycle — so every framework adapter (React, Vue, Svelte,
|
|
81
|
+
* Angular) shares ONE implementation and binds it with its NATIVE
|
|
82
|
+
* reactivity (context revision, signals, computed, $derived).
|
|
83
|
+
*
|
|
84
|
+
* The boundary laws:
|
|
85
|
+
* - Placement policy lives HERE, above the projector seam: a projector
|
|
86
|
+
* only knows geometry — never what is being positioned nor where it
|
|
87
|
+
* prefers to sit.
|
|
88
|
+
* - A projector describes the CURRENT projection; it never implements a
|
|
89
|
+
* framework-style reactive lifecycle. State-driven changes (the Stage
|
|
90
|
+
* camera) must reach consumers through the framework's own render
|
|
91
|
+
* cycle so surface and overlay commit TOGETHER; only genuinely
|
|
92
|
+
* browser-driven changes (a PageView moving because the document
|
|
93
|
+
* scrolled) use an external listener ({@link observeClientGeometry}).
|
|
94
|
+
*/
|
|
95
|
+
interface AnchoredRect {
|
|
96
|
+
x: number;
|
|
97
|
+
y: number;
|
|
98
|
+
width: number;
|
|
99
|
+
height: number;
|
|
100
|
+
}
|
|
101
|
+
interface AnchoredPoint {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
}
|
|
105
|
+
type AnchoredPlacement = 'top' | 'right' | 'bottom' | 'left';
|
|
106
|
+
/** CSS-ready output: absolute/fixed `left`/`top` plus a centering transform. */
|
|
107
|
+
interface AnchoredPosition {
|
|
108
|
+
left: number;
|
|
109
|
+
top: number;
|
|
110
|
+
transform: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Place an upright element around `box` (screen px). `avoid` is a screen
|
|
114
|
+
* point the element must clear (e.g. the rotate knob): the element extends
|
|
115
|
+
* ONLY the edge it sits on, and ONLY when the point protrudes past that
|
|
116
|
+
* edge — so it clears the obstacle without ever shifting off-centre on the
|
|
117
|
+
* other axis. When the point is on another side (e.g. a 90° shape, knob at
|
|
118
|
+
* mid-height for a `top` placement) the edge is untouched and the element
|
|
119
|
+
* stays centred on `box`.
|
|
120
|
+
*/
|
|
121
|
+
/** What anchored UI attaches to: a content-space rect on a page, plus
|
|
122
|
+
* optional content-space points the UI must clear (e.g. a rotate knob).
|
|
123
|
+
* Structural — plugin anchor reads satisfy it without importing this
|
|
124
|
+
* package. */
|
|
125
|
+
interface AnchorTarget {
|
|
126
|
+
pon: number;
|
|
127
|
+
bounds: AnchoredRect;
|
|
128
|
+
avoid?: AnchoredPoint[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A page surface's PROJECTION SNAPSHOT: how a content-space rect on a page
|
|
132
|
+
* becomes screen coordinates, right now. Provided by `<Stage>`
|
|
133
|
+
* (camera-driven, pure state, no DOM reads) and `<PageView>` (DOM-measured).
|
|
134
|
+
* Deliberately NO subscribe here — when projection changes is a framework
|
|
135
|
+
* binding concern (see the module doc), not part of the snapshot.
|
|
136
|
+
*/
|
|
137
|
+
interface ViewProjector {
|
|
138
|
+
/**
|
|
139
|
+
* Which coordinate space `toScreen` speaks, and therefore how anchored
|
|
140
|
+
* content mounts:
|
|
141
|
+
* - 'overlay': coords are relative to the surface's own positioned
|
|
142
|
+
* container → rendered in place, position:absolute. (Stage)
|
|
143
|
+
* - 'client': coords are viewport px → portalled to the document body,
|
|
144
|
+
* position:fixed, so no ancestor overflow can clip. (PageView)
|
|
145
|
+
*/
|
|
146
|
+
space: 'overlay' | 'client';
|
|
147
|
+
/** Content-space rect on a page → coords in `space`. Null: not projectable
|
|
148
|
+
* right now (page not shown / not measurable yet). */
|
|
149
|
+
toScreen(pon: number, rect: AnchoredRect): AnchoredRect | null;
|
|
150
|
+
toScreenPoint(pon: number, at: AnchoredPoint): AnchoredPoint | null;
|
|
151
|
+
/** The page's live view facts (for anchor reads that need them — e.g. the
|
|
152
|
+
* screen-constant rotate-knob stalk), or null when the page isn't shown. */
|
|
153
|
+
viewEnv(pon: number): {
|
|
154
|
+
scale: number;
|
|
155
|
+
rotation: 0 | 90 | 180 | 270;
|
|
156
|
+
zoom: number;
|
|
157
|
+
} | null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* THE anchored projection: anchor → screen position, in one pure call —
|
|
161
|
+
* projection, avoid-point transform, and the shared placement policy.
|
|
162
|
+
* Frameworks recompute this inside their native reactive primitive
|
|
163
|
+
* (a React render, a Vue `computed`, an Angular signal, a Svelte
|
|
164
|
+
* `$derived`); nothing here schedules anything.
|
|
165
|
+
*/
|
|
166
|
+
declare function projectAnchoredTarget(projector: ViewProjector, anchor: AnchorTarget, placement: AnchoredPlacement, gap: number): AnchoredPosition | null;
|
|
167
|
+
/**
|
|
168
|
+
* The one genuinely BROWSER-driven invalidation: an element in normal
|
|
169
|
+
* document flow moves when the document scrolls or the window resizes —
|
|
170
|
+
* no state change announces it. `<PageView>`'s binding registers this;
|
|
171
|
+
* the Stage never does (its camera is state, delivered through render).
|
|
172
|
+
*/
|
|
173
|
+
declare function observeClientGeometry(callback: () => void): () => void;
|
|
174
|
+
declare function positionAnchoredRect(box: AnchoredRect, placement: AnchoredPlacement, gap: number, avoid?: AnchoredPoint | null): AnchoredPosition;
|
|
175
|
+
//#endregion
|
|
19
176
|
//#region src/cursor.d.ts
|
|
20
177
|
/**
|
|
21
178
|
* SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the
|
|
@@ -59,5 +216,28 @@ declare function svgCursor({ svg, hotspot, fallback }: SvgCursorOptions): string
|
|
|
59
216
|
*/
|
|
60
217
|
declare function sanitizeExternalUri(uri: string, allowedSchemes?: Iterable<string>): string | null;
|
|
61
218
|
//#endregion
|
|
62
|
-
|
|
219
|
+
//#region src/painted-image.d.ts
|
|
220
|
+
/** A browser image source whose object URL has an explicit lifetime. */
|
|
221
|
+
interface ObjectUrlImageSource {
|
|
222
|
+
objectUrl(signal?: AbortSignal): Promise<{
|
|
223
|
+
url: string;
|
|
224
|
+
revoke(): void;
|
|
225
|
+
}>;
|
|
226
|
+
}
|
|
227
|
+
interface PaintedImageCallbacks {
|
|
228
|
+
onPainted(): void;
|
|
229
|
+
onUnpainted(): void;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Bind a raster source to an <img> without ever painting its incomplete-image
|
|
233
|
+
* placeholder. The source becomes visible on load; retained coverage is
|
|
234
|
+
* released only after the browser has had one full frame to present it.
|
|
235
|
+
*
|
|
236
|
+
* Call this from the framework's pre-paint mount hook and invoke the returned
|
|
237
|
+
* cleanup on unmount. Keeping this DOM lifecycle here lets every framework
|
|
238
|
+
* adapter remain a ref plus one bind/unbind hook.
|
|
239
|
+
*/
|
|
240
|
+
declare function bindPaintedImage(image: HTMLImageElement, source: ObjectUrlImageSource, callbacks: PaintedImageCallbacks): () => void;
|
|
241
|
+
//#endregion
|
|
242
|
+
export { type AnchorTarget, type AnchoredPlacement, type AnchoredPoint, type AnchoredPosition, type AnchoredRect, type ClipboardSelectionSource, type ObjectUrlImageSource, type PaintedImageCallbacks, type PickFileOptions, type SelectionClipboardOptions, type SvgCursorOptions, type ViewProjector, bindPaintedImage, copySelection, observeClientGeometry, pickFile, pickImageFile, positionAnchoredRect, projectAnchoredTarget, sanitizeExternalUri, saveFile, svgCursor, wireSelectionClipboard };
|
|
63
243
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,136 @@ function saveFile(bytes, name, mimeType) {
|
|
|
59
59
|
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
60
60
|
}
|
|
61
61
|
//#endregion
|
|
62
|
+
//#region src/clipboard.ts
|
|
63
|
+
const PREFETCH_DEBOUNCE_MS = 150;
|
|
64
|
+
/**
|
|
65
|
+
* Wire clipboard copy for a selection. Two delivery paths, both fed by the
|
|
66
|
+
* prefetched text:
|
|
67
|
+
*
|
|
68
|
+
* - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page
|
|
69
|
+
* has a DOM selection or focused editable — e.g. the viewer shell's
|
|
70
|
+
* focus sink): answered SYNCHRONOUSLY from the cache;
|
|
71
|
+
* - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually
|
|
72
|
+
* have none, and browsers don't dispatch `copy` then): a keydown
|
|
73
|
+
* fallback writes via the async Clipboard API inside the keystroke's
|
|
74
|
+
* user-activation window.
|
|
75
|
+
*
|
|
76
|
+
* Returns the unwire function. Mount once per document view (not per page).
|
|
77
|
+
*/
|
|
78
|
+
function wireSelectionClipboard(selection, options = {}) {
|
|
79
|
+
const target = options.target ?? document;
|
|
80
|
+
const prefetch = options.prefetch ?? true;
|
|
81
|
+
let cached = null;
|
|
82
|
+
let generation = 0;
|
|
83
|
+
let timer = null;
|
|
84
|
+
const refresh = () => {
|
|
85
|
+
cached = null;
|
|
86
|
+
if (!prefetch || !selection.hasSelection() || !selection.canCopy()) return;
|
|
87
|
+
const gen = ++generation;
|
|
88
|
+
selection.readText().then((text) => {
|
|
89
|
+
if (gen === generation) cached = text;
|
|
90
|
+
}, () => {});
|
|
91
|
+
};
|
|
92
|
+
const offCommit = selection.onCommit(() => {
|
|
93
|
+
if (timer) clearTimeout(timer);
|
|
94
|
+
timer = null;
|
|
95
|
+
refresh();
|
|
96
|
+
});
|
|
97
|
+
const offChange = selection.onChange(() => {
|
|
98
|
+
generation++;
|
|
99
|
+
cached = null;
|
|
100
|
+
if (timer) clearTimeout(timer);
|
|
101
|
+
timer = setTimeout(refresh, PREFETCH_DEBOUNCE_MS);
|
|
102
|
+
});
|
|
103
|
+
const onCopy = (e) => {
|
|
104
|
+
if (cached == null || cached === "" || !selection.hasSelection()) return;
|
|
105
|
+
e.clipboardData?.setData("text/plain", cached);
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
};
|
|
108
|
+
const onKeyDown = (e) => {
|
|
109
|
+
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "c" || e.shiftKey || e.altKey) return;
|
|
110
|
+
if (!selection.hasSelection() || !selection.canCopy()) return;
|
|
111
|
+
const dom = target.getSelection?.();
|
|
112
|
+
if (dom && !dom.isCollapsed) return;
|
|
113
|
+
copySelection(selection);
|
|
114
|
+
};
|
|
115
|
+
target.addEventListener("copy", onCopy);
|
|
116
|
+
target.addEventListener("keydown", onKeyDown);
|
|
117
|
+
return () => {
|
|
118
|
+
offCommit();
|
|
119
|
+
offChange();
|
|
120
|
+
if (timer) clearTimeout(timer);
|
|
121
|
+
target.removeEventListener("copy", onCopy);
|
|
122
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Toolbar/menu path: read the selected text and write it to the clipboard
|
|
127
|
+
* via the async Clipboard API. Call from a user-gesture handler (browsers
|
|
128
|
+
* require a secure context + user activation for clipboard writes). Resolves
|
|
129
|
+
* with what was copied ('' when nothing was — nothing is written then, so an
|
|
130
|
+
* empty selection never clobbers the user's clipboard).
|
|
131
|
+
*/
|
|
132
|
+
async function copySelection(selection) {
|
|
133
|
+
const text = await selection.readText();
|
|
134
|
+
if (text !== "") await navigator.clipboard.writeText(text);
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/anchored-position.ts
|
|
139
|
+
/**
|
|
140
|
+
* THE anchored projection: anchor → screen position, in one pure call —
|
|
141
|
+
* projection, avoid-point transform, and the shared placement policy.
|
|
142
|
+
* Frameworks recompute this inside their native reactive primitive
|
|
143
|
+
* (a React render, a Vue `computed`, an Angular signal, a Svelte
|
|
144
|
+
* `$derived`); nothing here schedules anything.
|
|
145
|
+
*/
|
|
146
|
+
function projectAnchoredTarget(projector, anchor, placement, gap) {
|
|
147
|
+
const box = projector.toScreen(anchor.pon, anchor.bounds);
|
|
148
|
+
if (!box) return null;
|
|
149
|
+
return positionAnchoredRect(box, placement, gap, anchor.avoid?.length ? projector.toScreenPoint(anchor.pon, anchor.avoid[0]) : null);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The one genuinely BROWSER-driven invalidation: an element in normal
|
|
153
|
+
* document flow moves when the document scrolls or the window resizes —
|
|
154
|
+
* no state change announces it. `<PageView>`'s binding registers this;
|
|
155
|
+
* the Stage never does (its camera is state, delivered through render).
|
|
156
|
+
*/
|
|
157
|
+
function observeClientGeometry(callback) {
|
|
158
|
+
window.addEventListener("scroll", callback, true);
|
|
159
|
+
window.addEventListener("resize", callback);
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener("scroll", callback, true);
|
|
162
|
+
window.removeEventListener("resize", callback);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function positionAnchoredRect(box, placement, gap, avoid) {
|
|
166
|
+
const cx = box.x + box.width / 2;
|
|
167
|
+
const cy = box.y + box.height / 2;
|
|
168
|
+
switch (placement) {
|
|
169
|
+
case "bottom": return {
|
|
170
|
+
left: cx,
|
|
171
|
+
top: Math.max(box.y + box.height, avoid ? avoid.y : -Infinity) + gap,
|
|
172
|
+
transform: "translate(-50%, 0)"
|
|
173
|
+
};
|
|
174
|
+
case "left": return {
|
|
175
|
+
left: Math.min(box.x, avoid ? avoid.x : Infinity) - gap,
|
|
176
|
+
top: cy,
|
|
177
|
+
transform: "translate(-100%, -50%)"
|
|
178
|
+
};
|
|
179
|
+
case "right": return {
|
|
180
|
+
left: Math.max(box.x + box.width, avoid ? avoid.x : -Infinity) + gap,
|
|
181
|
+
top: cy,
|
|
182
|
+
transform: "translate(0, -50%)"
|
|
183
|
+
};
|
|
184
|
+
default: return {
|
|
185
|
+
left: cx,
|
|
186
|
+
top: Math.min(box.y, avoid ? avoid.y : Infinity) - gap,
|
|
187
|
+
transform: "translate(-50%, -100%)"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
62
192
|
//#region src/cursor.ts
|
|
63
193
|
/** Build the CSS cursor value: `url("data:image/svg+xml,…") x y, fallback`. */
|
|
64
194
|
function svgCursor({ svg, hotspot, fallback = "crosshair" }) {
|
|
@@ -104,6 +234,52 @@ function sanitizeExternalUri(uri, allowedSchemes = DEFAULT_ALLOWED_SCHEMES) {
|
|
|
104
234
|
}
|
|
105
235
|
}
|
|
106
236
|
//#endregion
|
|
107
|
-
|
|
237
|
+
//#region src/painted-image.ts
|
|
238
|
+
/**
|
|
239
|
+
* Bind a raster source to an <img> without ever painting its incomplete-image
|
|
240
|
+
* placeholder. The source becomes visible on load; retained coverage is
|
|
241
|
+
* released only after the browser has had one full frame to present it.
|
|
242
|
+
*
|
|
243
|
+
* Call this from the framework's pre-paint mount hook and invoke the returned
|
|
244
|
+
* cleanup on unmount. Keeping this DOM lifecycle here lets every framework
|
|
245
|
+
* adapter remain a ref plus one bind/unbind hook.
|
|
246
|
+
*/
|
|
247
|
+
function bindPaintedImage(image, source, callbacks) {
|
|
248
|
+
const controller = new AbortController();
|
|
249
|
+
let revoke;
|
|
250
|
+
let frame = 0;
|
|
251
|
+
let painted = false;
|
|
252
|
+
image.style.visibility = "hidden";
|
|
253
|
+
const onLoad = () => {
|
|
254
|
+
image.style.visibility = "visible";
|
|
255
|
+
frame = requestAnimationFrame(() => {
|
|
256
|
+
frame = requestAnimationFrame(() => {
|
|
257
|
+
frame = 0;
|
|
258
|
+
painted = true;
|
|
259
|
+
callbacks.onPainted();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
image.addEventListener("load", onLoad, { once: true });
|
|
264
|
+
source.objectUrl(controller.signal).then((objectUrl) => {
|
|
265
|
+
if (controller.signal.aborted) {
|
|
266
|
+
objectUrl.revoke();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
revoke = objectUrl.revoke;
|
|
270
|
+
image.src = objectUrl.url;
|
|
271
|
+
}, () => {});
|
|
272
|
+
return () => {
|
|
273
|
+
controller.abort();
|
|
274
|
+
if (frame) cancelAnimationFrame(frame);
|
|
275
|
+
image.removeEventListener("load", onLoad);
|
|
276
|
+
image.style.visibility = "hidden";
|
|
277
|
+
image.removeAttribute("src");
|
|
278
|
+
revoke?.();
|
|
279
|
+
if (painted) callbacks.onUnpainted();
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { bindPaintedImage, copySelection, observeClientGeometry, pickFile, pickImageFile, positionAnchoredRect, projectAnchoredTarget, sanitizeExternalUri, saveFile, svgCursor, wireSelectionClipboard };
|
|
108
284
|
|
|
109
285
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/file-picker.ts","../src/cursor.ts","../src/external-uri.ts"],"sourcesContent":["/**\n * A one-shot file dialog. Opens a throwaway `<input type=file>`, resolves the\n * chosen `File`, or `null` when the dialog is dismissed. MUST be called\n * synchronously from a user gesture (a click) or the browser refuses to open it.\n *\n * A `File` is a `Blob`, which is a `BinarySource` everywhere in the stack, so the\n * caller can hand the result straight to the engine — this module stays a pure\n * DOM utility with no EmbedPDF types.\n */\nconst IMAGE_ACCEPT = 'image/png,image/jpeg,application/pdf';\n\nexport interface PickFileOptions {\n /** The dialog's `accept` filter. Defaults to PNG / JPEG / PDF. */\n accept?: string;\n /** Allow multiple selection (returns the first file). Defaults to false. */\n multiple?: boolean;\n}\n\n/** Open the file dialog; resolve the picked file, or null if cancelled. */\nexport function pickImageFile(options: PickFileOptions = {}): Promise<File | null> {\n const { accept = IMAGE_ACCEPT, multiple = false } = options;\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.multiple = multiple;\n input.style.display = 'none';\n\n let settled = false;\n const finish = (value: File | null): void => {\n if (settled) return;\n settled = true;\n window.removeEventListener('focus', onFocus);\n input.remove();\n resolve(value);\n };\n\n // A window refocus means the dialog closed. Give a queued `change` a tick to\n // win, then treat a bare refocus as a cancel — the fallback for browsers that\n // predate the `cancel` event.\n const onFocus = (): void => {\n setTimeout(() => finish(null), 300);\n };\n\n input.addEventListener('change', () => finish(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => finish(null), { once: true });\n window.addEventListener('focus', onFocus, { once: true });\n\n document.body.appendChild(input);\n input.click();\n });\n}\n\n/** The same dialog with NO type filter — any file (attachments). */\nexport const pickFile = (options: PickFileOptions = {}): Promise<File | null> =>\n pickImageFile({ accept: '*/*', ...options });\n\n/**\n * Hand bytes to the user as a browser download (a one-shot `<a download>`).\n * The DOM half of \"download this attachment\" — pair with the annotation\n * capability's `downloadAttachment` or the document `attachments.download`.\n */\nexport function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void {\n const blob =\n bytes instanceof Blob\n ? bytes\n : new Blob([bytes as BlobPart], { type: mimeType ?? 'application/octet-stream' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = name;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n","/**\n * SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the\n * only zero-latency pointer-locked pixel the web has — the OS composites it),\n * so an app hands its toolbar icon here and assigns the result via the\n * interaction hub's `setToolCursor`.\n *\n * Browser rules baked into this module:\n * - The SVG must carry explicit `width`/`height` attributes (not just a\n * viewBox) or Chromium/WebKit ignore it — enforced here.\n * - The hard ceiling is 128×128 CSS px (bigger falls back to the keyword).\n * Above 32×32, Chromium temporarily resets the cursor to default while it\n * would overlap browser UI (anti-spoofing) — fine mid-viewport, so ~40px\n * compositions are common; stay ≤128.\n * - The keyword fallback covers browsers that refuse SVG cursor images.\n *\n * Pure string work with no EmbedPDF types — like the file picker, this module\n * is a plain web utility.\n */\n\nexport interface SvgCursorOptions {\n /** Complete `<svg>` markup, colors baked in (no `currentColor` — a cursor\n * image has no cascade to inherit from). */\n svg: string;\n /** The click point inside the image, CSS px from the top-left. Default 0,0\n * (arrow-style: the image's corner is the tip). */\n hotspot?: { x: number; y: number };\n /** Keyword cursor when the image can't render. Default `'crosshair'`. */\n fallback?: string;\n}\n\n/** Build the CSS cursor value: `url(\"data:image/svg+xml,…\") x y, fallback`. */\nexport function svgCursor({ svg, hotspot, fallback = 'crosshair' }: SvgCursorOptions): string {\n // Scan only the opening `<svg …>` tag (bounded slice) rather than running\n // `/<svg[^>]*\\swidth\\s*=/` over the whole string — the unbounded form is\n // quadratic on pathological inputs (many `<svg` fragments with no `>`),\n // while accepting exactly the same markup: `[^>]*` could never cross a\n // `>` either.\n const tagStart = svg.indexOf('<svg');\n const tagEnd = tagStart >= 0 ? svg.indexOf('>', tagStart) : -1;\n const openTag = tagEnd >= 0 ? svg.slice(tagStart, tagEnd + 1) : '';\n if (!/\\swidth\\s*=/.test(openTag) || !/\\sheight\\s*=/.test(openTag)) {\n throw new Error(\n '[web] svgCursor: the <svg> needs explicit width/height attributes — ' +\n 'browsers ignore cursor images without an intrinsic size',\n );\n }\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n const spot = hotspot ? ` ${Math.round(hotspot.x)} ${Math.round(hotspot.y)}` : '';\n return `url(\"${uri}\")${spot}, ${fallback}`;\n}\n","/**\n * External-URI hygiene for PDF link annotations — written ONCE here so every\n * framework adapter (react, vue, svelte, angular) shares the same policy.\n *\n * PDFs are untrusted input: a link's `/URI` can carry anything, including\n * `javascript:` payloads. The default allowlist covers what a document link\n * legitimately opens; everything else (javascript:, file:, data:, vbscript:,\n * custom app schemes) resolves to `null` and the adapters render the link\n * WITHOUT an href — activation still reports through the plugin, so an\n * embedder that needs a custom scheme can handle it in `onActivate`.\n */\nconst DEFAULT_ALLOWED_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * Validate a link URI for opening in the browser. Returns the URI when its\n * scheme is allowed (scheme-relative and bare URIs resolve as https), else\n * `null`. Never throws — a malformed URI is just an unopenable one.\n */\nexport function sanitizeExternalUri(\n uri: string,\n allowedSchemes: Iterable<string> = DEFAULT_ALLOWED_SCHEMES,\n): string | null {\n const trimmed = uri.trim();\n if (!trimmed) return null;\n const allowed = new Set(\n [...allowedSchemes].map((s) => (s.endsWith(':') ? s.toLowerCase() : `${s.toLowerCase()}:`)),\n );\n try {\n // Base resolves scheme-relative (\"//host\") and bare (\"example.com/x\" is a\n // relative path → https) forms; an absolute scheme wins over the base.\n const url = new URL(trimmed, 'https://invalid.example/');\n return allowed.has(url.protocol) ? trimmed : null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,eAAe;;AAUrB,SAAgB,cAAc,UAA2B,CAAC,GAAyB;CACjF,MAAM,EAAE,SAAS,cAAc,WAAW,UAAU;CACpD,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,SAAS,cAAc,OAAO;EAC5C,MAAM,OAAO;EACb,MAAM,SAAS;EACf,MAAM,WAAW;EACjB,MAAM,MAAM,UAAU;EAEtB,IAAI,UAAU;EACd,MAAM,UAAU,UAA6B;GAC3C,IAAI,SAAS;GACb,UAAU;GACV,OAAO,oBAAoB,SAAS,OAAO;GAC3C,MAAM,OAAO;GACb,QAAQ,KAAK;EACf;EAKA,MAAM,gBAAsB;GAC1B,iBAAiB,OAAO,IAAI,GAAG,GAAG;EACpC;EAEA,MAAM,iBAAiB,gBAAgB,OAAO,MAAM,QAAQ,MAAM,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACvF,MAAM,iBAAiB,gBAAgB,OAAO,IAAI,GAAG,EAAE,MAAM,KAAK,CAAC;EACnE,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAExD,SAAS,KAAK,YAAY,KAAK;EAC/B,MAAM,MAAM;CACd,CAAC;AACH;;AAGA,MAAa,YAAY,UAA2B,CAAC,MACnD,cAAc;CAAE,QAAQ;CAAO,GAAG;AAAQ,CAAC;;;;;;AAO7C,SAAgB,SAAS,OAA0B,MAAc,UAAyB;CACxF,MAAM,OACJ,iBAAiB,OACb,QACA,IAAI,KAAK,CAAC,KAAiB,GAAG,EAAE,MAAM,YAAY,2BAA2B,CAAC;CACpF,MAAM,MAAM,IAAI,gBAAgB,IAAI;CACpC,MAAM,IAAI,SAAS,cAAc,GAAG;CACpC,EAAE,OAAO;CACT,EAAE,WAAW;CACb,EAAE,MAAM,UAAU;CAClB,SAAS,KAAK,YAAY,CAAC;CAC3B,EAAE,MAAM;CACR,EAAE,OAAO;CACT,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,GAAI;AACjD;;;;AC7CA,SAAgB,UAAU,EAAE,KAAK,SAAS,WAAW,eAAyC;CAM5F,MAAM,WAAW,IAAI,QAAQ,MAAM;CACnC,MAAM,SAAS,YAAY,IAAI,IAAI,QAAQ,KAAK,QAAQ,IAAI;CAC5D,MAAM,UAAU,UAAU,IAAI,IAAI,MAAM,UAAU,SAAS,CAAC,IAAI;CAChE,IAAI,CAAC,cAAc,KAAK,OAAO,KAAK,CAAC,eAAe,KAAK,OAAO,GAC9D,MAAM,IAAI,MACR,6HAEF;CAIF,OAAO,QAAQ,sBAFmB,mBAAmB,GAAG,IAErC,IADN,UAAU,IAAI,KAAK,MAAM,QAAQ,CAAC,EAAE,GAAG,KAAK,MAAM,QAAQ,CAAC,MAAM,GAClD,IAAI;AAClC;;;;;;;;;;;;;;ACtCA,MAAM,0CAA0B,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;AAO9E,SAAgB,oBACd,KACA,iBAAmC,yBACpB;CACf,MAAM,UAAU,IAAI,KAAK;CACzB,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,UAAU,IAAI,IAClB,CAAC,GAAG,cAAc,CAAC,CAAC,KAAK,MAAO,EAAE,SAAS,GAAG,IAAI,EAAE,YAAY,IAAI,GAAG,EAAE,YAAY,EAAE,EAAG,CAC5F;CACA,IAAI;EAGF,MAAM,MAAM,IAAI,IAAI,SAAS,0BAA0B;EACvD,OAAO,QAAQ,IAAI,IAAI,QAAQ,IAAI,UAAU;CAC/C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/file-picker.ts","../src/clipboard.ts","../src/anchored-position.ts","../src/cursor.ts","../src/external-uri.ts","../src/painted-image.ts"],"sourcesContent":["/**\n * A one-shot file dialog. Opens a throwaway `<input type=file>`, resolves the\n * chosen `File`, or `null` when the dialog is dismissed. MUST be called\n * synchronously from a user gesture (a click) or the browser refuses to open it.\n *\n * A `File` is a `Blob`, which is a `BinarySource` everywhere in the stack, so the\n * caller can hand the result straight to the engine — this module stays a pure\n * DOM utility with no EmbedPDF types.\n */\nconst IMAGE_ACCEPT = 'image/png,image/jpeg,application/pdf';\n\nexport interface PickFileOptions {\n /** The dialog's `accept` filter. Defaults to PNG / JPEG / PDF. */\n accept?: string;\n /** Allow multiple selection (returns the first file). Defaults to false. */\n multiple?: boolean;\n}\n\n/** Open the file dialog; resolve the picked file, or null if cancelled. */\nexport function pickImageFile(options: PickFileOptions = {}): Promise<File | null> {\n const { accept = IMAGE_ACCEPT, multiple = false } = options;\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.multiple = multiple;\n input.style.display = 'none';\n\n let settled = false;\n const finish = (value: File | null): void => {\n if (settled) return;\n settled = true;\n window.removeEventListener('focus', onFocus);\n input.remove();\n resolve(value);\n };\n\n // A window refocus means the dialog closed. Give a queued `change` a tick to\n // win, then treat a bare refocus as a cancel — the fallback for browsers that\n // predate the `cancel` event.\n const onFocus = (): void => {\n setTimeout(() => finish(null), 300);\n };\n\n input.addEventListener('change', () => finish(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => finish(null), { once: true });\n window.addEventListener('focus', onFocus, { once: true });\n\n document.body.appendChild(input);\n input.click();\n });\n}\n\n/** The same dialog with NO type filter — any file (attachments). */\nexport const pickFile = (options: PickFileOptions = {}): Promise<File | null> =>\n pickImageFile({ accept: '*/*', ...options });\n\n/**\n * Hand bytes to the user as a browser download (a one-shot `<a download>`).\n * The DOM half of \"download this attachment\" — pair with the annotation\n * capability's `downloadAttachment` or the document `attachments.download`.\n */\nexport function saveFile(bytes: Uint8Array | Blob, name: string, mimeType?: string): void {\n const blob =\n bytes instanceof Blob\n ? bytes\n : new Blob([bytes as BlobPart], { type: mimeType ?? 'application/octet-stream' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = name;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n","/**\n * Clipboard wiring for text selection — the environmental half of copy.\n *\n * The selection plugin is deliberately DOM-free: its public capability ends\n * at `readText()` (data). Everything that touches `navigator.clipboard` or\n * the native `copy` event lives HERE, structurally typed so this module\n * stays a pure DOM utility with no EmbedPDF dependencies (the selection\n * capability satisfies {@link ClipboardSelectionSource} as-is).\n */\n\n/** The slice of the selection capability clipboard wiring needs. */\nexport interface ClipboardSelectionSource {\n hasSelection(): boolean;\n canCopy(): boolean;\n readText(): Promise<string>;\n onChange(cb: () => void): () => void;\n onCommit(cb: () => void): () => void;\n}\n\nexport interface SelectionClipboardOptions {\n /**\n * Prefetch the selected text when the selection settles (default true).\n * This is what makes the NATIVE copy path work: a `copy` event handler\n * must call `clipboardData.setData` synchronously — awaiting a page-text\n * read inside it is too late — and it also keeps the async path instant.\n * The fetch is one versioned, cached, permission-gated read per page;\n * pass false to make copying strictly on-demand.\n */\n prefetch?: boolean;\n /** Event target to listen on. Defaults to `document`. */\n target?: Document;\n}\n\nconst PREFETCH_DEBOUNCE_MS = 150;\n\n/**\n * Wire clipboard copy for a selection. Two delivery paths, both fed by the\n * prefetched text:\n *\n * - the native `copy` event (menu Edit→Copy, or ctrl/cmd+C while the page\n * has a DOM selection or focused editable — e.g. the viewer shell's\n * focus sink): answered SYNCHRONOUSLY from the cache;\n * - ctrl/cmd+C with no DOM selection (canvas-rendered viewers usually\n * have none, and browsers don't dispatch `copy` then): a keydown\n * fallback writes via the async Clipboard API inside the keystroke's\n * user-activation window.\n *\n * Returns the unwire function. Mount once per document view (not per page).\n */\nexport function wireSelectionClipboard(\n selection: ClipboardSelectionSource,\n options: SelectionClipboardOptions = {},\n): () => void {\n const target = options.target ?? document;\n const prefetch = options.prefetch ?? true;\n let cached: string | null = null;\n let generation = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const refresh = (): void => {\n cached = null;\n if (!prefetch || !selection.hasSelection() || !selection.canCopy()) return;\n const gen = ++generation;\n selection.readText().then(\n (text) => {\n if (gen === generation) cached = text;\n },\n () => {}, // denied / failed — the async path will surface it if used\n );\n };\n\n // Commit (gesture end) refreshes immediately; change (drag, programmatic\n // select) debounces so a drag doesn't fetch per pointer move.\n const offCommit = selection.onCommit(() => {\n if (timer) clearTimeout(timer);\n timer = null;\n refresh();\n });\n const offChange = selection.onChange(() => {\n generation++;\n cached = null;\n if (timer) clearTimeout(timer);\n timer = setTimeout(refresh, PREFETCH_DEBOUNCE_MS);\n });\n\n const onCopy = (e: ClipboardEvent): void => {\n if (cached == null || cached === '' || !selection.hasSelection()) return;\n e.clipboardData?.setData('text/plain', cached);\n e.preventDefault();\n };\n\n const onKeyDown = (e: KeyboardEvent): void => {\n if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'c' || e.shiftKey || e.altKey) {\n return;\n }\n if (!selection.hasSelection() || !selection.canCopy()) return;\n // A live DOM selection means the browser will dispatch `copy` itself and\n // our synchronous handler answers — don't double-write.\n const dom = target.getSelection?.();\n if (dom && !dom.isCollapsed) return;\n void copySelection(selection); // instant when prefetched; still inside user activation\n };\n\n target.addEventListener('copy', onCopy);\n target.addEventListener('keydown', onKeyDown);\n return () => {\n offCommit();\n offChange();\n if (timer) clearTimeout(timer);\n target.removeEventListener('copy', onCopy);\n target.removeEventListener('keydown', onKeyDown);\n };\n}\n\n/**\n * Toolbar/menu path: read the selected text and write it to the clipboard\n * via the async Clipboard API. Call from a user-gesture handler (browsers\n * require a secure context + user activation for clipboard writes). Resolves\n * with what was copied ('' when nothing was — nothing is written then, so an\n * empty selection never clobbers the user's clipboard).\n */\nexport async function copySelection(selection: ClipboardSelectionSource): Promise<string> {\n const text = await selection.readText();\n if (text !== '') await navigator.clipboard.writeText(text);\n return text;\n}\n","/**\n * Anchored-overlay geometry for UI floating over page content (selection\n * menus, draft menus, popovers): the projector SNAPSHOT contract, the\n * anchor shape, and the placement math. Pure values and pure functions over\n * structural rect/point shapes — no EmbedPDF types, no framework, no\n * reactive lifecycle — so every framework adapter (React, Vue, Svelte,\n * Angular) shares ONE implementation and binds it with its NATIVE\n * reactivity (context revision, signals, computed, $derived).\n *\n * The boundary laws:\n * - Placement policy lives HERE, above the projector seam: a projector\n * only knows geometry — never what is being positioned nor where it\n * prefers to sit.\n * - A projector describes the CURRENT projection; it never implements a\n * framework-style reactive lifecycle. State-driven changes (the Stage\n * camera) must reach consumers through the framework's own render\n * cycle so surface and overlay commit TOGETHER; only genuinely\n * browser-driven changes (a PageView moving because the document\n * scrolled) use an external listener ({@link observeClientGeometry}).\n */\n\nexport interface AnchoredRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface AnchoredPoint {\n x: number;\n y: number;\n}\n\nexport type AnchoredPlacement = 'top' | 'right' | 'bottom' | 'left';\n\n/** CSS-ready output: absolute/fixed `left`/`top` plus a centering transform. */\nexport interface AnchoredPosition {\n left: number;\n top: number;\n transform: string;\n}\n\n/**\n * Place an upright element around `box` (screen px). `avoid` is a screen\n * point the element must clear (e.g. the rotate knob): the element extends\n * ONLY the edge it sits on, and ONLY when the point protrudes past that\n * edge — so it clears the obstacle without ever shifting off-centre on the\n * other axis. When the point is on another side (e.g. a 90° shape, knob at\n * mid-height for a `top` placement) the edge is untouched and the element\n * stays centred on `box`.\n */\n/** What anchored UI attaches to: a content-space rect on a page, plus\n * optional content-space points the UI must clear (e.g. a rotate knob).\n * Structural — plugin anchor reads satisfy it without importing this\n * package. */\nexport interface AnchorTarget {\n pon: number;\n bounds: AnchoredRect;\n avoid?: AnchoredPoint[];\n}\n\n/**\n * A page surface's PROJECTION SNAPSHOT: how a content-space rect on a page\n * becomes screen coordinates, right now. Provided by `<Stage>`\n * (camera-driven, pure state, no DOM reads) and `<PageView>` (DOM-measured).\n * Deliberately NO subscribe here — when projection changes is a framework\n * binding concern (see the module doc), not part of the snapshot.\n */\nexport interface ViewProjector {\n /**\n * Which coordinate space `toScreen` speaks, and therefore how anchored\n * content mounts:\n * - 'overlay': coords are relative to the surface's own positioned\n * container → rendered in place, position:absolute. (Stage)\n * - 'client': coords are viewport px → portalled to the document body,\n * position:fixed, so no ancestor overflow can clip. (PageView)\n */\n space: 'overlay' | 'client';\n /** Content-space rect on a page → coords in `space`. Null: not projectable\n * right now (page not shown / not measurable yet). */\n toScreen(pon: number, rect: AnchoredRect): AnchoredRect | null;\n toScreenPoint(pon: number, at: AnchoredPoint): AnchoredPoint | null;\n /** The page's live view facts (for anchor reads that need them — e.g. the\n * screen-constant rotate-knob stalk), or null when the page isn't shown. */\n viewEnv(pon: number): {\n scale: number;\n rotation: 0 | 90 | 180 | 270;\n zoom: number;\n } | null;\n}\n\n/**\n * THE anchored projection: anchor → screen position, in one pure call —\n * projection, avoid-point transform, and the shared placement policy.\n * Frameworks recompute this inside their native reactive primitive\n * (a React render, a Vue `computed`, an Angular signal, a Svelte\n * `$derived`); nothing here schedules anything.\n */\nexport function projectAnchoredTarget(\n projector: ViewProjector,\n anchor: AnchorTarget,\n placement: AnchoredPlacement,\n gap: number,\n): AnchoredPosition | null {\n const box = projector.toScreen(anchor.pon, anchor.bounds);\n if (!box) return null;\n const avoid = anchor.avoid?.length\n ? projector.toScreenPoint(anchor.pon, anchor.avoid[0])\n : null;\n return positionAnchoredRect(box, placement, gap, avoid);\n}\n\n/**\n * The one genuinely BROWSER-driven invalidation: an element in normal\n * document flow moves when the document scrolls or the window resizes —\n * no state change announces it. `<PageView>`'s binding registers this;\n * the Stage never does (its camera is state, delivered through render).\n */\nexport function observeClientGeometry(callback: () => void): () => void {\n window.addEventListener('scroll', callback, true);\n window.addEventListener('resize', callback);\n return () => {\n window.removeEventListener('scroll', callback, true);\n window.removeEventListener('resize', callback);\n };\n}\n\nexport function positionAnchoredRect(\n box: AnchoredRect,\n placement: AnchoredPlacement,\n gap: number,\n avoid?: AnchoredPoint | null,\n): AnchoredPosition {\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n\n switch (placement) {\n case 'bottom': {\n const edge = Math.max(box.y + box.height, avoid ? avoid.y : -Infinity);\n return { left: cx, top: edge + gap, transform: 'translate(-50%, 0)' };\n }\n case 'left': {\n const edge = Math.min(box.x, avoid ? avoid.x : Infinity);\n return { left: edge - gap, top: cy, transform: 'translate(-100%, -50%)' };\n }\n case 'right': {\n const edge = Math.max(box.x + box.width, avoid ? avoid.x : -Infinity);\n return { left: edge + gap, top: cy, transform: 'translate(0, -50%)' };\n }\n case 'top':\n default: {\n const edge = Math.min(box.y, avoid ? avoid.y : Infinity);\n return { left: cx, top: edge - gap, transform: 'translate(-50%, -100%)' };\n }\n }\n}\n","/**\n * SVG → CSS cursor string. The armed-tool indicator is the CURSOR itself (the\n * only zero-latency pointer-locked pixel the web has — the OS composites it),\n * so an app hands its toolbar icon here and assigns the result via the\n * interaction hub's `setToolCursor`.\n *\n * Browser rules baked into this module:\n * - The SVG must carry explicit `width`/`height` attributes (not just a\n * viewBox) or Chromium/WebKit ignore it — enforced here.\n * - The hard ceiling is 128×128 CSS px (bigger falls back to the keyword).\n * Above 32×32, Chromium temporarily resets the cursor to default while it\n * would overlap browser UI (anti-spoofing) — fine mid-viewport, so ~40px\n * compositions are common; stay ≤128.\n * - The keyword fallback covers browsers that refuse SVG cursor images.\n *\n * Pure string work with no EmbedPDF types — like the file picker, this module\n * is a plain web utility.\n */\n\nexport interface SvgCursorOptions {\n /** Complete `<svg>` markup, colors baked in (no `currentColor` — a cursor\n * image has no cascade to inherit from). */\n svg: string;\n /** The click point inside the image, CSS px from the top-left. Default 0,0\n * (arrow-style: the image's corner is the tip). */\n hotspot?: { x: number; y: number };\n /** Keyword cursor when the image can't render. Default `'crosshair'`. */\n fallback?: string;\n}\n\n/** Build the CSS cursor value: `url(\"data:image/svg+xml,…\") x y, fallback`. */\nexport function svgCursor({ svg, hotspot, fallback = 'crosshair' }: SvgCursorOptions): string {\n // Scan only the opening `<svg …>` tag (bounded slice) rather than running\n // `/<svg[^>]*\\swidth\\s*=/` over the whole string — the unbounded form is\n // quadratic on pathological inputs (many `<svg` fragments with no `>`),\n // while accepting exactly the same markup: `[^>]*` could never cross a\n // `>` either.\n const tagStart = svg.indexOf('<svg');\n const tagEnd = tagStart >= 0 ? svg.indexOf('>', tagStart) : -1;\n const openTag = tagEnd >= 0 ? svg.slice(tagStart, tagEnd + 1) : '';\n if (!/\\swidth\\s*=/.test(openTag) || !/\\sheight\\s*=/.test(openTag)) {\n throw new Error(\n '[web] svgCursor: the <svg> needs explicit width/height attributes — ' +\n 'browsers ignore cursor images without an intrinsic size',\n );\n }\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n const spot = hotspot ? ` ${Math.round(hotspot.x)} ${Math.round(hotspot.y)}` : '';\n return `url(\"${uri}\")${spot}, ${fallback}`;\n}\n","/**\n * External-URI hygiene for PDF link annotations — written ONCE here so every\n * framework adapter (react, vue, svelte, angular) shares the same policy.\n *\n * PDFs are untrusted input: a link's `/URI` can carry anything, including\n * `javascript:` payloads. The default allowlist covers what a document link\n * legitimately opens; everything else (javascript:, file:, data:, vbscript:,\n * custom app schemes) resolves to `null` and the adapters render the link\n * WITHOUT an href — activation still reports through the plugin, so an\n * embedder that needs a custom scheme can handle it in `onActivate`.\n */\nconst DEFAULT_ALLOWED_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * Validate a link URI for opening in the browser. Returns the URI when its\n * scheme is allowed (scheme-relative and bare URIs resolve as https), else\n * `null`. Never throws — a malformed URI is just an unopenable one.\n */\nexport function sanitizeExternalUri(\n uri: string,\n allowedSchemes: Iterable<string> = DEFAULT_ALLOWED_SCHEMES,\n): string | null {\n const trimmed = uri.trim();\n if (!trimmed) return null;\n const allowed = new Set(\n [...allowedSchemes].map((s) => (s.endsWith(':') ? s.toLowerCase() : `${s.toLowerCase()}:`)),\n );\n try {\n // Base resolves scheme-relative (\"//host\") and bare (\"example.com/x\" is a\n // relative path → https) forms; an absolute scheme wins over the base.\n const url = new URL(trimmed, 'https://invalid.example/');\n return allowed.has(url.protocol) ? trimmed : null;\n } catch {\n return null;\n }\n}\n","/** A browser image source whose object URL has an explicit lifetime. */\nexport interface ObjectUrlImageSource {\n objectUrl(signal?: AbortSignal): Promise<{ url: string; revoke(): void }>;\n}\n\nexport interface PaintedImageCallbacks {\n onPainted(): void;\n onUnpainted(): void;\n}\n\n/**\n * Bind a raster source to an <img> without ever painting its incomplete-image\n * placeholder. The source becomes visible on load; retained coverage is\n * released only after the browser has had one full frame to present it.\n *\n * Call this from the framework's pre-paint mount hook and invoke the returned\n * cleanup on unmount. Keeping this DOM lifecycle here lets every framework\n * adapter remain a ref plus one bind/unbind hook.\n */\nexport function bindPaintedImage(\n image: HTMLImageElement,\n source: ObjectUrlImageSource,\n callbacks: PaintedImageCallbacks,\n): () => void {\n const controller = new AbortController();\n let revoke: (() => void) | undefined;\n let frame = 0;\n let painted = false;\n\n // The element is already committed when a mount hook runs. Hide it before\n // that commit can paint, then reveal only once the bitmap has loaded.\n image.style.visibility = 'hidden';\n\n const onLoad = () => {\n image.style.visibility = 'visible';\n\n // rAF callbacks run before a frame paints. The first frame presents the\n // newly visible image while retained coverage remains; the second reports\n // that presentation opportunity and permits the retained source to leave.\n frame = requestAnimationFrame(() => {\n frame = requestAnimationFrame(() => {\n frame = 0;\n painted = true;\n callbacks.onPainted();\n });\n });\n };\n\n image.addEventListener('load', onLoad, { once: true });\n\n void source.objectUrl(controller.signal).then(\n (objectUrl) => {\n if (controller.signal.aborted) {\n objectUrl.revoke();\n return;\n }\n revoke = objectUrl.revoke;\n image.src = objectUrl.url;\n },\n () => {\n // Aborted or unavailable: remain hidden and retain fallback coverage.\n },\n );\n\n return () => {\n controller.abort();\n if (frame) cancelAnimationFrame(frame);\n image.removeEventListener('load', onLoad);\n image.style.visibility = 'hidden';\n image.removeAttribute('src');\n revoke?.();\n if (painted) callbacks.onUnpainted();\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embedpdf/web",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Framework-free browser adapters for EmbedPDF v3 — the one home for code that touches window/document (file dialog, clipboard, print…). Imported by the framework layers; never by a plugin or a *-core package, which stay DOM-free.",
|
|
6
6
|
"type": "module",
|