@embedpdf/plugin-link 3.0.0-next.13 → 3.0.0-next.15
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/contract/host.cjs +9 -0
- package/dist/contract/host.cjs.map +1 -0
- package/dist/contract/host.d.cts +3 -0
- package/dist/contract/host.d.ts +3 -0
- package/dist/contract/host.js +9 -0
- package/dist/contract/host.js.map +1 -0
- package/dist/contract-BL4jWtGT.d.ts +136 -0
- package/dist/contract-C8by3cQG.d.cts +136 -0
- package/dist/contract-CaaMtYBR.cjs +19 -0
- package/dist/contract-CaaMtYBR.cjs.map +1 -0
- package/dist/contract-D0k-EtjB.js +15 -0
- package/dist/contract-D0k-EtjB.js.map +1 -0
- package/dist/contract.cjs +1 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +1 -1
- package/dist/host-contract-BEEpcMUN.d.ts +23 -0
- package/dist/host-contract-WJZHqcPp.d.cts +23 -0
- package/dist/index.cjs +233 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +234 -175
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
- package/dist/contract-B8bEMeH8.d.ts +0 -92
- package/dist/contract-DGKEPl11.cjs +0 -11
- package/dist/contract-DGKEPl11.cjs.map +0 -1
- package/dist/contract-DO3vDsAV.d.cts +0 -92
- package/dist/contract-HRggERzU.js +0 -7
- package/dist/contract-HRggERzU.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,196 +1,267 @@
|
|
|
1
|
-
import { t as LinkToken } from "./contract-
|
|
2
|
-
import {
|
|
1
|
+
import { t as LinkToken } from "./contract-D0k-EtjB.js";
|
|
2
|
+
import { LinkToken as LinkToken$1 } from "./contract/host.js";
|
|
3
|
+
import { PluginError, definePlugin } from "@embedpdf/core";
|
|
3
4
|
import { ActionsToken } from "@embedpdf/plugin-actions/contract";
|
|
4
5
|
import { AnnotationToken } from "@embedpdf/plugin-annotation/contract";
|
|
5
|
-
import { AnnotationToken as AnnotationToken$1 } from "@embedpdf/plugin-annotation/contract/host";
|
|
6
6
|
import { InteractionToken } from "@embedpdf/plugin-interaction/contract";
|
|
7
7
|
import { StageToken } from "@embedpdf/plugin-stage/contract";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { AnnotationToken as AnnotationToken$1 } from "@embedpdf/plugin-annotation/contract/host";
|
|
9
|
+
import { InteractionToken as InteractionToken$1 } from "@embedpdf/plugin-interaction/contract/host";
|
|
10
|
+
import { annotationKey } from "@embedpdf/engine-core/runtime";
|
|
11
|
+
//#region src/connect.ts
|
|
12
|
+
/**
|
|
13
|
+
* Links are annotations: while a navigation tool is active the link layer
|
|
14
|
+
* owns their input (its anchors take the clicks), so the annotation plane
|
|
15
|
+
* stands down for them. A link still shows as the file draws it.
|
|
16
|
+
*/
|
|
17
|
+
function connectLink(ctx) {
|
|
18
|
+
const annotation = ctx.tryGet(AnnotationToken$1);
|
|
19
|
+
if (!annotation) return;
|
|
20
|
+
const interaction = ctx.get(InteractionToken$1);
|
|
21
|
+
ctx.cleanup(annotation.registerBehavior({
|
|
22
|
+
id: "link-nav",
|
|
23
|
+
matches: (target) => target.subtype === "link",
|
|
24
|
+
engaged: () => interaction.activeToolEnables("link-nav")
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
10
28
|
//#region src/source.ts
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
io.dispatch({
|
|
34
|
-
type: "SET_PAGE",
|
|
35
|
-
pon,
|
|
36
|
-
items
|
|
29
|
+
/**
|
|
30
|
+
* The stand-alone source, used when no annotation plugin is installed: the
|
|
31
|
+
* clickable areas among one page's annotations, in page space.
|
|
32
|
+
*/
|
|
33
|
+
/** The visible link annotations of a page that have a target. */
|
|
34
|
+
function linksOf(annotations) {
|
|
35
|
+
const links = [];
|
|
36
|
+
for (const dto of annotations) {
|
|
37
|
+
if (dto.subtype !== "link" || dto.target == null) continue;
|
|
38
|
+
if (dto.hidden || dto.noView) continue;
|
|
39
|
+
links.push({
|
|
40
|
+
id: annotationKey(dto.ref),
|
|
41
|
+
bounds: dto.rect,
|
|
42
|
+
target: dto.target,
|
|
43
|
+
...dto.actions?.activate ? { activate: dto.actions.activate } : {},
|
|
44
|
+
...dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root ? { hoverEvents: {
|
|
45
|
+
enter: Boolean(dto.actions?.cursorEnter?.root),
|
|
46
|
+
exit: Boolean(dto.actions?.cursorExit?.root)
|
|
47
|
+
} } : {},
|
|
48
|
+
ref: dto.ref,
|
|
49
|
+
attached: dto.reply?.type === "group"
|
|
37
50
|
});
|
|
38
|
-
}
|
|
51
|
+
}
|
|
52
|
+
return links;
|
|
39
53
|
}
|
|
40
54
|
//#endregion
|
|
41
|
-
//#region src/
|
|
42
|
-
const EMPTY = [];
|
|
55
|
+
//#region src/controller.ts
|
|
56
|
+
const EMPTY = Object.freeze([]);
|
|
57
|
+
const isLink = (value) => "target" in value && "bounds" in value;
|
|
58
|
+
const contains = (bounds, point) => point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height;
|
|
59
|
+
/** The pages whose annotation lists an event changed, each once. */
|
|
60
|
+
function pagesChangedBy(event) {
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case "annotations.created": return [event.annotation.page];
|
|
63
|
+
case "annotations.updated": return [event.annotation.page];
|
|
64
|
+
case "annotations.deleted":
|
|
65
|
+
case "annotations.reordered": return [event.page];
|
|
66
|
+
case "redaction.applied":
|
|
67
|
+
case "pages.flattened": return event.results.filter((result) => result.status === "applied").map((result) => result.page);
|
|
68
|
+
case "annotations.flattened": return event.results.some((result) => result.status === "applied") ? [event.page] : null;
|
|
69
|
+
default: return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
43
72
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
73
|
+
* The link controller. Two sources for the clickable areas: the annotation
|
|
74
|
+
* plugin's folded model when it is installed (always current), else a page
|
|
75
|
+
* mirror of this plugin's own per-page reads, re-read when a confirmed
|
|
76
|
+
* change (an annotation write, a redaction, a flatten) touches a loaded page. Activation is pure resolution
|
|
77
|
+
* (`resolve`) plus the one side effect this plugin owns, a stage reveal;
|
|
78
|
+
* everything else is reported to the host, which keeps the user gesture.
|
|
49
79
|
*/
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
function createLinkController(ctx) {
|
|
81
|
+
const annotationHost = () => ctx.tryGet(AnnotationToken$1);
|
|
82
|
+
const activated = ctx.events.source();
|
|
83
|
+
const loaded = ctx.events.source();
|
|
84
|
+
const pages = ctx.pageMirror({
|
|
85
|
+
name: "links",
|
|
86
|
+
load: async (doc, page) => {
|
|
87
|
+
if (!ctx.getPage(page)) throw new PluginError("not-found", "link", `page ${page.objectNumber} is not in this document`);
|
|
88
|
+
return linksOf((await doc.page(page).annotations.list()).annotations);
|
|
89
|
+
},
|
|
90
|
+
affected: pagesChangedBy,
|
|
91
|
+
changed: ({ page, cause }) => {
|
|
92
|
+
if (cause === "load") loaded.emit({ page });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const listLinks = (pageArgument) => {
|
|
96
|
+
const page = ctx.getPage(pageArgument)?.ref;
|
|
97
|
+
if (!page) return EMPTY;
|
|
98
|
+
const host = annotationHost();
|
|
99
|
+
if (host) return host.listLinkItems(page);
|
|
100
|
+
return pages.get(page) ?? EMPTY;
|
|
101
|
+
};
|
|
102
|
+
const ensureLoaded = async (pageArgument, options) => {
|
|
103
|
+
const { ref } = ctx.pageOf(pageArgument);
|
|
104
|
+
if (annotationHost()) return;
|
|
105
|
+
await ctx.cancellable(options?.signal, pages.ensureLoaded(ref));
|
|
106
|
+
};
|
|
107
|
+
const uriOpeners = [];
|
|
108
|
+
const resolve = (target) => {
|
|
109
|
+
switch (target.kind) {
|
|
110
|
+
case "goto": return ctx.getPage(target.destination.page) ? {
|
|
111
|
+
kind: "destination",
|
|
112
|
+
destination: target.destination
|
|
113
|
+
} : {
|
|
114
|
+
kind: "reported",
|
|
115
|
+
target
|
|
66
116
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
case "uri": return {
|
|
118
|
+
kind: "uri",
|
|
119
|
+
uri: target.uri
|
|
120
|
+
};
|
|
121
|
+
case "named": return {
|
|
122
|
+
kind: "named",
|
|
123
|
+
name: target.name
|
|
124
|
+
};
|
|
125
|
+
default: return {
|
|
126
|
+
kind: "reported",
|
|
127
|
+
target
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const perform = (target, context) => {
|
|
132
|
+
const actions = ctx.tryGet(ActionsToken);
|
|
133
|
+
if (actions && context?.activate) return {
|
|
134
|
+
outcome: "dispatched",
|
|
135
|
+
dispatch: actions.execute(context.activate, {
|
|
136
|
+
origin: "user",
|
|
137
|
+
source: {
|
|
138
|
+
kind: "link",
|
|
139
|
+
annotation: context.ref,
|
|
140
|
+
page: context.page
|
|
141
|
+
},
|
|
142
|
+
event: { scope: "activate" }
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
const resolution = resolve(target);
|
|
146
|
+
switch (resolution.kind) {
|
|
147
|
+
case "destination": {
|
|
148
|
+
const stage = context?.stage ?? ctx.tryGet(StageToken);
|
|
149
|
+
if (!stage) return {
|
|
150
|
+
outcome: "destination",
|
|
151
|
+
destination: resolution.destination
|
|
85
152
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
stage.goToDestination(resolution.destination);
|
|
154
|
+
return { outcome: "revealed" };
|
|
155
|
+
}
|
|
156
|
+
case "uri": {
|
|
157
|
+
const open = uriOpeners[0];
|
|
158
|
+
if (!open || open(resolution.uri)) return {
|
|
159
|
+
outcome: "uri",
|
|
160
|
+
uri: resolution.uri
|
|
89
161
|
};
|
|
90
|
-
|
|
162
|
+
return {
|
|
91
163
|
outcome: "reported",
|
|
92
164
|
target
|
|
93
165
|
};
|
|
94
166
|
}
|
|
95
|
-
|
|
96
|
-
|
|
167
|
+
case "named": return {
|
|
168
|
+
outcome: "named",
|
|
169
|
+
name: resolution.name
|
|
170
|
+
};
|
|
171
|
+
default: return {
|
|
172
|
+
outcome: "reported",
|
|
173
|
+
target
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const activate = (input, context) => {
|
|
178
|
+
const target = isLink(input) ? input.target : input;
|
|
179
|
+
const linkContext = isLink(input) ? {
|
|
180
|
+
activate: input.activate,
|
|
181
|
+
ref: input.ref,
|
|
182
|
+
...context
|
|
183
|
+
} : context;
|
|
184
|
+
const activation = perform(target, linkContext);
|
|
185
|
+
activated.emit({
|
|
97
186
|
target,
|
|
98
187
|
activation
|
|
99
188
|
});
|
|
100
189
|
return activation;
|
|
101
190
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const host = anno();
|
|
110
|
-
if (host) {
|
|
111
|
-
host.ensurePage(pon);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!(pon in ctx.getState().pages)) loadLinksPage(ctx, pon);
|
|
115
|
-
},
|
|
116
|
-
engaged: () => ctx.tryGet(InteractionToken)?.activeTool()?.enables.has("link-nav") ?? false,
|
|
117
|
-
activate
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/effects.ts
|
|
122
|
-
/**
|
|
123
|
-
* Keeps the ENGINE-BACKED page cache honest in viewer-only deployments:
|
|
124
|
-
* any annotation mutation on a cached page (own or remote — the cache has
|
|
125
|
-
* no other refresh path, unlike the annotation plugin's optimistic model)
|
|
126
|
-
* refetches that page's links. With the annotation plugin present the
|
|
127
|
-
* folded model is the source and this effect never fires a fetch.
|
|
128
|
-
*/
|
|
129
|
-
function registerLinkEffects(ctx) {
|
|
130
|
-
const doc = ctx.doc;
|
|
131
|
-
if (!doc) return;
|
|
132
|
-
const refetch = (pon) => {
|
|
133
|
-
if (ctx.tryGet(AnnotationToken$1)) return;
|
|
134
|
-
if (pon in ctx.getState().pages) loadLinksPage(ctx, pon);
|
|
191
|
+
const getLinkAt = (page, point) => {
|
|
192
|
+
let best = null;
|
|
193
|
+
for (const link of listLinks(page)) {
|
|
194
|
+
if (!contains(link.bounds, point)) continue;
|
|
195
|
+
if (!best || link.bounds.width * link.bounds.height < best.bounds.width * best.bounds.height) best = link;
|
|
196
|
+
}
|
|
197
|
+
return best;
|
|
135
198
|
};
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
case "
|
|
142
|
-
|
|
143
|
-
break;
|
|
144
|
-
case "annotation.moved":
|
|
145
|
-
if (event.moved.length) refetch(event.moved[0].pageObjectNumber);
|
|
146
|
-
break;
|
|
147
|
-
case "annotation.deleted":
|
|
148
|
-
refetch(event.pageObjectNumber);
|
|
149
|
-
break;
|
|
150
|
-
default: break;
|
|
199
|
+
const getLabel = (input) => {
|
|
200
|
+
const target = isLink(input) ? input.target : input;
|
|
201
|
+
switch (target.kind) {
|
|
202
|
+
case "uri": return target.uri;
|
|
203
|
+
case "goto": return "Go to destination";
|
|
204
|
+
case "named": return target.name;
|
|
205
|
+
default: return "Link";
|
|
151
206
|
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
api: {
|
|
210
|
+
listLinks,
|
|
211
|
+
getLink: (page, linkId) => listLinks(page).find((link) => link.id === linkId) ?? null,
|
|
212
|
+
getLinkAt,
|
|
213
|
+
listAllLinks: async (options) => {
|
|
214
|
+
const layouts = ctx.document()?.pages ?? [];
|
|
215
|
+
await ctx.cancellable(options?.signal, Promise.all(layouts.map((layout) => ensureLoaded(layout.ref, options))));
|
|
216
|
+
return layouts.flatMap((layout) => listLinks(layout.ref));
|
|
217
|
+
},
|
|
218
|
+
ensureLoaded,
|
|
219
|
+
isLoaded: (page) => {
|
|
220
|
+
const ref = ctx.getPage(page)?.ref;
|
|
221
|
+
return !!ref && (annotationHost() !== null || pages.getStatus(ref) === "ready");
|
|
222
|
+
},
|
|
223
|
+
getStatus: (page) => {
|
|
224
|
+
const ref = ctx.getPage(page)?.ref;
|
|
225
|
+
if (!ref) return "idle";
|
|
226
|
+
return annotationHost() ? "ready" : pages.getStatus(ref);
|
|
227
|
+
},
|
|
228
|
+
resolve,
|
|
229
|
+
activate,
|
|
230
|
+
activateAt: (pageArgument, point, context) => {
|
|
231
|
+
const page = ctx.pageOf(pageArgument).ref;
|
|
232
|
+
const link = getLinkAt(page, point);
|
|
233
|
+
return link ? activate(link, {
|
|
234
|
+
page,
|
|
235
|
+
...context
|
|
236
|
+
}) : null;
|
|
237
|
+
},
|
|
238
|
+
getLabel,
|
|
239
|
+
onActivated: activated.on,
|
|
240
|
+
onLoaded: loaded.on,
|
|
241
|
+
registerUriOpener: (opener) => {
|
|
242
|
+
uriOpeners.unshift(opener);
|
|
243
|
+
return () => {
|
|
244
|
+
const index = uriOpeners.indexOf(opener);
|
|
245
|
+
if (index >= 0) uriOpeners.splice(index, 1);
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
isNavigationEngaged: () => ctx.tryGet(InteractionToken$1)?.activeToolEnables("link-nav") ?? false
|
|
249
|
+
},
|
|
250
|
+
connect() {
|
|
251
|
+
connectLink(ctx);
|
|
177
252
|
}
|
|
178
|
-
}
|
|
253
|
+
};
|
|
179
254
|
}
|
|
180
255
|
//#endregion
|
|
181
256
|
//#region src/link.plugin.ts
|
|
182
257
|
/**
|
|
183
|
-
*
|
|
184
|
-
* interaction hub
|
|
185
|
-
*
|
|
186
|
-
* present, its folded model becomes the data source (no double fetch) and a
|
|
187
|
-
* Behavior keeps links navigation-owned while a `link-nav` tool is active
|
|
188
|
-
* (pointer/pan/form-fill by default) — the single-active-tool hub IS the
|
|
189
|
-
* mode switch: grab the link tool and every link becomes an editable rect.
|
|
258
|
+
* Clickable link regions and their activation. Document-scoped; needs the
|
|
259
|
+
* interaction hub (the navigation tool tag), optionally the stage (reveal),
|
|
260
|
+
* the annotation plugin (the folded link model) and actions (`/A` trees).
|
|
190
261
|
*/
|
|
191
|
-
const linkPlugin = (
|
|
262
|
+
const linkPlugin = () => definePlugin({
|
|
192
263
|
id: "link",
|
|
193
|
-
token: LinkToken,
|
|
264
|
+
token: LinkToken$1,
|
|
194
265
|
scope: "document",
|
|
195
266
|
requires: [InteractionToken],
|
|
196
267
|
optional: [
|
|
@@ -198,19 +269,7 @@ const linkPlugin = (config) => definePlugin({
|
|
|
198
269
|
AnnotationToken,
|
|
199
270
|
ActionsToken
|
|
200
271
|
],
|
|
201
|
-
|
|
202
|
-
reduce: linkReducer,
|
|
203
|
-
capability: (ctx) => createLinkCapability(ctx, config),
|
|
204
|
-
effects: registerLinkEffects,
|
|
205
|
-
init: (ctx) => {
|
|
206
|
-
const interaction = ctx.get(InteractionToken);
|
|
207
|
-
const annotation = ctx.tryGet(AnnotationToken$1);
|
|
208
|
-
if (annotation) annotation.registerBehavior({
|
|
209
|
-
id: "link-nav",
|
|
210
|
-
matches: (a) => a.subtype === "link",
|
|
211
|
-
engaged: () => interaction.activeTool()?.enables.has("link-nav") ?? false
|
|
212
|
-
});
|
|
213
|
-
}
|
|
272
|
+
create: createLinkController
|
|
214
273
|
});
|
|
215
274
|
//#endregion
|
|
216
275
|
export { LinkToken, linkPlugin };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["AnnotationHostToken","AnnotationHostToken","AnnotationHostToken"],"sources":["../src/source.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/link.plugin.ts"],"sourcesContent":["import type { DocumentHandle, PageLayout } from '@embedpdf/core';\nimport { pdfToContentRect } from '@embedpdf/core-annotation';\nimport type { LinkAction, LinkNavItem } from './types';\n\n/**\n * The ENGINE-BACKED link source, for viewer-only deployments (no annotation\n * plugin): read a page's annotation list and keep just the navigable links —\n * standalone AND attached children alike (both are raw link DTOs here).\n * Shared by the capability's lazy `ensurePage` and the effects module's\n * event-driven refetch; both PluginContext and EffectContext satisfy the\n * structural `io` slice.\n */\nexport interface LinkSourceIO {\n doc: DocumentHandle | null;\n document(): { pages: readonly PageLayout[] } | null;\n dispatch(action: LinkAction): void;\n}\n\nexport function loadLinksPage(io: LinkSourceIO, pon: number): void {\n const doc = io.doc;\n const crop = io.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;\n if (!doc || !crop) return;\n doc\n .page(pon)\n .annotations.list()\n .then(\n (snap) => {\n const items: LinkNavItem[] = [];\n for (const dto of snap.annotations) {\n if (dto.subtype !== 'link' || dto.target == null) continue;\n if (dto.flags.hidden || dto.flags.noView) continue;\n items.push({\n id:\n dto.ref.kind === 'objectNumber'\n ? `obj:${dto.ref.annotObjectNumber}`\n : `idx:${pon}:${dto.index}`,\n rect: pdfToContentRect(dto.rect, crop),\n target: dto.target,\n ...(dto.actions?.activate ? { activate: dto.actions.activate } : {}),\n ...(dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root\n ? {\n hoverEvents: {\n enter: Boolean(dto.actions?.cursorEnter?.root),\n exit: Boolean(dto.actions?.cursorExit?.root),\n },\n }\n : {}),\n ref: dto.ref,\n // A `/RT /Group` child riding another annotation — labeled so the\n // nav layer can defer to editing (moot in viewer-only deployments,\n // but the item contract stays truthful either way).\n attached: dto.replyType === 'group' && dto.inReplyTo != null,\n });\n }\n io.dispatch({ type: 'SET_PAGE', pon, items });\n },\n () => {},\n );\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport type { PdfLinkTarget } from '@embedpdf/engine-core/runtime';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\n// The host lens (linkItemsOn) — same runtime token as the public one.\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\nimport { destinationToReveal } from '@embedpdf/plugin-stage/destination';\nimport { loadLinksPage } from './source';\nimport type {\n LinkAction,\n LinkActivateContext,\n LinkActivation,\n LinkCapability,\n LinkNavItem,\n LinkPluginConfig,\n LinkState,\n} from './types';\n\nconst EMPTY: LinkNavItem[] = [];\n\n/**\n * The navigation plane's surface. Data source is decided PER CALL (lazily,\n * so plugin registration order can't matter): the annotation plugin's\n * folded model when present — its `linkItemsOn` lens is memoized by model\n * identity and already excludes hidden links — else this plugin's own\n * engine-backed page cache (see source.ts).\n */\nexport function createLinkCapability(\n ctx: PluginContext<LinkState, LinkAction>,\n config: LinkPluginConfig = {},\n): LinkCapability {\n const anno = () => ctx.tryGet(AnnotationHostToken);\n\n const activate = (target: PdfLinkTarget, context?: LinkActivateContext): LinkActivation => {\n const activation = ((): LinkActivation => {\n // The action engine takes precedence when it is installed and the item\n // carries its payload tree: named verbs execute, mixed /Next chains\n // run, and the dispatcher enforces policy + the incomplete-tree law.\n // Without it, the classic root-projection path below is the fallback.\n const actions = ctx.tryGet(ActionsToken);\n if (actions && context?.activate) {\n const dispatch = actions.execute(context.activate, {\n origin: 'user',\n source: { kind: 'link', annotation: context.ref, pon: context.pon },\n event: { scope: 'activate' },\n });\n return { outcome: 'dispatched', dispatch };\n }\n switch (target.kind) {\n case 'goto': {\n const stage = ctx.tryGet(StageToken);\n const layout = ctx\n .document()\n ?.pages.find((p) => p.pageObjectNumber === target.destination.pageObjectNumber);\n if (!stage || !layout) {\n // No camera to drive — hand the embedder the explicit destination.\n return { outcome: 'destination', destination: target.destination };\n }\n const { pageIndex, options } = destinationToReveal(target.destination, layout);\n stage.reveal(pageIndex, { ...options, behavior: 'smooth' });\n return { outcome: 'revealed' };\n }\n case 'uri':\n // Opening is the FRAMEWORK layer's job (a real <a href> after\n // sanitizing) — this plugin never touches the DOM.\n return { outcome: 'uri', uri: target.uri };\n case 'named':\n return { outcome: 'named', name: target.name };\n default:\n // javascript / goto-remote / launch / unsupported: reported, never\n // executed here. JavaScript activation belongs to the scripting\n // orchestrator once it lands — the chain already rides the\n // annotation's base `actions`.\n return { outcome: 'reported', target };\n }\n })();\n config.onActivate?.({ target, activation });\n return activation;\n };\n\n return {\n linksOn: (pon) => {\n const host = anno();\n if (host) return host.linkItemsOn(pon);\n return ctx.getState().pages[pon] ?? EMPTY;\n },\n ensurePage: (pon) => {\n const host = anno();\n if (host) {\n host.ensurePage(pon); // the folded model is the source; it lazy-loads\n return;\n }\n if (!(pon in ctx.getState().pages)) loadLinksPage(ctx, pon);\n },\n engaged: () => ctx.tryGet(InteractionToken)?.activeTool()?.enables.has('link-nav') ?? false,\n activate,\n };\n}\n","import type { DocumentEvent, EffectContext } from '@embedpdf/core';\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { loadLinksPage } from './source';\nimport type { LinkAction, LinkState } from './types';\n\n/**\n * Keeps the ENGINE-BACKED page cache honest in viewer-only deployments:\n * any annotation mutation on a cached page (own or remote — the cache has\n * no other refresh path, unlike the annotation plugin's optimistic model)\n * refetches that page's links. With the annotation plugin present the\n * folded model is the source and this effect never fires a fetch.\n */\nexport function registerLinkEffects(ctx: EffectContext<LinkState, LinkAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n const refetch = (pon: number): void => {\n if (ctx.tryGet(AnnotationHostToken)) return; // annotation model owns the data\n if (pon in ctx.getState().pages) loadLinksPage(ctx, pon);\n };\n\n const unsubscribe = doc.events.subscribe((event: DocumentEvent) => {\n switch (event.type) {\n case 'annotation.created':\n refetch(event.created.pageObjectNumber);\n break;\n case 'annotation.updated':\n refetch(event.updated.pageObjectNumber);\n break;\n case 'annotation.moved':\n if (event.moved.length) refetch(event.moved[0].pageObjectNumber);\n break;\n case 'annotation.deleted':\n refetch(event.pageObjectNumber);\n break;\n default:\n break;\n }\n });\n ctx.cleanup(unsubscribe);\n}\n","import type { LinkAction, LinkState } from './types';\n\nexport const initialLinkState = (): LinkState => ({ pages: {} });\n\n/** Pure. Engine-backed page cache only — the annotation-backed mode reads the\n * annotation plugin's model and never touches this state. */\nexport function linkReducer(state: LinkState, a: LinkAction): LinkState {\n switch (a.type) {\n case 'SET_PAGE':\n return { ...state, pages: { ...state.pages, [a.pon]: a.items } };\n case 'DROP_PAGE': {\n if (!(a.pon in state.pages)) return state;\n const pages = { ...state.pages };\n delete pages[a.pon];\n return { ...state, pages };\n }\n }\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract';\n// Behavior registration lives on the HOST capability (framework/plugin\n// surface) — same runtime token, wider type. The form plugin's precedent.\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { createLinkCapability } from './capability';\nimport { registerLinkEffects } from './effects';\nimport { initialLinkState, linkReducer } from './reducer';\nimport { LinkToken } from './types';\nimport type { LinkAction, LinkCapability, LinkPluginConfig, LinkState } from './types';\n\n/**\n * The link plugin: the NAVIGATION plane. Document-scoped; requires only the\n * interaction hub. Navigation works with no annotation plugin at all —\n * links are read from the engine's link DTOs. When the annotation plugin IS\n * present, its folded model becomes the data source (no double fetch) and a\n * Behavior keeps links navigation-owned while a `link-nav` tool is active\n * (pointer/pan/form-fill by default) — the single-active-tool hub IS the\n * mode switch: grab the link tool and every link becomes an editable rect.\n */\nexport const linkPlugin = (config?: LinkPluginConfig) =>\n definePlugin<LinkState, LinkAction, LinkCapability>({\n id: 'link',\n token: LinkToken,\n scope: 'document',\n requires: [InteractionToken],\n optional: [StageToken, AnnotationToken, ActionsToken],\n initialState: initialLinkState,\n reduce: linkReducer,\n capability: (ctx) => createLinkCapability(ctx, config),\n effects: registerLinkEffects,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const annotation = ctx.tryGet(AnnotationHostToken);\n if (annotation) {\n annotation.registerBehavior({\n id: 'link-nav',\n matches: (a) => a.subtype === 'link',\n engaged: () => interaction.activeTool()?.enables.has('link-nav') ?? false,\n });\n }\n },\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["AnnotationToken","InteractionToken","AnnotationHostToken","InteractionToken","LinkToken"],"sources":["../src/connect.ts","../src/source.ts","../src/controller.ts","../src/link.plugin.ts"],"sourcesContent":["/** The link plugin's wiring to its siblings, run from the controller's `connect`. */\nimport type { PluginContext } from '@embedpdf/core';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract/host';\n\n/**\n * Links are annotations: while a navigation tool is active the link layer\n * owns their input (its anchors take the clicks), so the annotation plane\n * stands down for them. A link still shows as the file draws it.\n */\nexport function connectLink(ctx: PluginContext<void>): void {\n const annotation = ctx.tryGet(AnnotationToken);\n if (!annotation) return;\n const interaction = ctx.get(InteractionToken);\n ctx.cleanup(\n annotation.registerBehavior({\n id: 'link-nav',\n matches: (target) => target.subtype === 'link',\n engaged: () => interaction.activeToolEnables('link-nav'),\n }),\n );\n}\n","/**\n * The stand-alone source, used when no annotation plugin is installed: the\n * clickable areas among one page's annotations, in page space.\n */\nimport { annotationKey, type Annotation } from '@embedpdf/engine-core/runtime';\n\nimport type { Link } from './contract';\n\n/** The visible link annotations of a page that have a target. */\nexport function linksOf(annotations: readonly Annotation[]): readonly Link[] {\n const links: Link[] = [];\n for (const dto of annotations) {\n if (dto.subtype !== 'link' || dto.target == null) continue;\n if (dto.hidden || dto.noView) continue;\n links.push({\n id: annotationKey(dto.ref),\n bounds: dto.rect,\n target: dto.target,\n ...(dto.actions?.activate ? { activate: dto.actions.activate } : {}),\n ...(dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root\n ? {\n hoverEvents: {\n enter: Boolean(dto.actions?.cursorEnter?.root),\n exit: Boolean(dto.actions?.cursorExit?.root),\n },\n }\n : {}),\n ref: dto.ref,\n attached: dto.reply?.type === 'group',\n });\n }\n return links;\n}\n","import {\n PluginError,\n type PluginContext,\n type DocumentEvent,\n type OperationOptions,\n type PageRef,\n} from '@embedpdf/core';\nimport type { Point } from '@embedpdf/core-geometry';\nimport type { PdfLinkTarget } from '@embedpdf/engine-core/runtime';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract/host';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { connectLink } from './connect';\nimport type {\n Link,\n LinkActivateContext,\n LinkActivatedEvent,\n LinkActivation,\n LinkLoadedEvent,\n LinkResolution,\n} from './contract';\nimport type { LinkHostCapability, UriOpener } from './host-contract';\nimport { linksOf } from './source';\n\nconst EMPTY: readonly Link[] = Object.freeze([]);\n\nconst isLink = (value: PdfLinkTarget | Link): value is Link =>\n 'target' in value && 'bounds' in value;\nconst contains = (\n bounds: { x: number; y: number; width: number; height: number },\n point: Point,\n): boolean =>\n point.x >= bounds.x &&\n point.x <= bounds.x + bounds.width &&\n point.y >= bounds.y &&\n point.y <= bounds.y + bounds.height;\n\n/** The pages whose annotation lists an event changed, each once. */\nfunction pagesChangedBy(event: DocumentEvent): readonly PageRef[] | null {\n switch (event.type) {\n case 'annotations.created':\n return [event.annotation.page];\n case 'annotations.updated':\n return [event.annotation.page];\n case 'annotations.deleted':\n case 'annotations.reordered':\n return [event.page];\n // These can remove annotations, links included, from the pages they applied to.\n case 'redaction.applied':\n case 'pages.flattened':\n return event.results\n .filter((result) => result.status === 'applied')\n .map((result) => result.page);\n case 'annotations.flattened':\n return event.results.some((result) => result.status === 'applied') ? [event.page] : null;\n default:\n return null;\n }\n}\n\n/**\n * The link controller. Two sources for the clickable areas: the annotation\n * plugin's folded model when it is installed (always current), else a page\n * mirror of this plugin's own per-page reads, re-read when a confirmed\n * change (an annotation write, a redaction, a flatten) touches a loaded page. Activation is pure resolution\n * (`resolve`) plus the one side effect this plugin owns, a stage reveal;\n * everything else is reported to the host, which keeps the user gesture.\n */\nexport function createLinkController(ctx: PluginContext<void>) {\n const annotationHost = () => ctx.tryGet(AnnotationHostToken);\n const activated = ctx.events.source<LinkActivatedEvent>();\n const loaded = ctx.events.source<LinkLoadedEvent>();\n\n const pages = ctx.pageMirror<readonly Link[]>({\n name: 'links',\n load: async (doc, page) => {\n if (!ctx.getPage(page)) {\n throw new PluginError(\n 'not-found',\n 'link',\n `page ${page.objectNumber} is not in this document`,\n );\n }\n const snapshot = await doc.page(page).annotations.list();\n return linksOf(snapshot.annotations);\n },\n affected: pagesChangedBy,\n changed: ({ page, cause }) => {\n if (cause === 'load') loaded.emit({ page });\n },\n });\n\n // Reads take a page's ref or its index; a page that isn't there has no links.\n const listLinks = (pageArgument: PageRef | number): readonly Link[] => {\n const page = ctx.getPage(pageArgument)?.ref;\n if (!page) return EMPTY;\n const host = annotationHost();\n if (host) return host.listLinkItems(page);\n return pages.get(page) ?? EMPTY;\n };\n\n const ensureLoaded = async (\n pageArgument: PageRef | number,\n options?: OperationOptions,\n ): Promise<void> => {\n const { ref } = ctx.pageOf(pageArgument);\n // With the annotation plugin installed, its model owns the data.\n if (annotationHost()) return;\n await ctx.cancellable(options?.signal, pages.ensureLoaded(ref));\n };\n\n // The framework's website openers, the latest first: the plugin never opens one itself.\n const uriOpeners: UriOpener[] = [];\n\n const resolve = (target: PdfLinkTarget): LinkResolution => {\n switch (target.kind) {\n case 'goto':\n // A page the document doesn't have can't be shown.\n return ctx.getPage(target.destination.page)\n ? { kind: 'destination', destination: target.destination }\n : { kind: 'reported', target };\n case 'uri':\n return { kind: 'uri', uri: target.uri };\n case 'named':\n return { kind: 'named', name: target.name };\n default:\n return { kind: 'reported', target };\n }\n };\n\n const perform = (target: PdfLinkTarget, context?: LinkActivateContext): LinkActivation => {\n const actions = ctx.tryGet(ActionsToken);\n if (actions && context?.activate) {\n const dispatch = actions.execute(context.activate, {\n origin: 'user',\n source: { kind: 'link', annotation: context.ref, page: context.page },\n event: { scope: 'activate' },\n });\n return { outcome: 'dispatched', dispatch };\n }\n const resolution = resolve(target);\n switch (resolution.kind) {\n case 'destination': {\n // The view the link was followed in, by its own scroll behavior.\n const stage = context?.stage ?? ctx.tryGet(StageToken);\n if (!stage) return { outcome: 'destination', destination: resolution.destination };\n stage.goToDestination(resolution.destination);\n return { outcome: 'revealed' };\n }\n case 'uri': {\n const open = uriOpeners[0];\n if (!open || open(resolution.uri)) return { outcome: 'uri', uri: resolution.uri };\n return { outcome: 'reported', target };\n }\n case 'named':\n return { outcome: 'named', name: resolution.name };\n default:\n return { outcome: 'reported', target };\n }\n };\n\n const activate = (input: PdfLinkTarget | Link, context?: LinkActivateContext): LinkActivation => {\n const target = isLink(input) ? input.target : input;\n const linkContext: LinkActivateContext | undefined = isLink(input)\n ? { activate: input.activate, ref: input.ref, ...context }\n : context;\n const activation = perform(target, linkContext);\n activated.emit({ target, activation });\n return activation;\n };\n\n const getLinkAt = (page: PageRef | number, point: Point): Link | null => {\n let best: Link | null = null;\n for (const link of listLinks(page)) {\n if (!contains(link.bounds, point)) continue;\n if (\n !best ||\n link.bounds.width * link.bounds.height < best.bounds.width * best.bounds.height\n ) {\n best = link;\n }\n }\n return best;\n };\n\n const getLabel = (input: Link | PdfLinkTarget): string => {\n const target = isLink(input) ? input.target : input;\n switch (target.kind) {\n case 'uri':\n return target.uri;\n case 'goto':\n return 'Go to destination';\n case 'named':\n return target.name;\n default:\n return 'Link';\n }\n };\n\n const api: LinkHostCapability = {\n listLinks,\n getLink: (page, linkId) => listLinks(page).find((link) => link.id === linkId) ?? null,\n getLinkAt,\n listAllLinks: async (options) => {\n const layouts = ctx.document()?.pages ?? [];\n await ctx.cancellable(\n options?.signal,\n Promise.all(layouts.map((layout) => ensureLoaded(layout.ref, options))),\n );\n return layouts.flatMap((layout) => listLinks(layout.ref));\n },\n ensureLoaded,\n isLoaded: (page) => {\n const ref = ctx.getPage(page)?.ref;\n return !!ref && (annotationHost() !== null || pages.getStatus(ref) === 'ready');\n },\n getStatus: (page) => {\n const ref = ctx.getPage(page)?.ref;\n if (!ref) return 'idle';\n return annotationHost() ? 'ready' : pages.getStatus(ref);\n },\n resolve,\n activate,\n activateAt: (pageArgument, point, context) => {\n const page = ctx.pageOf(pageArgument).ref;\n const link = getLinkAt(page, point);\n return link ? activate(link, { page, ...context }) : null;\n },\n getLabel,\n onActivated: activated.on,\n onLoaded: loaded.on,\n registerUriOpener: (opener) => {\n uriOpeners.unshift(opener);\n return () => {\n const index = uriOpeners.indexOf(opener);\n if (index >= 0) uriOpeners.splice(index, 1);\n };\n },\n isNavigationEngaged: () => ctx.tryGet(InteractionToken)?.activeToolEnables('link-nav') ?? false,\n };\n\n return {\n api,\n connect() {\n connectLink(ctx);\n },\n };\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { createLinkController } from './controller';\nimport { LinkToken } from './host-contract';\n\n/**\n * Clickable link regions and their activation. Document-scoped; needs the\n * interaction hub (the navigation tool tag), optionally the stage (reveal),\n * the annotation plugin (the folded link model) and actions (`/A` trees).\n */\nexport const linkPlugin = () =>\n definePlugin({\n id: 'link',\n token: LinkToken,\n scope: 'document',\n requires: [InteractionToken],\n optional: [StageToken, AnnotationToken, ActionsToken],\n create: createLinkController,\n });\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embedpdf/plugin-link",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The navigation plane for PDF link annotations: per-page clickable link areas + one activate() entry point (GoTo → stage reveal, URI → framework opener). Works standalone in viewer deployments; with the annotation plugin present it reads the folded model (no double fetch) and a Behavior hands links to the editing plane when the link tool is active.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,20 +16,24 @@
|
|
|
16
16
|
"import": "./dist/contract.js",
|
|
17
17
|
"require": "./dist/contract.cjs"
|
|
18
18
|
},
|
|
19
|
+
"./contract/host": {
|
|
20
|
+
"import": "./dist/contract/host.js",
|
|
21
|
+
"require": "./dist/contract/host.cjs"
|
|
22
|
+
},
|
|
19
23
|
"./package.json": "./package.json"
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist"
|
|
23
27
|
],
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"@embedpdf/core-annotation": "3.0.0-next.
|
|
26
|
-
"@embedpdf/
|
|
27
|
-
"@embedpdf/core": "3.0.0-next.
|
|
28
|
-
"@embedpdf/
|
|
29
|
-
"@embedpdf/plugin-annotation": "3.0.0-next.
|
|
30
|
-
"@embedpdf/plugin-stage": "3.0.0-next.
|
|
31
|
-
"@embedpdf/
|
|
32
|
-
"@embedpdf/
|
|
29
|
+
"@embedpdf/core-annotation": "3.0.0-next.15",
|
|
30
|
+
"@embedpdf/core-geometry": "3.0.0-next.15",
|
|
31
|
+
"@embedpdf/core": "3.0.0-next.15",
|
|
32
|
+
"@embedpdf/plugin-interaction": "3.0.0-next.15",
|
|
33
|
+
"@embedpdf/plugin-annotation": "3.0.0-next.15",
|
|
34
|
+
"@embedpdf/plugin-stage": "3.0.0-next.15",
|
|
35
|
+
"@embedpdf/engine-core": "3.0.0-next.15",
|
|
36
|
+
"@embedpdf/plugin-actions": "3.0.0-next.15"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"typescript": "^5.0.0",
|