@embedpdf/plugin-form 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 +29 -2
- package/dist/contract/host.cjs.map +1 -0
- package/dist/contract/host.d.cts +3 -2
- package/dist/contract/host.d.ts +3 -2
- package/dist/contract/host.js +14 -2
- package/dist/contract/host.js.map +1 -0
- package/dist/contract-BwcgtCOD.d.ts +566 -0
- package/dist/contract-CU0jrI_D.cjs +56 -0
- package/dist/contract-CU0jrI_D.cjs.map +1 -0
- package/dist/contract-D-yQxw3H.d.cts +566 -0
- package/dist/contract-DcncvPcF.js +45 -0
- package/dist/contract-DcncvPcF.js.map +1 -0
- package/dist/contract.cjs +17 -6
- package/dist/contract.d.cts +2 -8
- package/dist/contract.d.ts +2 -8
- package/dist/contract.js +2 -7
- package/dist/controller-CS6tV2m1.cjs +514 -0
- package/dist/controller-CS6tV2m1.cjs.map +1 -0
- package/dist/controller-Sr5M_8G8.js +497 -0
- package/dist/controller-Sr5M_8G8.js.map +1 -0
- package/dist/host-contract-1epLfK1S.d.cts +87 -0
- package/dist/host-contract-BRYUbpT_.d.ts +87 -0
- package/dist/index.cjs +2916 -1205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -17
- package/dist/index.d.ts +46 -17
- package/dist/index.js +2895 -1206
- package/dist/index.js.map +1 -1
- package/dist/scripting.cjs +3 -484
- package/dist/scripting.d.cts +58 -22
- package/dist/scripting.d.ts +58 -22
- package/dist/scripting.js +1 -482
- package/package.json +15 -14
- package/dist/contract.cjs.map +0 -1
- package/dist/contract.js.map +0 -1
- package/dist/internal.cjs +0 -4
- package/dist/internal.d.cts +0 -3
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -3
- package/dist/scripting.cjs.map +0 -1
- package/dist/scripting.js.map +0 -1
- package/dist/types-Bpc32Qmx.d.cts +0 -309
- package/dist/types-C1uIIYfn.cjs +0 -16
- package/dist/types-C1uIIYfn.cjs.map +0 -1
- package/dist/types-DRirhZKr.d.ts +0 -309
- package/dist/types-DkQmmcTG.js +0 -12
- package/dist/types-DkQmmcTG.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,28 +1,2519 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
2
|
+
const require_contract = require("./contract-CU0jrI_D.cjs");
|
|
3
|
+
const require_controller = require("./controller-CS6tV2m1.cjs");
|
|
4
|
+
const require_contract_host = require("./contract/host.cjs");
|
|
5
5
|
let _embedpdf_core = require("@embedpdf/core");
|
|
6
6
|
let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
|
|
7
|
-
let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
|
|
8
7
|
let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
|
|
9
|
-
let
|
|
10
|
-
let
|
|
8
|
+
let _embedpdf_plugin_interaction_contract_host = require("@embedpdf/plugin-interaction/contract/host");
|
|
9
|
+
let _embedpdf_plugin_render_contract = require("@embedpdf/plugin-render/contract");
|
|
11
10
|
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
12
|
-
let
|
|
11
|
+
let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
|
|
12
|
+
let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
|
|
13
|
+
let _embedpdf_core_annotation = require("@embedpdf/core-annotation");
|
|
14
|
+
let _embedpdf_core_form = require("@embedpdf/core-form");
|
|
15
|
+
let _embedpdf_plugin_render_contract_host = require("@embedpdf/plugin-render/contract/host");
|
|
16
|
+
let _embedpdf_engine_core_public = require("@embedpdf/engine-core/public");
|
|
17
|
+
//#region src/tools/definitions.ts
|
|
18
|
+
/** Palette tools keep widgets editable right after placement. */
|
|
19
|
+
const PLACE_TAGS = ["form-place", "annotation-edit"];
|
|
20
|
+
const FIELD_CHROME = {
|
|
21
|
+
interiorColor: "#ffffff",
|
|
22
|
+
color: "#6b7280",
|
|
23
|
+
strokeWidth: 1
|
|
24
|
+
};
|
|
25
|
+
const FORM_TOOLS = [
|
|
26
|
+
{
|
|
27
|
+
id: "form-text",
|
|
28
|
+
family: "text",
|
|
29
|
+
visualKind: "widget-text",
|
|
30
|
+
clickCreate: {
|
|
31
|
+
width: 160,
|
|
32
|
+
height: 24
|
|
33
|
+
},
|
|
34
|
+
defaults: {
|
|
35
|
+
...FIELD_CHROME,
|
|
36
|
+
fontSize: 12
|
|
37
|
+
},
|
|
38
|
+
cursor: "crosshair"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "form-checkbox",
|
|
42
|
+
family: "checkbox",
|
|
43
|
+
visualKind: "widget-toggle",
|
|
44
|
+
clickCreate: {
|
|
45
|
+
width: 18,
|
|
46
|
+
height: 18
|
|
47
|
+
},
|
|
48
|
+
defaults: FIELD_CHROME,
|
|
49
|
+
cursor: "crosshair"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "form-radio",
|
|
53
|
+
family: "radio",
|
|
54
|
+
visualKind: "widget-radio",
|
|
55
|
+
clickCreate: {
|
|
56
|
+
width: 18,
|
|
57
|
+
height: 18
|
|
58
|
+
},
|
|
59
|
+
defaults: FIELD_CHROME,
|
|
60
|
+
cursor: "crosshair"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "form-combobox",
|
|
64
|
+
family: "combobox",
|
|
65
|
+
visualKind: "widget-choice",
|
|
66
|
+
clickCreate: {
|
|
67
|
+
width: 140,
|
|
68
|
+
height: 24
|
|
69
|
+
},
|
|
70
|
+
defaults: {
|
|
71
|
+
...FIELD_CHROME,
|
|
72
|
+
fontSize: 12
|
|
73
|
+
},
|
|
74
|
+
cursor: "crosshair"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "form-listbox",
|
|
78
|
+
family: "listbox",
|
|
79
|
+
visualKind: "widget-choice",
|
|
80
|
+
clickCreate: {
|
|
81
|
+
width: 140,
|
|
82
|
+
height: 72
|
|
83
|
+
},
|
|
84
|
+
defaults: {
|
|
85
|
+
...FIELD_CHROME,
|
|
86
|
+
fontSize: 12
|
|
87
|
+
},
|
|
88
|
+
cursor: "crosshair"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "form-button",
|
|
92
|
+
family: "pushbutton",
|
|
93
|
+
visualKind: "widget-button",
|
|
94
|
+
clickCreate: {
|
|
95
|
+
width: 100,
|
|
96
|
+
height: 28
|
|
97
|
+
},
|
|
98
|
+
defaults: {
|
|
99
|
+
...FIELD_CHROME,
|
|
100
|
+
interiorColor: "#e5e7eb",
|
|
101
|
+
fontSize: 12
|
|
102
|
+
},
|
|
103
|
+
cursor: "crosshair"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "form-signature",
|
|
107
|
+
family: "signature",
|
|
108
|
+
visualKind: "widget-box",
|
|
109
|
+
clickCreate: {
|
|
110
|
+
width: 160,
|
|
111
|
+
height: 48
|
|
112
|
+
},
|
|
113
|
+
defaults: FIELD_CHROME,
|
|
114
|
+
cursor: "crosshair"
|
|
115
|
+
}
|
|
116
|
+
];
|
|
117
|
+
const FORM_TOOL_BY_ID = new Map(FORM_TOOLS.map((tool) => [tool.id, tool]));
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/tools/handlers.ts
|
|
120
|
+
/** A name for a field placed with a tool: `text_1`, `text_2`, …, the first one the form doesn't have. */
|
|
121
|
+
function nextFieldName(family, taken) {
|
|
122
|
+
let count = 1;
|
|
123
|
+
while (taken.has(`${family}_${count}`)) count++;
|
|
124
|
+
return `${family}_${count}`;
|
|
125
|
+
}
|
|
126
|
+
/** The part of a box on the page; `null` when too little of it is. */
|
|
127
|
+
function clampToPage(box, page) {
|
|
128
|
+
const x = Math.max(page.x, Math.min(box.x, page.width));
|
|
129
|
+
const y = Math.max(page.y, Math.min(box.y, page.height));
|
|
130
|
+
const clamped = {
|
|
131
|
+
x,
|
|
132
|
+
y,
|
|
133
|
+
width: Math.max(0, Math.min(box.x + box.width, page.width) - x),
|
|
134
|
+
height: Math.max(0, Math.min(box.y + box.height, page.height) - y)
|
|
135
|
+
};
|
|
136
|
+
return clamped.width < 1 || clamped.height < 1 ? null : clamped;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The field a palette tool places: one widget at `widget`, named like
|
|
140
|
+
* `text_1`. A radio button stands for `'Choice1'` and a dropdown or a list
|
|
141
|
+
* starts with two options, as in Acrobat; rename and change them in a
|
|
142
|
+
* settings panel.
|
|
143
|
+
*/
|
|
144
|
+
function toolDraft(tool, name, widget) {
|
|
145
|
+
switch (tool.family) {
|
|
146
|
+
case "radio": return {
|
|
147
|
+
family: "radio",
|
|
148
|
+
name,
|
|
149
|
+
widgets: [{
|
|
150
|
+
...widget,
|
|
151
|
+
exportValue: "Choice1"
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
case "combobox":
|
|
155
|
+
case "listbox": return {
|
|
156
|
+
family: tool.family,
|
|
157
|
+
name,
|
|
158
|
+
widgets: [widget],
|
|
159
|
+
options: [{
|
|
160
|
+
label: "Option 1",
|
|
161
|
+
value: "Option 1"
|
|
162
|
+
}, {
|
|
163
|
+
label: "Option 2",
|
|
164
|
+
value: "Option 2"
|
|
165
|
+
}]
|
|
166
|
+
};
|
|
167
|
+
case "pushbutton": return {
|
|
168
|
+
family: "pushbutton",
|
|
169
|
+
name,
|
|
170
|
+
widgets: [{
|
|
171
|
+
...widget,
|
|
172
|
+
caption: "Button"
|
|
173
|
+
}]
|
|
174
|
+
};
|
|
175
|
+
default: return {
|
|
176
|
+
family: tool.family,
|
|
177
|
+
name,
|
|
178
|
+
widgets: [widget]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Draw-to-place: drag a box or just click. The commit creates the field and
|
|
184
|
+
* its widget through `form.create()`, styled from the tool's live defaults.
|
|
185
|
+
* Where the field goes is `gesturePlacement`, the call the annotation core
|
|
186
|
+
* makes for its own drawings: the dragged box once the gesture is a drag
|
|
187
|
+
* (the shared click ↔ drag threshold), else the tool's `clickCreate` box.
|
|
188
|
+
* What follows is the annotation plugin's `afterCreate` setting, as for
|
|
189
|
+
* every tool: select the new widget, and keep the tool or go back to the
|
|
190
|
+
* default one.
|
|
191
|
+
*
|
|
192
|
+
* With the annotation plugin, the tool's ghost shows the field a click
|
|
193
|
+
* places: this handler's hover puts it where the click would reach it (no
|
|
194
|
+
* widget or annotation under the pointer claims it) and the user may design
|
|
195
|
+
* forms. The gesture reports itself (`previewPlacement`), so once it drags
|
|
196
|
+
* the page paints the field the release places, a radio button round.
|
|
197
|
+
*
|
|
198
|
+
* Gesture rules match the annotation handlers: page-anchored via the sample
|
|
199
|
+
* projection (the box keeps sizing along the edge when the cursor overshoots
|
|
200
|
+
* or crosses a gap), and the up sample is the final point.
|
|
201
|
+
*/
|
|
202
|
+
function createPlaceHandler(form, interaction, annotation) {
|
|
203
|
+
let origin = null;
|
|
204
|
+
/** Drop the gesture and what it painted: its placement and the tool's ghost. */
|
|
205
|
+
const endGesture = () => {
|
|
206
|
+
origin = null;
|
|
207
|
+
annotation?.clearPlacementPreview();
|
|
208
|
+
annotation?.clearGhost();
|
|
209
|
+
};
|
|
210
|
+
const place = async (tool, page, box) => {
|
|
211
|
+
const pageBox = form.getPageBox(page);
|
|
212
|
+
const rect = pageBox ? clampToPage(box, pageBox) : null;
|
|
213
|
+
if (!rect) return;
|
|
214
|
+
const look = (0, _embedpdf_core_annotation.widgetAppearanceOf)(annotation ? annotation.tools.getDefaults(tool.id) : tool.defaults);
|
|
215
|
+
const name = nextFieldName(tool.family, new Set(form.list().map((field) => field.name)));
|
|
216
|
+
const created = form.create(toolDraft(tool, name, {
|
|
217
|
+
page,
|
|
218
|
+
rect,
|
|
219
|
+
...look
|
|
220
|
+
}));
|
|
221
|
+
const field = form.get({
|
|
222
|
+
kind: "fqn",
|
|
223
|
+
name
|
|
224
|
+
}) ?? (await created).field;
|
|
225
|
+
if (!annotation || interaction.getActiveToolId() !== tool.id) return;
|
|
226
|
+
const policy = annotation.getSettings().afterCreate;
|
|
227
|
+
const widget = field.widgets.find((candidate) => candidate.page?.objectNumber === page.objectNumber);
|
|
228
|
+
if (policy.select && widget?.ref) annotation.selection.set([widget.ref]);
|
|
229
|
+
if (policy.tool === "default") interaction.activateTool("form-edit");
|
|
230
|
+
await created;
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
id: "form-place",
|
|
234
|
+
priority: 95,
|
|
235
|
+
enabledFor: (tool) => tool.enables.has("form-place"),
|
|
236
|
+
onHover: (sample) => {
|
|
237
|
+
if (!annotation) return;
|
|
238
|
+
const toolId = interaction.getActiveToolId();
|
|
239
|
+
if (sample.page && FORM_TOOL_BY_ID.has(toolId) && form.canDesign() && !interaction.hasCursorClaim()) annotation.hoverGhostAt(toolId, sample.page.ref, sample.page.point, sample.page.rotation, sample.page.zoom);
|
|
240
|
+
else annotation.clearGhost();
|
|
241
|
+
},
|
|
242
|
+
onDown: (sample) => {
|
|
243
|
+
if (!sample.page || !FORM_TOOL_BY_ID.has(interaction.getActiveToolId())) return false;
|
|
244
|
+
if (!form.canDesign()) return false;
|
|
245
|
+
origin = {
|
|
246
|
+
page: sample.page.ref,
|
|
247
|
+
start: sample.page.point,
|
|
248
|
+
last: sample.page.point
|
|
249
|
+
};
|
|
250
|
+
return true;
|
|
251
|
+
},
|
|
252
|
+
onMove: (sample) => {
|
|
253
|
+
if (!origin) return;
|
|
254
|
+
const point = (0, _embedpdf_plugin_interaction_contract_host.samplePointOn)(sample, origin.page);
|
|
255
|
+
if (!point) return;
|
|
256
|
+
origin.last = point;
|
|
257
|
+
annotation?.previewPlacement(interaction.getActiveToolId(), origin.page, origin.start, point);
|
|
258
|
+
},
|
|
259
|
+
onUp: (sample) => {
|
|
260
|
+
if (!origin) return;
|
|
261
|
+
const gesture = origin;
|
|
262
|
+
endGesture();
|
|
263
|
+
const tool = FORM_TOOL_BY_ID.get(interaction.getActiveToolId());
|
|
264
|
+
if (!tool) return;
|
|
265
|
+
const end = (0, _embedpdf_plugin_interaction_contract_host.samplePointOn)(sample, gesture.page) ?? gesture.last;
|
|
266
|
+
const placement = (0, _embedpdf_core_annotation.gesturePlacement)("box", gesture.start, end, tool.clickCreate, { pageBox: form.getPageBox(gesture.page) ?? void 0 });
|
|
267
|
+
if (placement?.kind !== "box") return;
|
|
268
|
+
place(tool, gesture.page, placement.rect).catch((error) => {
|
|
269
|
+
globalThis.console?.error("[form] placing the field failed:", error);
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
onCancel: endGesture
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/connect.ts
|
|
277
|
+
/**
|
|
278
|
+
* The form plugin's wiring to its siblings, run once every capability exists:
|
|
279
|
+
* its tools and place handler on the interaction hub, its widgets lent to the
|
|
280
|
+
* annotation plugin for design, and the reset executor, commit sink, form
|
|
281
|
+
* reader and submit resolver on the actions plugin. Every registration is released with
|
|
282
|
+
* the document.
|
|
283
|
+
*/
|
|
284
|
+
function connectForm(ctx, form, lender) {
|
|
285
|
+
const interaction = ctx.get(_embedpdf_plugin_interaction_contract_host.InteractionToken);
|
|
286
|
+
const annotation = ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
|
|
287
|
+
ctx.cleanup(interaction.registerTool({
|
|
288
|
+
id: "form-fill",
|
|
289
|
+
cursor: "default",
|
|
290
|
+
enables: /* @__PURE__ */ new Set(["form-fill", "link-nav"])
|
|
291
|
+
}));
|
|
292
|
+
ctx.cleanup(interaction.registerTool({
|
|
293
|
+
id: "form-edit",
|
|
294
|
+
cursor: "default",
|
|
295
|
+
enables: /* @__PURE__ */ new Set(["annotation-edit", "annotation-marquee"])
|
|
296
|
+
}));
|
|
297
|
+
for (const tool of FORM_TOOLS) ctx.cleanup(annotation ? annotation.tools.register({
|
|
298
|
+
id: tool.id,
|
|
299
|
+
subtype: tool.visualKind,
|
|
300
|
+
cursor: tool.cursor,
|
|
301
|
+
enables: [...PLACE_TAGS],
|
|
302
|
+
clickCreate: tool.clickCreate,
|
|
303
|
+
defaults: tool.defaults,
|
|
304
|
+
ghost: true
|
|
305
|
+
}) : interaction.registerTool({
|
|
306
|
+
id: tool.id,
|
|
307
|
+
cursor: tool.cursor,
|
|
308
|
+
enables: new Set(PLACE_TAGS)
|
|
309
|
+
}));
|
|
310
|
+
ctx.cleanup(interaction.registerHandler(createPlaceHandler(form, interaction, annotation)));
|
|
311
|
+
if (annotation && ctx.allows("doc.forms.modify")) ctx.cleanup(annotation.lend(lender));
|
|
312
|
+
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
|
|
313
|
+
if (!actions) return;
|
|
314
|
+
ctx.cleanup(actions.registerFormCommitSink((effects) => form.commitScriptFormEffects(effects)));
|
|
315
|
+
ctx.cleanup(actions.registerFormReader(() => form.getSnapshot()));
|
|
316
|
+
ctx.cleanup(actions.registerSubmitResolver((intent, actionContext, diagnose) => form.resolveSubmitDataset(intent, actionContext, diagnose)));
|
|
317
|
+
ctx.cleanup(actions.registerExecutor("reset-form", async (node, actionContext) => {
|
|
318
|
+
if (node.type !== "reset-form") return {
|
|
319
|
+
status: "inert",
|
|
320
|
+
reason: "not a reset-form node"
|
|
321
|
+
};
|
|
322
|
+
const result = await form.resetFormAction(node.fields, node.exclude, actionContext.origin);
|
|
323
|
+
if (result.status === "failed") return {
|
|
324
|
+
status: "failed",
|
|
325
|
+
error: result.error?.message ?? "reset failed"
|
|
326
|
+
};
|
|
327
|
+
if (result.status === "unchanged") return {
|
|
328
|
+
status: "inert",
|
|
329
|
+
reason: result.diagnostics[0]?.message ?? "reset-form resolved no fields"
|
|
330
|
+
};
|
|
331
|
+
return { status: "executed" };
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/model.ts
|
|
336
|
+
/**
|
|
337
|
+
* The form plugin's data, all pure:
|
|
338
|
+
*
|
|
339
|
+
* - `FieldIndex` is the form as the engine confirmed it: the field tree and
|
|
340
|
+
* every widget row, indexed by field key and by widget. It is the value of
|
|
341
|
+
* the plugin's `fields` mirror and changes only through `foldFormEvent` and
|
|
342
|
+
* loads; `predictFormOp` (sync/predict.ts) shows this session's changes on
|
|
343
|
+
* top of it.
|
|
344
|
+
* - `PageWidgets` is one page's widgets, where each is and how it looks,
|
|
345
|
+
* derived from the index's widget rows (`pageWidgetsOf`).
|
|
346
|
+
*
|
|
347
|
+
* The text being typed in a field is not here: it waits in `write/typing.ts`
|
|
348
|
+
* until it is committed.
|
|
349
|
+
*/
|
|
350
|
+
const fieldKeyOfRef = (ref) => ref.kind === "objectNumber" ? `obj:${ref.objectNumber}` : `fqn:${ref.name}`;
|
|
351
|
+
/** A field's ref is its object number, or its name when it has none. */
|
|
352
|
+
const fieldKeyOf = (field) => fieldKeyOfRef(field.ref);
|
|
353
|
+
const emptyFieldIndex = () => ({
|
|
354
|
+
snapshot: null,
|
|
355
|
+
byKey: {},
|
|
356
|
+
byName: {},
|
|
357
|
+
byWidget: {},
|
|
358
|
+
rowByWidget: {},
|
|
359
|
+
pictureVersions: {}
|
|
360
|
+
});
|
|
361
|
+
function indexFields(snapshot, pictureVersions = {}) {
|
|
362
|
+
const byKey = {};
|
|
363
|
+
const byName = {};
|
|
364
|
+
const byWidget = {};
|
|
365
|
+
const rowByWidget = {};
|
|
366
|
+
snapshot.fields.forEach((field, position) => {
|
|
367
|
+
var _field$name;
|
|
368
|
+
byKey[fieldKeyOf(field)] = position;
|
|
369
|
+
byName[_field$name = field.name] ?? (byName[_field$name] = position);
|
|
370
|
+
for (const widget of field.widgets) if (widget.objectNumber > 0) byWidget[widget.objectNumber] = position;
|
|
371
|
+
});
|
|
372
|
+
snapshot.widgets.forEach((row, position) => {
|
|
373
|
+
if (row.ref.kind === "objectNumber") rowByWidget[row.ref.objectNumber] = position;
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
snapshot,
|
|
377
|
+
byKey,
|
|
378
|
+
byName,
|
|
379
|
+
byWidget,
|
|
380
|
+
rowByWidget,
|
|
381
|
+
pictureVersions
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** The index of `snapshot`, keeping `index`'s picture versions. */
|
|
385
|
+
const reindex = (index, snapshot) => indexFields(snapshot, index.pictureVersions);
|
|
386
|
+
/**
|
|
387
|
+
* A fresh read of the whole form: every widget's picture version moves past
|
|
388
|
+
* the one it had, so a reload re-fetches every picture (the pictures may
|
|
389
|
+
* have changed while the event stream wasn't trusted). A new widget starts at 0.
|
|
390
|
+
*/
|
|
391
|
+
function indexLoadedFields(previous, snapshot) {
|
|
392
|
+
const pictureVersions = {};
|
|
393
|
+
for (const [number, version] of Object.entries(previous.pictureVersions)) pictureVersions[Number(number)] = version + 1;
|
|
394
|
+
for (const row of snapshot.widgets) {
|
|
395
|
+
const number = widgetNumber$1(row);
|
|
396
|
+
if (number > 0) pictureVersions[number] ?? (pictureVersions[number] = 0);
|
|
397
|
+
}
|
|
398
|
+
return indexFields(snapshot, pictureVersions);
|
|
399
|
+
}
|
|
400
|
+
/** The picture version of a widget; 0 for one the index has never seen. */
|
|
401
|
+
const pictureVersionOf = (index, annotObjectNumber) => index.pictureVersions[annotObjectNumber] ?? 0;
|
|
402
|
+
/** Replace a field by key, or append it when it is new. */
|
|
403
|
+
function upsertFields(index, fields) {
|
|
404
|
+
if (!index.snapshot || fields.length === 0) return index;
|
|
405
|
+
const next = index.snapshot.fields.slice();
|
|
406
|
+
for (const field of fields) {
|
|
407
|
+
const position = index.byKey[fieldKeyOf(field)];
|
|
408
|
+
if (position === void 0) next.push(field);
|
|
409
|
+
else next[position] = field;
|
|
410
|
+
}
|
|
411
|
+
return reindex(index, {
|
|
412
|
+
...index.snapshot,
|
|
413
|
+
fields: next
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const widgetNumber$1 = (widget) => widget.ref.kind === "objectNumber" ? widget.ref.objectNumber : 0;
|
|
417
|
+
/** Replace widget rows by object number, or append the ones that are new. */
|
|
418
|
+
function upsertWidgets(index, rows) {
|
|
419
|
+
if (!index.snapshot || rows.length === 0) return index;
|
|
420
|
+
const incoming = new Map(rows.map((row) => [widgetNumber$1(row), row]));
|
|
421
|
+
const next = index.snapshot.widgets.map((row) => {
|
|
422
|
+
const replaced = incoming.get(widgetNumber$1(row));
|
|
423
|
+
if (replaced) incoming.delete(widgetNumber$1(row));
|
|
424
|
+
return replaced ?? row;
|
|
425
|
+
});
|
|
426
|
+
return reindex(index, {
|
|
427
|
+
...index.snapshot,
|
|
428
|
+
widgets: [...next, ...incoming.values()]
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/** The page's widget rows in `order` (bottom to top), in their slots: other pages' keep theirs. */
|
|
432
|
+
function reorderWidgetRows(index, page, order) {
|
|
433
|
+
if (!index.snapshot) return index;
|
|
434
|
+
const widgets = (0, _embedpdf_engine_core_runtime.reorderPart)(index.snapshot.widgets, (row) => row.page.objectNumber === page.objectNumber, order.map(_embedpdf_engine_core_runtime.annotationKey), (row) => (0, _embedpdf_engine_core_runtime.annotationKey)(row.ref));
|
|
435
|
+
return reindex(index, {
|
|
436
|
+
...index.snapshot,
|
|
437
|
+
widgets
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/** Drop the rows of widgets that went (with their field, or with their page). */
|
|
441
|
+
function removeWidgets(index, gone) {
|
|
442
|
+
if (!index.snapshot || gone.length === 0) return index;
|
|
443
|
+
const numbers = new Set(gone.map((widget) => widget.objectNumber));
|
|
444
|
+
const widgets = index.snapshot.widgets.filter((row) => !numbers.has(widgetNumber$1(row)));
|
|
445
|
+
if (widgets.length === index.snapshot.widgets.length) return index;
|
|
446
|
+
return reindex(index, {
|
|
447
|
+
...index.snapshot,
|
|
448
|
+
widgets
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function removeField(index, ref) {
|
|
452
|
+
if (!index.snapshot) return index;
|
|
453
|
+
const fields = index.snapshot.fields.filter((field) => ref.kind === "objectNumber" ? !(field.ref.kind === "objectNumber" && field.ref.objectNumber === ref.objectNumber) : field.name !== ref.name);
|
|
454
|
+
if (fields.length === index.snapshot.fields.length) return index;
|
|
455
|
+
return reindex(index, {
|
|
456
|
+
...index.snapshot,
|
|
457
|
+
fields
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Apply one confirmed document event to the field index. Every form event
|
|
462
|
+
* carries the fields and the widget rows it touched as the engine read them
|
|
463
|
+
* back, so the index never needs a re-read except for a repair, whose result
|
|
464
|
+
* reports only counts, and for writes that remove widgets with their pages.
|
|
465
|
+
* The widgets the engine drew again take a new picture version.
|
|
466
|
+
*/
|
|
467
|
+
function foldFormEvent(index, event) {
|
|
468
|
+
return foldFormFacts(withRedrawnPictures(index, redrawnWidgetsOf(event)), event);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* The widgets a confirmed fact drew again: the ones the engine says it
|
|
472
|
+
* redrew (`meta.redrawnWidgets`: a checkbox that only switched its state isn't
|
|
473
|
+
* one), else the ones it changed; the signed widget of a signing.
|
|
474
|
+
*/
|
|
475
|
+
function redrawnWidgetsOf(event) {
|
|
476
|
+
if (event.type === "signatures.completed") return event.signature.widget ? [event.signature.widget] : [];
|
|
477
|
+
if (!event.type.startsWith("forms.") || !("meta" in event)) return [];
|
|
478
|
+
const meta = event.meta;
|
|
479
|
+
return meta.redrawnWidgets ?? meta.changedWidgets ?? [];
|
|
480
|
+
}
|
|
481
|
+
/** Each widget drawn again takes a new picture version; one named by its page only, all of that page's. */
|
|
482
|
+
function withRedrawnPictures(index, redrawn) {
|
|
483
|
+
if (redrawn.length === 0) return index;
|
|
484
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
485
|
+
for (const widget of redrawn) if (widget.objectNumber) numbers.add(widget.objectNumber);
|
|
486
|
+
else if (widget.page) {
|
|
487
|
+
for (const row of index.snapshot?.widgets ?? []) if (row.page.objectNumber === widget.page.objectNumber) numbers.add(widgetNumber$1(row));
|
|
488
|
+
}
|
|
489
|
+
if (numbers.size === 0) return index;
|
|
490
|
+
const pictureVersions = { ...index.pictureVersions };
|
|
491
|
+
for (const number of numbers) pictureVersions[number] = (pictureVersions[number] ?? 0) + 1;
|
|
492
|
+
return {
|
|
493
|
+
...index,
|
|
494
|
+
pictureVersions
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* A merged field split as it took a second widget: its widget half moved to a new number (one
|
|
499
|
+
* of the event's widgets), and the row at the field's own number is gone with it.
|
|
500
|
+
*/
|
|
501
|
+
function withoutSplitRow(index, field) {
|
|
502
|
+
if (field.ref.kind !== "objectNumber") return index;
|
|
503
|
+
const own = field.ref.objectNumber;
|
|
504
|
+
if (field.widgets.some((widget) => widget.objectNumber === own)) return index;
|
|
505
|
+
const row = widgetRowOf(index, own);
|
|
506
|
+
return row ? removeWidgets(index, [{
|
|
507
|
+
ref: row.ref,
|
|
508
|
+
objectNumber: own,
|
|
509
|
+
page: row.page
|
|
510
|
+
}]) : index;
|
|
511
|
+
}
|
|
512
|
+
function foldFormFacts(index, event) {
|
|
513
|
+
switch (event.type) {
|
|
514
|
+
case "forms.widgetAdded": return withoutSplitRow(upsertWidgets(upsertFields(index, [event.field]), event.widgets), event.field);
|
|
515
|
+
case "forms.valueSet":
|
|
516
|
+
case "forms.widgetRemoved":
|
|
517
|
+
case "forms.restored": return upsertWidgets(upsertFields(index, [event.field]), event.widgets);
|
|
518
|
+
case "forms.updated": return withCalculationOrder(upsertWidgets(upsertFields(index, [event.field]), event.widgets), event.calculationOrder);
|
|
519
|
+
case "forms.created": {
|
|
520
|
+
const created = upsertWidgets(upsertFields(index, [event.field]), event.widgets);
|
|
521
|
+
return withCalculationOrder(created.snapshot?.formKind === "none" ? reindex(created, {
|
|
522
|
+
...created.snapshot,
|
|
523
|
+
formKind: "acroform"
|
|
524
|
+
}) : created, event.calculationOrder);
|
|
525
|
+
}
|
|
526
|
+
case "forms.calculationsReordered": return withCalculationOrder(index, event.calculationOrder);
|
|
527
|
+
case "forms.widgetDeleted": {
|
|
528
|
+
const gone = removeWidgets(index, event.meta.changedWidgets);
|
|
529
|
+
return event.field ? upsertFields(gone, [event.field]) : gone;
|
|
530
|
+
}
|
|
531
|
+
case "forms.widgetRestored": return upsertWidgets(event.field ? upsertFields(index, [event.field]) : index, event.widgets);
|
|
532
|
+
case "forms.widgetUpdated": return upsertWidgets(index, [event.widget]);
|
|
533
|
+
case "forms.widgetsReordered": return reorderWidgetRows(index, event.page, event.order);
|
|
534
|
+
case "forms.deleted": return event.deleted ? withCalculationOrder(removeWidgets(removeField(index, event.deleted), event.meta.changedWidgets), event.calculationOrder) : (0, _embedpdf_core.reload)();
|
|
535
|
+
case "forms.repaired":
|
|
536
|
+
case "pages.deleted":
|
|
537
|
+
case "pages.inserted":
|
|
538
|
+
case "pages.flattened":
|
|
539
|
+
case "annotations.flattened":
|
|
540
|
+
case "redaction.applied": return (0, _embedpdf_core.reload)();
|
|
541
|
+
default: return index;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/** Whether any field has a keystroke, validate, calculate or format script. */
|
|
545
|
+
function formHasScripts(index) {
|
|
546
|
+
return (index.snapshot?.fields ?? []).some(({ actions }) => !!(actions?.keystroke || actions?.validate || actions?.calculate || actions?.format));
|
|
547
|
+
}
|
|
548
|
+
/** The index with the calculation order a write answered, when the write changed it. */
|
|
549
|
+
function withCalculationOrder(index, order) {
|
|
550
|
+
if (!order || !index.snapshot) return index;
|
|
551
|
+
return reindex(index, {
|
|
552
|
+
...index.snapshot,
|
|
553
|
+
calculationOrder: [...order]
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const fieldByKey = (index, key) => {
|
|
557
|
+
const position = index.byKey[key];
|
|
558
|
+
return position === void 0 ? null : index.snapshot?.fields[position] ?? null;
|
|
559
|
+
};
|
|
560
|
+
/** The field a ref names: by object number, or by full name for `toFieldRef(name)`. */
|
|
561
|
+
function fieldByRef(index, ref) {
|
|
562
|
+
const position = ref.kind === "fqn" ? index.byName[ref.name] : index.byKey[fieldKeyOfRef(ref)];
|
|
563
|
+
return position === void 0 ? null : index.snapshot?.fields[position] ?? null;
|
|
564
|
+
}
|
|
565
|
+
/** The key a field is known by, whichever ref names it: a name ref resolves to its field's key. */
|
|
566
|
+
const canonicalKey = (index, ref) => {
|
|
567
|
+
const field = fieldByRef(index, ref);
|
|
568
|
+
return field ? fieldKeyOf(field) : fieldKeyOfRef(ref);
|
|
569
|
+
};
|
|
570
|
+
const fieldForWidget = (index, annotObjectNumber) => {
|
|
571
|
+
const position = index.byWidget[annotObjectNumber];
|
|
572
|
+
return position === void 0 ? null : index.snapshot?.fields[position] ?? null;
|
|
573
|
+
};
|
|
574
|
+
/** The fields whose value entry differs between two indexes (new fields included). */
|
|
575
|
+
function fieldsWithChangedValues(previous, next) {
|
|
576
|
+
return (next.snapshot?.fields ?? []).filter((field) => {
|
|
577
|
+
const before = fieldByKey(previous, fieldKeyOf(field));
|
|
578
|
+
return !before || JSON.stringify(valueEntryOf(before)) !== JSON.stringify(valueEntryOf(field));
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const valueEntryOf = (field) => "valueEntry" in field ? field.valueEntry : null;
|
|
582
|
+
/** Whether a widget shows on screen: its `hidden` and `noView` flags hide it. */
|
|
583
|
+
const shownOnScreen = (row) => !row.hidden && !row.noView;
|
|
584
|
+
/** The widgets one page shows, from the form's widget rows. */
|
|
585
|
+
function pageWidgetsOf(index, pageObjectNumber) {
|
|
586
|
+
const widgets = {};
|
|
587
|
+
for (const row of index.snapshot?.widgets ?? []) {
|
|
588
|
+
if (row.page.objectNumber !== pageObjectNumber || row.ref.kind !== "objectNumber") continue;
|
|
589
|
+
if (!shownOnScreen(row)) continue;
|
|
590
|
+
widgets[row.ref.objectNumber] = {
|
|
591
|
+
box: row.rect,
|
|
592
|
+
look: {
|
|
593
|
+
border: row.color ?? null,
|
|
594
|
+
borderWidth: row.strokeWidth ?? 1,
|
|
595
|
+
borderStyle: row.borderStyle ?? "solid",
|
|
596
|
+
background: row.interiorColor ?? null,
|
|
597
|
+
color: row.fontColor ?? null,
|
|
598
|
+
fontFamily: row.fontFamily ?? null,
|
|
599
|
+
fontSize: row.fontSize ?? null,
|
|
600
|
+
textAlign: row.textAlign ?? "left"
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return widgets;
|
|
605
|
+
}
|
|
606
|
+
const AT_REST = {
|
|
607
|
+
pending: false,
|
|
608
|
+
pictureVersion: 0
|
|
609
|
+
};
|
|
610
|
+
/** The widgets one page shows, in the order the page draws them. */
|
|
611
|
+
function shownWidgetsOf(index, pageObjectNumber, lookOf = () => AT_REST) {
|
|
612
|
+
const shown = [];
|
|
613
|
+
for (const row of index.snapshot?.widgets ?? []) {
|
|
614
|
+
if (row.page.objectNumber !== pageObjectNumber || !shownOnScreen(row)) continue;
|
|
615
|
+
shown.push({
|
|
616
|
+
ref: row.ref,
|
|
617
|
+
rect: row.rect,
|
|
618
|
+
appearanceState: row.appearanceState,
|
|
619
|
+
...lookOf(row)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return shown;
|
|
623
|
+
}
|
|
624
|
+
function widgetRowOf(index, annotObjectNumber) {
|
|
625
|
+
const position = index.rowByWidget[annotObjectNumber];
|
|
626
|
+
return position === void 0 ? null : index.snapshot?.widgets[position] ?? null;
|
|
627
|
+
}
|
|
628
|
+
const boxContains = (box, point) => point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
|
|
629
|
+
/** The widget under a page-space point; nested widgets resolve to the smallest box. */
|
|
630
|
+
function widgetAt(index, widgets, point) {
|
|
631
|
+
if (!widgets) return null;
|
|
632
|
+
let best = null;
|
|
633
|
+
for (const [key, { box }] of Object.entries(widgets)) {
|
|
634
|
+
if (!boxContains(box, point)) continue;
|
|
635
|
+
const annotObjectNumber = Number(key);
|
|
636
|
+
const field = fieldForWidget(index, annotObjectNumber);
|
|
637
|
+
if (!field) continue;
|
|
638
|
+
if (!best || box.width * box.height < best.box.width * best.box.height) best = {
|
|
639
|
+
annotObjectNumber,
|
|
640
|
+
field,
|
|
641
|
+
box
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return best;
|
|
645
|
+
}
|
|
646
|
+
const initialFormState = () => ({});
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/read/fields.ts
|
|
649
|
+
/**
|
|
650
|
+
* Reads over the mirrored field tree: fields, values, the joins between a
|
|
651
|
+
* field and its widgets' rows, the field of the selected widget, and the
|
|
652
|
+
* required-field check.
|
|
653
|
+
*/
|
|
654
|
+
const NO_FIELDS$1 = Object.freeze([]);
|
|
655
|
+
const NOTHING_SELECTED = Object.freeze([]);
|
|
656
|
+
/** A widget address as the annotation object number the field index is keyed by. */
|
|
657
|
+
const widgetObjectOf = (widget) => "kind" in widget ? widget.kind === "objectNumber" ? widget.objectNumber : 0 : widget.objectNumber;
|
|
658
|
+
/** A field's value in the write vocabulary, or null for a valueless or unsupported entry. */
|
|
659
|
+
function valueOf$1(field) {
|
|
660
|
+
const entry = field.valueEntry;
|
|
661
|
+
if (entry.kind === "none" || entry.kind === "unsupported") return null;
|
|
662
|
+
switch (field.family) {
|
|
663
|
+
case "text": return { value: field.value };
|
|
664
|
+
case "checkbox": return { checked: field.checked };
|
|
665
|
+
case "radio": return { value: field.value === "Off" ? null : field.value };
|
|
666
|
+
case "combobox": return { value: field.value === "" ? null : field.value };
|
|
667
|
+
case "listbox": return { selectedValues: [...field.selectedValues] };
|
|
668
|
+
default: return null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** A field's value as plain data: what `exportValues()` gives and `importValues()` takes. */
|
|
672
|
+
function plainValueOf(field) {
|
|
673
|
+
const value = valueOf$1(field);
|
|
674
|
+
if (!value) return void 0;
|
|
675
|
+
if ("checked" in value) return value.checked;
|
|
676
|
+
if ("selectedValues" in value) return value.selectedValues;
|
|
677
|
+
return value.value;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The write a plain value makes for a field of this family, or null when the
|
|
681
|
+
* family can't take it (a list given `true`, a value for a push button).
|
|
682
|
+
*/
|
|
683
|
+
function writeOfPlainValue(field, value) {
|
|
684
|
+
switch (field.family) {
|
|
685
|
+
case "checkbox":
|
|
686
|
+
if (typeof value === "boolean") return { checked: value };
|
|
687
|
+
return typeof value === "string" || value === null ? { value } : null;
|
|
688
|
+
case "text":
|
|
689
|
+
case "radio":
|
|
690
|
+
case "combobox": return typeof value === "string" || value === null ? { value } : null;
|
|
691
|
+
case "listbox":
|
|
692
|
+
if (value === null) return { selectedValues: [] };
|
|
693
|
+
if (typeof value === "string") return { selectedValues: [value] };
|
|
694
|
+
return Array.isArray(value) ? { selectedValues: [...value] } : null;
|
|
695
|
+
default: return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/** Whether a field holds nothing a required check would accept. */
|
|
699
|
+
function isEmpty(field) {
|
|
700
|
+
switch (field.family) {
|
|
701
|
+
case "text":
|
|
702
|
+
case "combobox": return field.value === "";
|
|
703
|
+
case "checkbox": return !field.checked;
|
|
704
|
+
case "radio": return field.value === "Off" || field.value === "";
|
|
705
|
+
case "listbox": return field.selectedValues.length === 0;
|
|
706
|
+
case "signature": return field.valueEntry.kind === "none";
|
|
707
|
+
default: return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function createFieldReads(ctx, { fields, rights, siblings }) {
|
|
711
|
+
const all = () => fields.view().snapshot?.fields ?? NO_FIELDS$1;
|
|
712
|
+
const list = (filter) => {
|
|
713
|
+
if (!filter) return all();
|
|
714
|
+
const page = filter.page === void 0 ? void 0 : ctx.getPage(filter.page)?.ref;
|
|
715
|
+
if (filter.page !== void 0 && !page) return NO_FIELDS$1;
|
|
716
|
+
return all().filter((field) => (!filter.family || field.family === filter.family) && (filter.name === void 0 || field.name === filter.name) && (!page || field.widgets.some((widget) => widget.page && (0, _embedpdf_engine_core_runtime.pageRefsEqual)(widget.page, page))));
|
|
717
|
+
};
|
|
718
|
+
const valueByKey = (0, _embedpdf_core.memoByKey)((key) => {
|
|
719
|
+
const field = fieldByKey(fields.view(), key);
|
|
720
|
+
return [field ? JSON.stringify(valueOf$1(field)) : null];
|
|
721
|
+
}, (_key, value) => value === null ? null : JSON.parse(value), { maxEntries: 1024 });
|
|
722
|
+
const widgetsByKey = (0, _embedpdf_core.memoByKey)((key) => {
|
|
723
|
+
const index = fields.view();
|
|
724
|
+
return (fieldByKey(index, key)?.widgets ?? []).map((widget) => widgetRowOf(index, widget.objectNumber));
|
|
725
|
+
}, (_key, ...rows) => Object.freeze(rows.filter((row) => row !== null)), { maxEntries: 1024 });
|
|
726
|
+
const exportValues = (0, _embedpdf_core.memo)(() => [fields.view()], (index) => {
|
|
727
|
+
const values = {};
|
|
728
|
+
for (const field of index.snapshot?.fields ?? []) {
|
|
729
|
+
const value = plainValueOf(field);
|
|
730
|
+
if (value !== void 0) values[field.name] = value;
|
|
731
|
+
}
|
|
732
|
+
return values;
|
|
733
|
+
});
|
|
734
|
+
/** Where a field first shows: its page's index, then top to bottom, then left to right. */
|
|
735
|
+
const placeOf = (field) => {
|
|
736
|
+
const index = fields.view();
|
|
737
|
+
const row = field.widgets.map((widget) => widgetRowOf(index, widget.objectNumber)).find((candidate) => candidate !== null);
|
|
738
|
+
return [
|
|
739
|
+
row ? ctx.getPage(row.page)?.index ?? Infinity : Infinity,
|
|
740
|
+
row?.rect.y ?? 0,
|
|
741
|
+
row?.rect.x ?? 0
|
|
742
|
+
];
|
|
743
|
+
};
|
|
744
|
+
const validate = (0, _embedpdf_core.memo)(() => [
|
|
745
|
+
fields.view(),
|
|
746
|
+
ctx.document(),
|
|
747
|
+
rights.key()
|
|
748
|
+
], (index) => {
|
|
749
|
+
const missing = (index.snapshot?.fields ?? []).filter((field) => field.required && isEmpty(field) && rights.mayFill(field)).map((field) => ({
|
|
750
|
+
field,
|
|
751
|
+
place: placeOf(field)
|
|
752
|
+
})).sort((left, right) => left.place[0] - right.place[0] || left.place[1] - right.place[1] || left.place[2] - right.place[2]).map(({ field }) => field);
|
|
753
|
+
return {
|
|
754
|
+
valid: missing.length === 0,
|
|
755
|
+
missing
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
const getSelectedField = (0, _embedpdf_core.memo)(() => [fields.view(), siblings.annotation()?.selection.list() ?? NOTHING_SELECTED], (index, selected) => {
|
|
759
|
+
const [annotation] = selected;
|
|
760
|
+
if (selected.length !== 1 || !annotation?.subtype.startsWith("widget")) return null;
|
|
761
|
+
return fieldForWidget(index, widgetObjectOf(annotation.ref));
|
|
762
|
+
});
|
|
763
|
+
return { api: {
|
|
764
|
+
getSnapshot: () => fields.view().snapshot,
|
|
765
|
+
getStatus: fields.getStatus,
|
|
766
|
+
getFormKind: () => fields.view().snapshot?.formKind ?? "none",
|
|
767
|
+
refresh: (options) => ctx.cancellable(options?.signal, fields.refresh()),
|
|
768
|
+
get: (ref) => fieldByRef(fields.view(), ref),
|
|
769
|
+
getFieldForWidget: (widget) => fieldForWidget(fields.view(), widgetObjectOf(widget)),
|
|
770
|
+
getWidget: (widget) => widgetRowOf(fields.view(), widgetObjectOf(widget)),
|
|
771
|
+
getWidgets: (ref) => widgetsByKey(canonicalKey(fields.view(), ref)),
|
|
772
|
+
list,
|
|
773
|
+
getValue: (ref) => valueByKey(canonicalKey(fields.view(), ref)),
|
|
774
|
+
getSelectedField,
|
|
775
|
+
validate,
|
|
776
|
+
exportValues
|
|
777
|
+
} };
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/read/settings.ts
|
|
781
|
+
/**
|
|
782
|
+
* A field's settings, described for a settings panel the way an annotation
|
|
783
|
+
* kind describes its look: which settings the field's family has, how each is
|
|
784
|
+
* edited, and its value. `update()` writes them.
|
|
785
|
+
*/
|
|
786
|
+
/** What every field has. */
|
|
787
|
+
const COMMON = [
|
|
788
|
+
{
|
|
789
|
+
key: "name",
|
|
790
|
+
control: "text",
|
|
791
|
+
label: "Name",
|
|
792
|
+
labelKey: "form.settings.name"
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
key: "alternateName",
|
|
796
|
+
control: "text",
|
|
797
|
+
label: "Tooltip",
|
|
798
|
+
labelKey: "form.settings.tooltip"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
key: "required",
|
|
802
|
+
control: "flag",
|
|
803
|
+
label: "Required",
|
|
804
|
+
labelKey: "form.settings.required"
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
key: "readOnly",
|
|
808
|
+
control: "flag",
|
|
809
|
+
label: "Read-only",
|
|
810
|
+
labelKey: "form.settings.readOnly"
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
key: "noExport",
|
|
814
|
+
control: "flag",
|
|
815
|
+
label: "Don't export",
|
|
816
|
+
labelKey: "form.settings.noExport"
|
|
817
|
+
}
|
|
818
|
+
];
|
|
819
|
+
const DEFAULT_VALUE = {
|
|
820
|
+
label: "Default value",
|
|
821
|
+
labelKey: "form.settings.defaultValue"
|
|
822
|
+
};
|
|
823
|
+
const OPTIONS = {
|
|
824
|
+
key: "options",
|
|
825
|
+
control: "options",
|
|
826
|
+
label: "Options",
|
|
827
|
+
labelKey: "form.settings.options"
|
|
828
|
+
};
|
|
829
|
+
/** What each family adds. */
|
|
830
|
+
function familySettings(field) {
|
|
831
|
+
switch (field.family) {
|
|
832
|
+
case "text": return [
|
|
833
|
+
{
|
|
834
|
+
key: "defaultValue",
|
|
835
|
+
control: "text",
|
|
836
|
+
...DEFAULT_VALUE
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
key: "maxLength",
|
|
840
|
+
control: "number",
|
|
841
|
+
label: "Maximum length",
|
|
842
|
+
labelKey: "form.settings.maxLength",
|
|
843
|
+
min: 1
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
key: "multiline",
|
|
847
|
+
control: "flag",
|
|
848
|
+
label: "Several lines",
|
|
849
|
+
labelKey: "form.settings.multiline"
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
key: "password",
|
|
853
|
+
control: "flag",
|
|
854
|
+
label: "Password",
|
|
855
|
+
labelKey: "form.settings.password"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
key: "comb",
|
|
859
|
+
control: "flag",
|
|
860
|
+
label: "One character per box",
|
|
861
|
+
labelKey: "form.settings.comb"
|
|
862
|
+
}
|
|
863
|
+
];
|
|
864
|
+
case "radio": return [{
|
|
865
|
+
key: "radiosInUnison",
|
|
866
|
+
control: "flag",
|
|
867
|
+
label: "Buttons with one value turn on together",
|
|
868
|
+
labelKey: "form.settings.radiosInUnison"
|
|
869
|
+
}, {
|
|
870
|
+
key: "noToggleToOff",
|
|
871
|
+
control: "flag",
|
|
872
|
+
label: "Keep one on",
|
|
873
|
+
labelKey: "form.settings.keepOneOn"
|
|
874
|
+
}];
|
|
875
|
+
case "combobox": return [
|
|
876
|
+
OPTIONS,
|
|
877
|
+
{
|
|
878
|
+
key: "edit",
|
|
879
|
+
control: "flag",
|
|
880
|
+
label: "Allow typing",
|
|
881
|
+
labelKey: "form.settings.allowTyping"
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
key: "defaultValue",
|
|
885
|
+
control: "choice",
|
|
886
|
+
...DEFAULT_VALUE,
|
|
887
|
+
options: field.options.map((option) => option.value),
|
|
888
|
+
free: field.edit
|
|
889
|
+
}
|
|
890
|
+
];
|
|
891
|
+
case "listbox": return [
|
|
892
|
+
OPTIONS,
|
|
893
|
+
{
|
|
894
|
+
key: "multiSelect",
|
|
895
|
+
control: "flag",
|
|
896
|
+
label: "Several choices",
|
|
897
|
+
labelKey: "form.settings.multiSelect"
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
key: "defaultValue",
|
|
901
|
+
control: "choices",
|
|
902
|
+
...DEFAULT_VALUE,
|
|
903
|
+
options: field.options.map((option) => option.value)
|
|
904
|
+
}
|
|
905
|
+
];
|
|
906
|
+
default: return [];
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/** Each setting's value, as the field holds it. */
|
|
910
|
+
function valuesOf(field, settings) {
|
|
911
|
+
const held = field;
|
|
912
|
+
const values = {};
|
|
913
|
+
for (const { key } of settings) if (key === "name") values.name = field.name.slice(field.name.lastIndexOf(".") + 1);
|
|
914
|
+
else if (key === "alternateName") values.alternateName = field.alternateName ?? "";
|
|
915
|
+
else values[key] = held[key] ?? null;
|
|
916
|
+
return values;
|
|
917
|
+
}
|
|
918
|
+
function createFieldSettings({ fields }) {
|
|
919
|
+
const settingsByKey = (0, _embedpdf_core.memoByKey)((key) => [fieldByKey(fields.view(), key)], (_key, field) => {
|
|
920
|
+
if (!field) return null;
|
|
921
|
+
const properties = [...COMMON, ...familySettings(field)];
|
|
922
|
+
return {
|
|
923
|
+
properties,
|
|
924
|
+
values: valuesOf(field, properties)
|
|
925
|
+
};
|
|
926
|
+
}, { maxEntries: 256 });
|
|
927
|
+
return { api: { getFieldSettings: (ref) => settingsByKey(canonicalKey(fields.view(), ref)) } };
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/read/fill-items.ts
|
|
931
|
+
/**
|
|
932
|
+
* Project one widget of a field into a fill control; `fillable`: the user may
|
|
933
|
+
* fill the field in (a signature field: sign it). Null for families with no
|
|
934
|
+
* fill control.
|
|
935
|
+
*/
|
|
936
|
+
function projectWidget(field, annotObjectNumber, widget, fillable) {
|
|
937
|
+
const base = {
|
|
938
|
+
key: fieldKeyOf(field),
|
|
939
|
+
fieldRef: field.ref,
|
|
940
|
+
annotObjectNumber,
|
|
941
|
+
annotationRef: field.widgets.find((candidate) => candidate.objectNumber === annotObjectNumber)?.ref ?? null,
|
|
942
|
+
box: widget.box,
|
|
943
|
+
look: widget.look,
|
|
944
|
+
disabled: field.readOnly || !fillable,
|
|
945
|
+
label: field.alternateName ?? field.name
|
|
946
|
+
};
|
|
947
|
+
switch (field.family) {
|
|
948
|
+
case "text": return {
|
|
949
|
+
...base,
|
|
950
|
+
control: "text",
|
|
951
|
+
value: field.value,
|
|
952
|
+
multiline: field.multiline,
|
|
953
|
+
password: field.password,
|
|
954
|
+
maxLength: field.maxLength,
|
|
955
|
+
comb: field.comb
|
|
956
|
+
};
|
|
957
|
+
case "checkbox": {
|
|
958
|
+
const toggle = field.widgets.find((candidate) => candidate.objectNumber === annotObjectNumber);
|
|
959
|
+
return {
|
|
960
|
+
...base,
|
|
961
|
+
control: "toggle",
|
|
962
|
+
kind: "checkbox",
|
|
963
|
+
checked: field.checked,
|
|
964
|
+
exportValue: toggle && "exportValue" in toggle ? toggle.exportValue : field.exportValue
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
case "radio": {
|
|
968
|
+
const toggle = field.widgets.find((candidate) => candidate.objectNumber === annotObjectNumber);
|
|
969
|
+
return {
|
|
970
|
+
...base,
|
|
971
|
+
control: "toggle",
|
|
972
|
+
kind: "radio",
|
|
973
|
+
checked: toggle && "checked" in toggle ? toggle.checked : false,
|
|
974
|
+
exportValue: toggle && "exportValue" in toggle ? toggle.exportValue : ""
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
case "combobox": return {
|
|
978
|
+
...base,
|
|
979
|
+
control: "choice",
|
|
980
|
+
kind: "combo",
|
|
981
|
+
edit: field.edit,
|
|
982
|
+
multi: false,
|
|
983
|
+
options: field.options,
|
|
984
|
+
selected: field.value === "" ? [] : [field.value]
|
|
985
|
+
};
|
|
986
|
+
case "listbox": return {
|
|
987
|
+
...base,
|
|
988
|
+
control: "choice",
|
|
989
|
+
kind: "list",
|
|
990
|
+
edit: false,
|
|
991
|
+
multi: field.multiSelect,
|
|
992
|
+
options: field.options,
|
|
993
|
+
selected: field.selectedValues
|
|
994
|
+
};
|
|
995
|
+
case "pushbutton": return {
|
|
996
|
+
...base,
|
|
997
|
+
control: "button"
|
|
998
|
+
};
|
|
999
|
+
case "signature": return {
|
|
1000
|
+
...base,
|
|
1001
|
+
control: "signature",
|
|
1002
|
+
signed: field.valueEntry.kind !== "none"
|
|
1003
|
+
};
|
|
1004
|
+
default: return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Project one page's widgets into fill controls, each disabled when the user
|
|
1009
|
+
* may not fill its field in, or sign it (`mayFill`). Widgets whose page has not loaded (or
|
|
1010
|
+
* that are direct objects with no join key) are skipped; the projection runs
|
|
1011
|
+
* again when the page's widgets land.
|
|
1012
|
+
*/
|
|
1013
|
+
function fillItems(index, pageObjectNumber, widgets, mayFill) {
|
|
1014
|
+
if (!index.snapshot || !widgets) return [];
|
|
1015
|
+
const items = [];
|
|
1016
|
+
for (const field of index.snapshot.fields) for (const widget of field.widgets) {
|
|
1017
|
+
if (widget.page?.objectNumber !== pageObjectNumber) continue;
|
|
1018
|
+
const placed = widgets[widget.objectNumber];
|
|
1019
|
+
if (!placed) continue;
|
|
1020
|
+
const item = projectWidget(field, widget.objectNumber, placed, mayFill(field));
|
|
1021
|
+
if (item) items.push(item);
|
|
1022
|
+
}
|
|
1023
|
+
return items;
|
|
1024
|
+
}
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/read/widgets.ts
|
|
1027
|
+
/**
|
|
1028
|
+
* The widgets' reads: each page's widgets (from the form's widget rows), the
|
|
1029
|
+
* ones it shows for the form layer to paint (and the painter's scene of each,
|
|
1030
|
+
* `services/looks.ts`), the fill projection with the session's fill permission
|
|
1031
|
+
* folded in, and the widget hit test.
|
|
1032
|
+
*/
|
|
1033
|
+
const NO_WIDGETS = Object.freeze([]);
|
|
1034
|
+
/** Whether the `fillable` setting lets people fill in a field of this family by hand. */
|
|
1035
|
+
const isFillableByHand = (fillable, family) => typeof fillable === "boolean" ? fillable : fillable.includes(family);
|
|
1036
|
+
const NOTHING_SHOWN$1 = Object.freeze([]);
|
|
1037
|
+
function createWidgetReads(ctx, { fields, rights, looks, pictures }) {
|
|
1038
|
+
/** Every page's widgets come with the form: one read for the document. */
|
|
1039
|
+
const loadWidgets = (_page) => fields.settled();
|
|
1040
|
+
const widgetsOn = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => [fields.view()], (pageObjectNumber, index) => pageWidgetsOf(index, pageObjectNumber));
|
|
1041
|
+
/** The widget under a page point. */
|
|
1042
|
+
const getWidgetAt = (pageArgument, point) => {
|
|
1043
|
+
const page = ctx.getPage(pageArgument)?.ref;
|
|
1044
|
+
if (!page) return null;
|
|
1045
|
+
return widgetAt(fields.view(), widgetsOn(page.objectNumber), point);
|
|
1046
|
+
};
|
|
1047
|
+
const fillable = () => ctx.settings().get().fillable;
|
|
1048
|
+
const mayWrite = (field) => isFillableByHand(fillable(), field.family) && (field.family === "signature" ? rights.maySign(field) : rights.mayFill(field));
|
|
1049
|
+
const widgetsOfPage = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => [
|
|
1050
|
+
fields.view(),
|
|
1051
|
+
widgetsOn(pageObjectNumber),
|
|
1052
|
+
rights.key(),
|
|
1053
|
+
fillable()
|
|
1054
|
+
], (pageObjectNumber, index, widgets) => fillItems(index, pageObjectNumber, widgets, mayWrite));
|
|
1055
|
+
/** The page's content box (`{0, 0, width, height}`), for page-bound placement. */
|
|
1056
|
+
const getPageBox = (page) => {
|
|
1057
|
+
const size = ctx.getPage(page)?.size;
|
|
1058
|
+
return size ? {
|
|
1059
|
+
x: 0,
|
|
1060
|
+
y: 0,
|
|
1061
|
+
width: size.width,
|
|
1062
|
+
height: size.height
|
|
1063
|
+
} : null;
|
|
1064
|
+
};
|
|
1065
|
+
return {
|
|
1066
|
+
getPageBox,
|
|
1067
|
+
api: {
|
|
1068
|
+
listWidgets: (pageArgument) => {
|
|
1069
|
+
const page = ctx.getPage(pageArgument)?.ref;
|
|
1070
|
+
return page ? widgetsOfPage(page.objectNumber) : NO_WIDGETS;
|
|
1071
|
+
},
|
|
1072
|
+
listShownWidgets: (pageArgument) => {
|
|
1073
|
+
const page = ctx.getPage(pageArgument)?.ref;
|
|
1074
|
+
return page ? looks.listShownWidgets(page.objectNumber) : NOTHING_SHOWN$1;
|
|
1075
|
+
},
|
|
1076
|
+
getAppearanceEpoch: pictures.epoch,
|
|
1077
|
+
renderAppearances: pictures.render,
|
|
1078
|
+
getWidgetPainting: looks.getWidgetPainting,
|
|
1079
|
+
getTextEditorLayout: looks.getTextEditorLayout,
|
|
1080
|
+
ensureLoaded: loadWidgets,
|
|
1081
|
+
getWidgetAt,
|
|
1082
|
+
getPageBox
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/sync/predict.ts
|
|
1088
|
+
const widgetNumber = (widget) => widget.ref.kind === "objectNumber" ? widget.ref.objectNumber : 0;
|
|
1089
|
+
/** The rows of a field's widgets, in its widget order. */
|
|
1090
|
+
function rowsOfField(index, field) {
|
|
1091
|
+
return field.widgets.flatMap((widget) => {
|
|
1092
|
+
const row = widgetRowOf(index, widget.objectNumber);
|
|
1093
|
+
return row ? [row] : [];
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
/** A field and the rows of its widgets, as the index holds them. */
|
|
1097
|
+
const withRows = (index, field) => ({
|
|
1098
|
+
field,
|
|
1099
|
+
widgets: rowsOfField(index, field)
|
|
1100
|
+
});
|
|
1101
|
+
/** Fields and their widget rows put into the index, all at once. */
|
|
1102
|
+
function withFields(index, parts) {
|
|
1103
|
+
if (parts.length === 0) return index;
|
|
1104
|
+
return upsertWidgets(upsertFields(index, parts.map((part) => part.field)), parts.flatMap((part) => part.widgets));
|
|
1105
|
+
}
|
|
1106
|
+
/** The fields a reset of the whole form puts back: the ones that hold a value. */
|
|
1107
|
+
const resetTargets = (index) => (index.snapshot?.fields ?? []).filter((field) => RESET_FAMILIES.has(field.family));
|
|
1108
|
+
const RESET_FAMILIES = /* @__PURE__ */ new Set([
|
|
1109
|
+
"text",
|
|
1110
|
+
"checkbox",
|
|
1111
|
+
"radio",
|
|
1112
|
+
"combobox",
|
|
1113
|
+
"listbox"
|
|
1114
|
+
]);
|
|
1115
|
+
/**
|
|
1116
|
+
* One op of a change this session staged, shown before the engine answers,
|
|
1117
|
+
* by the engine's own rules (`fieldAfterValue`, `fieldAfterReset`,
|
|
1118
|
+
* `widgetsAfterDisplay`, `formFieldOfDraft`, `formFieldAfter`,
|
|
1119
|
+
* `widgetAfterPatch`): what the fields and their widgets hold after it. An op
|
|
1120
|
+
* those rules refuse shows nothing; one whose values only the engine's schemas
|
|
1121
|
+
* refuse shows until the engine's refusal takes the change away.
|
|
1122
|
+
* A create shows once it names its numbers; taking a widget out of its field
|
|
1123
|
+
* shows once the engine answers.
|
|
1124
|
+
*
|
|
1125
|
+
* The view replays every pending change each time any change is staged; the
|
|
1126
|
+
* same op over the same form gives the same form, so a field keeps its object
|
|
1127
|
+
* while nothing about it changed.
|
|
1128
|
+
*/
|
|
1129
|
+
function predictFormOp(index, op) {
|
|
1130
|
+
let byOp = predictions.get(index);
|
|
1131
|
+
const known = byOp?.get(op);
|
|
1132
|
+
if (known) return known;
|
|
1133
|
+
const next = predictOnce(index, op);
|
|
1134
|
+
if (!byOp) predictions.set(index, byOp = /* @__PURE__ */ new WeakMap());
|
|
1135
|
+
byOp.set(op, next);
|
|
1136
|
+
return next;
|
|
1137
|
+
}
|
|
1138
|
+
const predictions = /* @__PURE__ */ new WeakMap();
|
|
1139
|
+
function predictOnce(index, op) {
|
|
1140
|
+
try {
|
|
1141
|
+
switch (op.type) {
|
|
1142
|
+
case "forms.setValue": {
|
|
1143
|
+
const field = fieldByRef(index, op.field);
|
|
1144
|
+
return field ? withFields(index, [(0, _embedpdf_engine_core_runtime.fieldAfterValue)(withRows(index, field), op.value)]) : index;
|
|
1145
|
+
}
|
|
1146
|
+
case "forms.reset": return withFields(index, (op.fields ? op.fields.flatMap((ref) => fieldByRef(index, ref) ?? []) : resetTargets(index)).map((field) => (0, _embedpdf_engine_core_runtime.fieldAfterReset)(withRows(index, field))));
|
|
1147
|
+
case "forms.setDisplay": {
|
|
1148
|
+
const field = fieldByRef(index, op.field);
|
|
1149
|
+
if (!field) return index;
|
|
1150
|
+
return upsertWidgets(index, (0, _embedpdf_engine_core_runtime.widgetsAfterDisplay)(rowsOfField(index, field), op.display));
|
|
1151
|
+
}
|
|
1152
|
+
case "forms.restore": return withFields(index, [op]);
|
|
1153
|
+
case "forms.restoreWidget": {
|
|
1154
|
+
const restored = upsertWidgets(index, [op.widget]);
|
|
1155
|
+
return op.field ? upsertFields(restored, [op.field]) : restored;
|
|
1156
|
+
}
|
|
1157
|
+
case "forms.create": return createdField(index, op);
|
|
1158
|
+
case "forms.update": {
|
|
1159
|
+
const field = fieldByRef(index, op.field);
|
|
1160
|
+
return field ? withFields(index, [(0, _embedpdf_engine_core_runtime.formFieldAfter)(withRows(index, field), op.patch)]) : index;
|
|
1161
|
+
}
|
|
1162
|
+
case "forms.delete": {
|
|
1163
|
+
const field = fieldByRef(index, op.field);
|
|
1164
|
+
if (!field || !index.snapshot) return index;
|
|
1165
|
+
return withCalculationOrder(removeWidgets(removeField(index, field.ref), field.widgets), index.snapshot.calculationOrder.filter((ref) => !sameField(ref, field.ref)));
|
|
1166
|
+
}
|
|
1167
|
+
case "forms.updateWidget": {
|
|
1168
|
+
const row = rowOfRef(index, op.widget);
|
|
1169
|
+
return row ? upsertWidgets(index, [(0, _embedpdf_engine_core_runtime.widgetAfterPatch)(row, op.patch)]) : index;
|
|
1170
|
+
}
|
|
1171
|
+
case "forms.deleteWidget": {
|
|
1172
|
+
const row = rowOfRef(index, op.widget);
|
|
1173
|
+
if (!row) return index;
|
|
1174
|
+
const number = widgetNumber(row);
|
|
1175
|
+
const field = fieldForWidget(index, number);
|
|
1176
|
+
const gone = removeWidgets(index, [{
|
|
1177
|
+
objectNumber: number,
|
|
1178
|
+
page: row.page
|
|
1179
|
+
}]);
|
|
1180
|
+
if (!field) return gone;
|
|
1181
|
+
const widgets = field.widgets.filter((widget) => widget.objectNumber !== number);
|
|
1182
|
+
return upsertFields(gone, [{
|
|
1183
|
+
...field,
|
|
1184
|
+
widgets
|
|
1185
|
+
}]);
|
|
1186
|
+
}
|
|
1187
|
+
case "forms.reorderWidgets": {
|
|
1188
|
+
if (!index.snapshot) return index;
|
|
1189
|
+
const order = (0, _embedpdf_engine_core_runtime.reorderedList)(index.snapshot.widgets.filter((row) => row.page.objectNumber === op.page.objectNumber).map((row) => row.ref), op.widgets, op.position, _embedpdf_engine_core_runtime.annotationKey);
|
|
1190
|
+
return reorderWidgetRows(index, op.page, order);
|
|
1191
|
+
}
|
|
1192
|
+
case "forms.reorderCalculations":
|
|
1193
|
+
if (!index.snapshot) return index;
|
|
1194
|
+
return withCalculationOrder(index, (0, _embedpdf_engine_core_runtime.reorderedList)(index.snapshot.calculationOrder.filter((ref) => ref !== null), op.fields, op.position, fieldKeyOfRef));
|
|
1195
|
+
default: return index;
|
|
1196
|
+
}
|
|
1197
|
+
} catch {
|
|
1198
|
+
return index;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/** A widget's row, by its annotation ref. */
|
|
1202
|
+
const rowOfRef = (index, ref) => ref.kind === "objectNumber" ? widgetRowOf(index, ref.objectNumber) : null;
|
|
1203
|
+
const sameField = (left, right) => left !== null && fieldKeyOfRef(left) === fieldKeyOfRef(right);
|
|
1204
|
+
/**
|
|
1205
|
+
* `forms.create`, once it names the numbers it takes: the field and its
|
|
1206
|
+
* widgets as a read of them returns them, last in the form, and last in the
|
|
1207
|
+
* calculation order when it calculates.
|
|
1208
|
+
*/
|
|
1209
|
+
function createdField(index, op) {
|
|
1210
|
+
const placements = op.draft.widgets ?? [];
|
|
1211
|
+
const numbers = op.widgetObjectNumbers ?? [];
|
|
1212
|
+
if (op.objectNumber === void 0 || numbers.length !== placements.length) return index;
|
|
1213
|
+
const snapshot = index.snapshot;
|
|
1214
|
+
if (!snapshot || fieldByRef(index, {
|
|
1215
|
+
kind: "objectNumber",
|
|
1216
|
+
objectNumber: op.objectNumber
|
|
1217
|
+
})) return index;
|
|
1218
|
+
const created = (0, _embedpdf_engine_core_runtime.formFieldOfDraft)(op.draft, {
|
|
1219
|
+
objectNumber: op.objectNumber,
|
|
1220
|
+
widgets: placements.map((placement, at) => ({
|
|
1221
|
+
kind: "objectNumber",
|
|
1222
|
+
page: placement.page,
|
|
1223
|
+
objectNumber: numbers[at]
|
|
1224
|
+
}))
|
|
1225
|
+
});
|
|
1226
|
+
const next = withFields(snapshot.formKind === "none" ? indexFields({
|
|
1227
|
+
...snapshot,
|
|
1228
|
+
formKind: "acroform"
|
|
1229
|
+
}, index.pictureVersions) : index, [created]);
|
|
1230
|
+
return op.draft.actions?.calculate ? withCalculationOrder(next, [...snapshot.calculationOrder, created.field.ref]) : next;
|
|
1231
|
+
}
|
|
1232
|
+
/** The fields a change's ops write a value or a look to, each once, in op order. */
|
|
1233
|
+
function fieldsNamedBy(index, ops) {
|
|
1234
|
+
const named = /* @__PURE__ */ new Map();
|
|
1235
|
+
const add = (field) => {
|
|
1236
|
+
if (field && !named.has(fieldKeyOf(field))) named.set(fieldKeyOf(field), field);
|
|
1237
|
+
};
|
|
1238
|
+
for (const op of ops) switch (op.type) {
|
|
1239
|
+
case "forms.setValue":
|
|
1240
|
+
case "forms.setDisplay":
|
|
1241
|
+
case "forms.setAppearanceText":
|
|
1242
|
+
add(fieldByRef(index, op.field));
|
|
1243
|
+
break;
|
|
1244
|
+
case "forms.reset":
|
|
1245
|
+
for (const field of op.fields ? op.fields.map((ref) => fieldByRef(index, ref)) : resetTargets(index)) add(field);
|
|
1246
|
+
break;
|
|
1247
|
+
default: break;
|
|
1248
|
+
}
|
|
1249
|
+
return [...named.values()];
|
|
1250
|
+
}
|
|
1251
|
+
/** `index` with these fields, and their widget rows, as `source` holds them. */
|
|
1252
|
+
function withFieldsFrom(index, source, fields) {
|
|
1253
|
+
return withFields(index, fields.flatMap((field) => {
|
|
1254
|
+
const held = fieldByKey(source, fieldKeyOf(field));
|
|
1255
|
+
return held ? [withRows(source, held)] : [];
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
/** A field and its widgets back as `index` holds them. */
|
|
1259
|
+
const restoreOf = (index, field) => ({
|
|
1260
|
+
type: "forms.restore",
|
|
1261
|
+
field,
|
|
1262
|
+
widgets: rowsOfField(index, field)
|
|
1263
|
+
});
|
|
1264
|
+
/** A widget's row back as `index` holds it, with its field as it is there. */
|
|
1265
|
+
const restoreWidgetOf = (index, row) => ({
|
|
1266
|
+
type: "forms.restoreWidget",
|
|
1267
|
+
widget: row,
|
|
1268
|
+
field: fieldForWidget(index, widgetNumber(row))
|
|
1269
|
+
});
|
|
1270
|
+
/** What undoing one op looks like, over the form it applies to. */
|
|
1271
|
+
function undoOfOp(index, op) {
|
|
1272
|
+
switch (op.type) {
|
|
1273
|
+
case "forms.create": return op.objectNumber === void 0 ? [] : [{
|
|
1274
|
+
type: "forms.delete",
|
|
1275
|
+
field: {
|
|
1276
|
+
kind: "objectNumber",
|
|
1277
|
+
objectNumber: op.objectNumber
|
|
1278
|
+
}
|
|
1279
|
+
}];
|
|
1280
|
+
case "forms.update":
|
|
1281
|
+
case "forms.delete": {
|
|
1282
|
+
const field = fieldByRef(index, op.field);
|
|
1283
|
+
return field ? [restoreOf(index, field)] : [];
|
|
1284
|
+
}
|
|
1285
|
+
case "forms.updateWidget":
|
|
1286
|
+
case "forms.deleteWidget": {
|
|
1287
|
+
const row = rowOfRef(index, op.widget);
|
|
1288
|
+
return row ? [restoreWidgetOf(index, row)] : [];
|
|
1289
|
+
}
|
|
1290
|
+
case "forms.removeWidget": {
|
|
1291
|
+
const field = fieldByRef(index, op.field);
|
|
1292
|
+
const row = rowOfRef(index, op.widget);
|
|
1293
|
+
return [...row ? [restoreWidgetOf(index, row)] : [], ...field ? [restoreOf(index, field)] : []];
|
|
1294
|
+
}
|
|
1295
|
+
case "forms.reorderWidgets": {
|
|
1296
|
+
const widgets = (index.snapshot?.widgets ?? []).filter((row) => row.page.objectNumber === op.page.objectNumber).map((row) => row.ref);
|
|
1297
|
+
return [{
|
|
1298
|
+
type: "forms.reorderWidgets",
|
|
1299
|
+
page: op.page,
|
|
1300
|
+
widgets,
|
|
1301
|
+
position: "start"
|
|
1302
|
+
}];
|
|
1303
|
+
}
|
|
1304
|
+
case "forms.reorderCalculations": return [{
|
|
1305
|
+
type: "forms.reorderCalculations",
|
|
1306
|
+
fields: (index.snapshot?.calculationOrder ?? []).filter((ref) => ref !== null),
|
|
1307
|
+
position: "start"
|
|
1308
|
+
}];
|
|
1309
|
+
default: return fieldsNamedBy(index, [op]).map((field) => restoreOf(index, field));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* What undoing `ops` looks like, read off the form they apply to, each over
|
|
1314
|
+
* what the ones before it left: the ops that bring the form back, the last
|
|
1315
|
+
* op's first. Drawing only: the engine undoes a change by its own record of
|
|
1316
|
+
* it.
|
|
1317
|
+
*/
|
|
1318
|
+
function undoLookOf(before, ops) {
|
|
1319
|
+
const undo = [];
|
|
1320
|
+
let current = before;
|
|
1321
|
+
for (const op of ops) {
|
|
1322
|
+
undo.push(undoOfOp(current, op));
|
|
1323
|
+
current = predictFormOp(current, op);
|
|
1324
|
+
}
|
|
1325
|
+
return undo.reverse().flat();
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/sync/fields.ts
|
|
1329
|
+
function createFieldsMirror(ctx, events) {
|
|
1330
|
+
const fields = ctx.mirror({
|
|
1331
|
+
name: "fields",
|
|
1332
|
+
initial: emptyFieldIndex,
|
|
1333
|
+
readable: () => ctx.allows("doc.forms.read"),
|
|
1334
|
+
load: async (doc) => ({ value: indexLoadedFields(fields.get(), await doc.forms.list()) }),
|
|
1335
|
+
fold: foldFormEvent,
|
|
1336
|
+
predict: predictFormOp,
|
|
1337
|
+
changed: ({ cause, event, next }) => {
|
|
1338
|
+
if (cause === "load") {
|
|
1339
|
+
if (next.snapshot) events.resynced.emit({ snapshot: next.snapshot });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (!event || !("origin" in event)) return;
|
|
1343
|
+
const { origin } = event;
|
|
1344
|
+
switch (event.type) {
|
|
1345
|
+
case "forms.valueSet":
|
|
1346
|
+
events.valueChanged.emit({
|
|
1347
|
+
field: event.field,
|
|
1348
|
+
origin
|
|
1349
|
+
});
|
|
1350
|
+
return;
|
|
1351
|
+
case "forms.created":
|
|
1352
|
+
events.fieldCreated.emit({
|
|
1353
|
+
field: event.field,
|
|
1354
|
+
origin
|
|
1355
|
+
});
|
|
1356
|
+
return;
|
|
1357
|
+
case "forms.updated":
|
|
1358
|
+
case "forms.widgetAdded":
|
|
1359
|
+
case "forms.widgetRemoved":
|
|
1360
|
+
events.fieldUpdated.emit({
|
|
1361
|
+
field: event.field,
|
|
1362
|
+
origin
|
|
1363
|
+
});
|
|
1364
|
+
return;
|
|
1365
|
+
case "forms.widgetDeleted":
|
|
1366
|
+
case "forms.widgetRestored":
|
|
1367
|
+
if (event.field) events.fieldUpdated.emit({
|
|
1368
|
+
field: event.field,
|
|
1369
|
+
origin
|
|
1370
|
+
});
|
|
1371
|
+
return;
|
|
1372
|
+
case "forms.deleted":
|
|
1373
|
+
if (event.deleted) events.fieldDeleted.emit({
|
|
1374
|
+
ref: event.deleted,
|
|
1375
|
+
origin
|
|
1376
|
+
});
|
|
1377
|
+
return;
|
|
1378
|
+
default: return;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
return fields;
|
|
1383
|
+
}
|
|
1384
|
+
//#endregion
|
|
1385
|
+
//#region src/services/design.ts
|
|
1386
|
+
/**
|
|
1387
|
+
* Whether the form's widgets are lent to the annotation plugin now
|
|
1388
|
+
* (write/lending.ts): the session may design, the annotation plugin is there,
|
|
1389
|
+
* and the active tool doesn't fill. Then the annotation layer draws and edits
|
|
1390
|
+
* the widgets, and the form layer leaves them out.
|
|
1391
|
+
*/
|
|
1392
|
+
function createDesign(ctx, siblings) {
|
|
1393
|
+
const interaction = ctx.tryGet(_embedpdf_plugin_interaction_contract_host.InteractionToken);
|
|
1394
|
+
return { lending: () => siblings.annotation() !== null && interaction !== null && ctx.allows("doc.forms.modify") && !(interaction.getActiveTool()?.enables.has("form-fill") ?? false) };
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region src/services/events.ts
|
|
1398
|
+
function createEvents(ctx) {
|
|
1399
|
+
return {
|
|
1400
|
+
valueChanged: ctx.events.source(),
|
|
1401
|
+
fieldCreated: ctx.events.source(),
|
|
1402
|
+
fieldUpdated: ctx.events.source(),
|
|
1403
|
+
fieldDeleted: ctx.events.source(),
|
|
1404
|
+
validationRejected: ctx.events.source(),
|
|
1405
|
+
resynced: ctx.events.source()
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
//#endregion
|
|
1409
|
+
//#region src/services/fill-rights.ts
|
|
1410
|
+
/**
|
|
1411
|
+
* Who may fill in, and sign, which field: the engine's own answer per field
|
|
1412
|
+
* (`ctx.allowsField`), so a control gated on it never disagrees with the
|
|
1413
|
+
* write. A field's group decides: a `fields:fill` (`fields:sign`) permission
|
|
1414
|
+
* for its group, or `doc.forms.fill` (`doc.sign`) for every field.
|
|
1415
|
+
*/
|
|
1416
|
+
function createFillRights(ctx, fields) {
|
|
1417
|
+
const mayFill = (field) => ctx.allowsField("fill", field);
|
|
1418
|
+
const maySign = (field) => ctx.allowsField("sign", field);
|
|
1419
|
+
/** The form's groups, `null` (no group) among them. */
|
|
1420
|
+
const groups = (0, _embedpdf_core.memo)(() => [fields.view()], (index) => [...new Set((index.snapshot?.fields ?? []).map((field) => field.groupId))]);
|
|
1421
|
+
return {
|
|
1422
|
+
mayFill,
|
|
1423
|
+
maySign,
|
|
1424
|
+
assertMayFill: (field, operation) => ctx.assertAllowedField("fill", field, operation),
|
|
1425
|
+
key: () => groups().map((groupId) => `${mayFill({ groupId }) ? 1 : 0}${maySign({ groupId }) ? 1 : 0}`).join("")
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/services/formatted.ts
|
|
1430
|
+
/** The values remembered per field: enough for an undo or two. */
|
|
1431
|
+
const KEPT_PER_FIELD = 8;
|
|
1432
|
+
const valueOf = (field) => field.family === "text" || field.family === "combobox" ? field.value : null;
|
|
1433
|
+
function createFormattedTexts(ctx, fields) {
|
|
1434
|
+
const texts = /* @__PURE__ */ new Map();
|
|
1435
|
+
let version = 0;
|
|
1436
|
+
ctx.listen(ctx.changes.onStaged, (change) => {
|
|
1437
|
+
const view = fields.view();
|
|
1438
|
+
for (const op of change.shows) {
|
|
1439
|
+
if (op.type !== "forms.setAppearanceText") continue;
|
|
1440
|
+
const field = fieldByRef(view, op.field);
|
|
1441
|
+
const value = field ? valueOf(field) : null;
|
|
1442
|
+
if (!field || value === null) continue;
|
|
1443
|
+
const key = fieldKeyOf(field);
|
|
1444
|
+
const kept = texts.get(key) ?? /* @__PURE__ */ new Map();
|
|
1445
|
+
kept.delete(value);
|
|
1446
|
+
kept.set(value, op.text);
|
|
1447
|
+
while (kept.size > KEPT_PER_FIELD) kept.delete(kept.keys().next().value);
|
|
1448
|
+
texts.set(key, kept);
|
|
1449
|
+
version += 1;
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
return {
|
|
1453
|
+
textOf(field) {
|
|
1454
|
+
const value = valueOf(field);
|
|
1455
|
+
return value === null ? null : texts.get(fieldKeyOf(field))?.get(value) ?? null;
|
|
1456
|
+
},
|
|
1457
|
+
version: () => version
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/services/looks.ts
|
|
1462
|
+
/**
|
|
1463
|
+
* What a shown widget looks like now: the engine's picture, or the painter's
|
|
1464
|
+
* drawing of it (`@embedpdf/core-form`).
|
|
1465
|
+
*
|
|
1466
|
+
* A widget's look is everything its picture depends on: its size and turn,
|
|
1467
|
+
* its style, and what it shows (the value, as this session formatted it). Its
|
|
1468
|
+
* place isn't (a move keeps the look), nor the state a toggle shows (its
|
|
1469
|
+
* picture of every state is held). A widget is `pending` while its look in
|
|
1470
|
+
* the view isn't its confirmed look: a value typed or calculated, a restyle,
|
|
1471
|
+
* a field just placed. The layer then draws the painter's scene, as it does
|
|
1472
|
+
* while the picture it holds is older than the widget's look.
|
|
1473
|
+
*/
|
|
1474
|
+
const NOTHING_SHOWN = Object.freeze([]);
|
|
1475
|
+
const numberOf = (ref) => ref.kind === "objectNumber" ? ref.objectNumber : -1;
|
|
1476
|
+
function createWidgetLooks({ fields, formatted, design }) {
|
|
1477
|
+
/** What the widget's picture depends on, as one string. */
|
|
1478
|
+
const lookKeyOf = (row, field) => {
|
|
1479
|
+
const box = row.box ?? row.rect;
|
|
1480
|
+
const content = (() => {
|
|
1481
|
+
switch (field?.family) {
|
|
1482
|
+
case "text": return [
|
|
1483
|
+
formatted.textOf(field) ?? field.value,
|
|
1484
|
+
field.multiline,
|
|
1485
|
+
field.password,
|
|
1486
|
+
field.comb,
|
|
1487
|
+
field.maxLength
|
|
1488
|
+
];
|
|
1489
|
+
case "combobox": return [formatted.textOf(field) ?? field.value];
|
|
1490
|
+
case "listbox": return [field.topIndex, field.options.map((option) => [option.label, option.selected])];
|
|
1491
|
+
default: return [field?.family ?? null];
|
|
1492
|
+
}
|
|
1493
|
+
})();
|
|
1494
|
+
return JSON.stringify([
|
|
1495
|
+
box.width,
|
|
1496
|
+
box.height,
|
|
1497
|
+
row.rotation ?? 0,
|
|
1498
|
+
row.color ?? null,
|
|
1499
|
+
row.interiorColor ?? null,
|
|
1500
|
+
row.strokeWidth ?? null,
|
|
1501
|
+
row.borderStyle ?? null,
|
|
1502
|
+
row.dashArray ?? null,
|
|
1503
|
+
row.fontFamily ?? null,
|
|
1504
|
+
row.fontSize ?? null,
|
|
1505
|
+
row.fontColor ?? null,
|
|
1506
|
+
row.textAlign ?? null,
|
|
1507
|
+
row.caption ?? null,
|
|
1508
|
+
row.checkStyle ?? null,
|
|
1509
|
+
content
|
|
1510
|
+
]);
|
|
1511
|
+
};
|
|
1512
|
+
const lookOf = (index, number) => {
|
|
1513
|
+
const row = widgetRowOf(index, number);
|
|
1514
|
+
return row ? lookKeyOf(row, fieldForWidget(index, number)) : null;
|
|
1515
|
+
};
|
|
1516
|
+
const shownOn = (0, _embedpdf_core.memoByKey)((_pageObjectNumber) => [
|
|
1517
|
+
fields.view(),
|
|
1518
|
+
fields.get(),
|
|
1519
|
+
formatted.version(),
|
|
1520
|
+
design.lending()
|
|
1521
|
+
], (pageObjectNumber, view, truth, _formatted, lent) => lent ? NOTHING_SHOWN : shownWidgetsOf(view, pageObjectNumber, (row) => {
|
|
1522
|
+
const number = numberOf(row.ref);
|
|
1523
|
+
return {
|
|
1524
|
+
pending: lookOf(view, number) !== lookOf(truth, number),
|
|
1525
|
+
pictureVersion: pictureVersionOf(truth, number)
|
|
1526
|
+
};
|
|
1527
|
+
}));
|
|
1528
|
+
/** The painter's drawing of `row` (in the view's field, as this session formatted it) at `box`, turned. */
|
|
1529
|
+
const paint = (row, box, rotation) => {
|
|
1530
|
+
const field = fieldForWidget(fields.view(), numberOf(row.ref));
|
|
1531
|
+
if (!field) return null;
|
|
1532
|
+
const text = formatted.textOf(field);
|
|
1533
|
+
const drawing = (0, _embedpdf_core_form.widgetDrawing)({
|
|
1534
|
+
...row,
|
|
1535
|
+
box
|
|
1536
|
+
}, field, text === null ? {} : { text });
|
|
1537
|
+
return drawing && (0, _embedpdf_core_form.widgetScene)(drawing, {
|
|
1538
|
+
box,
|
|
1539
|
+
rotation
|
|
1540
|
+
});
|
|
1541
|
+
};
|
|
1542
|
+
/** The painter's drawing of a widget as the view has it; `null` for one it doesn't draw. */
|
|
1543
|
+
const sceneOf = (0, _embedpdf_core.memoByKey)((number) => {
|
|
1544
|
+
const view = fields.view();
|
|
1545
|
+
const row = widgetRowOf(view, number);
|
|
1546
|
+
const field = row ? fieldForWidget(view, number) : null;
|
|
1547
|
+
return [
|
|
1548
|
+
row,
|
|
1549
|
+
field,
|
|
1550
|
+
field ? formatted.textOf(field) : null
|
|
1551
|
+
];
|
|
1552
|
+
}, (_number, row) => {
|
|
1553
|
+
const scene = row && paint(row, row.box ?? row.rect, row.rotation);
|
|
1554
|
+
return scene ? { scene } : null;
|
|
1555
|
+
});
|
|
1556
|
+
return {
|
|
1557
|
+
listShownWidgets: (pageObjectNumber) => shownOn(pageObjectNumber),
|
|
1558
|
+
getWidgetPainting: (ref) => sceneOf(numberOf(ref)),
|
|
1559
|
+
/** The widget's look in the view isn't its confirmed look yet: it draws live until its picture is. */
|
|
1560
|
+
isPending: (ref) => {
|
|
1561
|
+
const number = numberOf(ref);
|
|
1562
|
+
return lookOf(fields.view(), number) !== lookOf(fields.get(), number);
|
|
1563
|
+
},
|
|
1564
|
+
/**
|
|
1565
|
+
* How the painter draws a widget row live, upright in a box (where a gesture has it); `null`
|
|
1566
|
+
* for one whose field it doesn't draw (a signature).
|
|
1567
|
+
*/
|
|
1568
|
+
painterOf: (row) => {
|
|
1569
|
+
const field = fieldForWidget(fields.view(), numberOf(row.ref));
|
|
1570
|
+
return field && (0, _embedpdf_core_form.paints)(field) ? (box) => paint(row, box, 0) ?? [] : null;
|
|
1571
|
+
},
|
|
1572
|
+
getTextEditorLayout: (ref, text) => {
|
|
1573
|
+
const view = fields.view();
|
|
1574
|
+
const number = numberOf(ref);
|
|
1575
|
+
const row = widgetRowOf(view, number);
|
|
1576
|
+
const field = row ? fieldForWidget(view, number) : null;
|
|
1577
|
+
return row && field ? (0, _embedpdf_core_form.textEditorLayout)(row, field, text) : null;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/services/pictures.ts
|
|
1583
|
+
/**
|
|
1584
|
+
* The engine's pictures of each page's widgets, through the render plugin's
|
|
1585
|
+
* one door. The form layer draws them, and the annotation plugin draws the
|
|
1586
|
+
* widgets it borrows from the same ones (design mode), so switching modes
|
|
1587
|
+
* fetches nothing. A page's pictures depend on its widgets' picture versions
|
|
1588
|
+
* (the mirror's) and nothing else: a checkbox that only switches its state
|
|
1589
|
+
* fetches nothing.
|
|
1590
|
+
*/
|
|
1591
|
+
const NO_PICTURES = Object.freeze([]);
|
|
1592
|
+
function createWidgetPictures(fields, { render }) {
|
|
1593
|
+
/** The page's widgets, as the engine confirmed them, each at its picture version. */
|
|
1594
|
+
const itemsOn = (0, _embedpdf_core.memoByKey)((_pageObjectNumber) => [fields.get()], (pageObjectNumber, index) => (index.snapshot?.widgets ?? []).flatMap((row) => row.page.objectNumber === pageObjectNumber && row.ref.kind === "objectNumber" ? [{
|
|
1595
|
+
ref: row.ref,
|
|
1596
|
+
version: pictureVersionOf(index, row.ref.objectNumber)
|
|
1597
|
+
}] : []));
|
|
1598
|
+
const epochOn = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => [itemsOn(pageObjectNumber)], (_pageObjectNumber, items) => items.map(({ ref, version }) => `${ref.kind === "objectNumber" ? ref.objectNumber : ""}@${version}`).sort().join("|"));
|
|
1599
|
+
return {
|
|
1600
|
+
epoch: (page) => epochOn(page.objectNumber),
|
|
1601
|
+
render: (page, scale, signal) => {
|
|
1602
|
+
const renderer = render();
|
|
1603
|
+
return renderer ? renderer.renderAppearances(page, {
|
|
1604
|
+
family: "widgets",
|
|
1605
|
+
scale,
|
|
1606
|
+
items: itemsOn(page.objectNumber),
|
|
1607
|
+
...signal ? { signal } : {}
|
|
1608
|
+
}) : Promise.resolve(NO_PICTURES);
|
|
1609
|
+
},
|
|
1610
|
+
versionOf: (widget) => widget.kind === "objectNumber" ? pictureVersionOf(fields.get(), widget.objectNumber) : 0
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
//#endregion
|
|
1614
|
+
//#region src/services/scripting.ts
|
|
1615
|
+
function createScriptingSeam(ctx, siblings) {
|
|
1616
|
+
let pipeline = null;
|
|
1617
|
+
const pipelineOf = () => {
|
|
1618
|
+
if (pipeline) return pipeline;
|
|
1619
|
+
const realm = siblings.actions()?.scriptRealm ?? null;
|
|
1620
|
+
if (!realm) return null;
|
|
1621
|
+
const made = require_controller.createFormScriptingController({
|
|
1622
|
+
document: () => ctx.document(),
|
|
1623
|
+
transaction: realm.transaction.bind(realm),
|
|
1624
|
+
budget: realm.budget
|
|
1625
|
+
});
|
|
1626
|
+
ctx.cleanup(() => made.dispose());
|
|
1627
|
+
pipeline = made;
|
|
1628
|
+
return made;
|
|
1629
|
+
};
|
|
1630
|
+
const settings = ctx.settings();
|
|
1631
|
+
const controller = () => settings.get().validation === "scripts" ? pipelineOf() : null;
|
|
1632
|
+
let warnedNoRealm = false;
|
|
1633
|
+
const scriptsFor = (form) => {
|
|
1634
|
+
if (!formHasScripts(form)) return null;
|
|
1635
|
+
if (!pipelineOf() && !warnedNoRealm && settings.get().validation === "scripts") {
|
|
1636
|
+
warnedNoRealm = true;
|
|
1637
|
+
console.warn("[form] this form has scripts, but they don't run: add the actions plugin with JavaScript on (`actionsPlugin({ javascript: { enabled: true, sandboxFactory } })`), or set the form's `validation` to 'none' to fill it in without them.");
|
|
1638
|
+
}
|
|
1639
|
+
return controller();
|
|
1640
|
+
};
|
|
1641
|
+
let warmed = false;
|
|
1642
|
+
return {
|
|
1643
|
+
/** The script pipeline when the document's scripts run, else `null`. */
|
|
1644
|
+
controller,
|
|
1645
|
+
/** The script pipeline for a write to this form: `null` too when no field has a script. */
|
|
1646
|
+
scriptsFor,
|
|
1647
|
+
/** Start the realm before the first write needs it, once, when the form has scripts. */
|
|
1648
|
+
warm(form) {
|
|
1649
|
+
if (warmed || !form.snapshot) return;
|
|
1650
|
+
const scripts = scriptsFor(form);
|
|
1651
|
+
if (!scripts) return;
|
|
1652
|
+
warmed = true;
|
|
1653
|
+
scripts.warm(form.snapshot).catch(() => {});
|
|
1654
|
+
},
|
|
1655
|
+
/** Script results (UI effects, diagnostics, errors) are surfaced through the
|
|
1656
|
+
* actions plugin, with the origin that caused them. */
|
|
1657
|
+
surface: (result, origin) => {
|
|
1658
|
+
siblings.actions()?.surfaceScriptCommit(result, {
|
|
1659
|
+
origin,
|
|
1660
|
+
realm: "document"
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/services/siblings.ts
|
|
1667
|
+
/** The optional sibling planes, looked up each time they're used, not once
|
|
1668
|
+
* at construction: what a sibling is depends on no creation order. A
|
|
1669
|
+
* fill-only setup may have none; every area guards on null. */
|
|
1670
|
+
function resolveSiblings(ctx) {
|
|
1671
|
+
return {
|
|
1672
|
+
/** The annotation plugin: design mode's selection, tools, ghosts and placement previews. */
|
|
1673
|
+
annotation: () => ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken),
|
|
1674
|
+
/** The script realm owner and the one surface port for script results. */
|
|
1675
|
+
actions: () => ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken),
|
|
1676
|
+
/** The page renderer: the widgets' pictures, shared by the form layer and design mode. */
|
|
1677
|
+
render: () => ctx.tryGet(_embedpdf_plugin_render_contract_host.RenderToken)
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1681
|
+
//#region src/services/index.ts
|
|
1682
|
+
function createServices(ctx) {
|
|
1683
|
+
const siblings = resolveSiblings(ctx);
|
|
1684
|
+
const events = createEvents(ctx);
|
|
1685
|
+
const fields = createFieldsMirror(ctx, events);
|
|
1686
|
+
const design = createDesign(ctx, siblings);
|
|
1687
|
+
const formatted = createFormattedTexts(ctx, fields);
|
|
1688
|
+
return {
|
|
1689
|
+
fields,
|
|
1690
|
+
rights: createFillRights(ctx, fields),
|
|
1691
|
+
events,
|
|
1692
|
+
siblings,
|
|
1693
|
+
scripting: createScriptingSeam(ctx, siblings),
|
|
1694
|
+
design,
|
|
1695
|
+
formatted,
|
|
1696
|
+
pictures: createWidgetPictures(fields, siblings),
|
|
1697
|
+
looks: createWidgetLooks({
|
|
1698
|
+
fields,
|
|
1699
|
+
formatted,
|
|
1700
|
+
design
|
|
1701
|
+
}),
|
|
1702
|
+
enqueue: ctx.serialQueue("writes"),
|
|
1703
|
+
keyOf: (ref) => canonicalKey(fields.view(), ref)
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/write/activation.ts
|
|
1708
|
+
/**
|
|
1709
|
+
* Widget activation and the widget DOM-event feed into the actions plane.
|
|
1710
|
+
* A click first offers the widget's `/A` tree to the dispatcher; only a
|
|
1711
|
+
* tree-less widget falls back to the form's own scripted activation.
|
|
1712
|
+
*/
|
|
1713
|
+
function createActivation(ctx, services, { fill }) {
|
|
1714
|
+
const { fields, scripting } = services;
|
|
1715
|
+
/** The widget's activation action, from its row in the form. */
|
|
1716
|
+
const annotationActivation = async (ref) => {
|
|
1717
|
+
await fields.settled();
|
|
1718
|
+
return (ref.kind === "objectNumber" ? widgetRowOf(fields.view(), ref.objectNumber) : null)?.actions?.activate ?? null;
|
|
1719
|
+
};
|
|
1720
|
+
/** The widget's activation through the form's own scripts: what they change is one fill. */
|
|
1721
|
+
const activateThroughScripts = async (ref, annotationRef, options) => {
|
|
1722
|
+
if (!scripting.controller()) return {
|
|
1723
|
+
status: "unchanged",
|
|
1724
|
+
scripted: false,
|
|
1725
|
+
uiEffects: [],
|
|
1726
|
+
diagnostics: []
|
|
1727
|
+
};
|
|
1728
|
+
const action = await annotationActivation(annotationRef);
|
|
1729
|
+
if (!action) return {
|
|
1730
|
+
status: "unchanged",
|
|
1731
|
+
scripted: true,
|
|
1732
|
+
uiEffects: [],
|
|
1733
|
+
diagnostics: []
|
|
1734
|
+
};
|
|
1735
|
+
const { result } = await fill({
|
|
1736
|
+
ops: [],
|
|
1737
|
+
scripts: {
|
|
1738
|
+
kind: "activate",
|
|
1739
|
+
field: ref,
|
|
1740
|
+
action
|
|
1741
|
+
},
|
|
1742
|
+
origin: "user"
|
|
1743
|
+
}, options);
|
|
1744
|
+
return result;
|
|
1745
|
+
};
|
|
1746
|
+
const widgetSource = (field, annotationRef) => ({
|
|
1747
|
+
kind: "widget",
|
|
1748
|
+
field,
|
|
1749
|
+
annotation: annotationRef,
|
|
1750
|
+
page: annotationRef.page
|
|
1751
|
+
});
|
|
1752
|
+
/**
|
|
1753
|
+
* One shared hover pump (an exit and the next enter delivered as one
|
|
1754
|
+
* ordered pair; intermediate hovers skipped), created on first use.
|
|
1755
|
+
*/
|
|
1756
|
+
let widgetPump = null;
|
|
1757
|
+
const widgetHoverPump = (actions) => {
|
|
1758
|
+
widgetPump ?? (widgetPump = (0, _embedpdf_plugin_actions_contract.createHoverPump)(actions.dispatch));
|
|
1759
|
+
return widgetPump;
|
|
1760
|
+
};
|
|
1761
|
+
const widgetHoverFlags = (annotationRef) => {
|
|
1762
|
+
const row = annotationRef.kind === "objectNumber" ? widgetRowOf(fields.view(), annotationRef.objectNumber) : null;
|
|
1763
|
+
if (!row) return null;
|
|
1764
|
+
return {
|
|
1765
|
+
enter: Boolean(row.actions?.cursorEnter?.root),
|
|
1766
|
+
exit: Boolean(row.actions?.cursorExit?.root)
|
|
1767
|
+
};
|
|
1768
|
+
};
|
|
1769
|
+
return { api: {
|
|
1770
|
+
activateWidget: async (annotationRef, options) => {
|
|
1771
|
+
const field = fieldForWidget(fields.view(), widgetObjectOf(annotationRef));
|
|
1772
|
+
if (!field) throw new _embedpdf_core.PluginError("not-found", "form", "no form field owns this widget");
|
|
1773
|
+
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
1774
|
+
if (actions) {
|
|
1775
|
+
const result = await ctx.cancellable(options?.signal, actions.dispatch({
|
|
1776
|
+
scope: "activate",
|
|
1777
|
+
ref: annotationRef,
|
|
1778
|
+
page: annotationRef.page,
|
|
1779
|
+
source: widgetSource(field.ref, annotationRef)
|
|
1780
|
+
}));
|
|
1781
|
+
if (!(result.status === "inert" && result.steps.length === 0 && result.diagnostics.length === 0)) return {
|
|
1782
|
+
kind: "dispatched",
|
|
1783
|
+
result
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
return {
|
|
1787
|
+
kind: "form",
|
|
1788
|
+
result: await activateThroughScripts(field.ref, annotationRef, options)
|
|
1789
|
+
};
|
|
1790
|
+
},
|
|
1791
|
+
notifyWidgetEvent: (fieldRef, annotationRef, event) => {
|
|
1792
|
+
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
1793
|
+
if (!actions) return;
|
|
1794
|
+
const source = widgetSource(fieldRef, annotationRef);
|
|
1795
|
+
if (event === "cursorEnter" || event === "cursorExit") {
|
|
1796
|
+
if (event === "cursorExit") {
|
|
1797
|
+
widgetHoverPump(actions).hover(null);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const flags = widgetHoverFlags(annotationRef);
|
|
1801
|
+
if (flags && !flags.enter && !flags.exit) return;
|
|
1802
|
+
widgetHoverPump(actions).hover({
|
|
1803
|
+
ref: annotationRef,
|
|
1804
|
+
page: annotationRef.page,
|
|
1805
|
+
source,
|
|
1806
|
+
...flags ? { events: flags } : {}
|
|
1807
|
+
});
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (event === "focus") scripting.warm(fields.view());
|
|
1811
|
+
actions.dispatch({
|
|
1812
|
+
scope: "annotation",
|
|
1813
|
+
event,
|
|
1814
|
+
ref: annotationRef,
|
|
1815
|
+
page: annotationRef.page,
|
|
1816
|
+
source
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
} };
|
|
1820
|
+
}
|
|
1821
|
+
//#endregion
|
|
1822
|
+
//#region src/services/object-numbers.ts
|
|
1823
|
+
function createObjectNumbers(ctx) {
|
|
1824
|
+
const held = [];
|
|
1825
|
+
let watching = false;
|
|
1826
|
+
const watchLost = () => {
|
|
1827
|
+
if (watching || !ctx.doc) return;
|
|
1828
|
+
watching = true;
|
|
1829
|
+
ctx.doc.objectNumbers.onLost(({ numbers }) => {
|
|
1830
|
+
const lost = new Set(numbers);
|
|
1831
|
+
held.splice(0, held.length, ...held.filter((number) => !lost.has(number)));
|
|
1832
|
+
});
|
|
1833
|
+
};
|
|
1834
|
+
const takeNow = (count) => {
|
|
1835
|
+
watchLost();
|
|
1836
|
+
while (held.length < count) {
|
|
1837
|
+
const number = ctx.changes.takeObjectNumber();
|
|
1838
|
+
if (number === null) return null;
|
|
1839
|
+
held.push(number);
|
|
1840
|
+
}
|
|
1841
|
+
return held.splice(0, count);
|
|
1842
|
+
};
|
|
1843
|
+
return {
|
|
1844
|
+
takeNow,
|
|
1845
|
+
async take(count) {
|
|
1846
|
+
for (;;) {
|
|
1847
|
+
const taken = takeNow(count);
|
|
1848
|
+
if (taken) return taken;
|
|
1849
|
+
await ctx.changes.reserveObjectNumbers(count - held.length);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
//#endregion
|
|
1855
|
+
//#region src/write/next-in-line.ts
|
|
1856
|
+
const fits = (box, page) => box.x >= 0 && box.y >= 0 && box.x + box.width <= page.width && box.y + box.height <= page.height;
|
|
1857
|
+
/** The box after `boxes`, the field's widgets on one page in the field's order. */
|
|
1858
|
+
function nextInLine(boxes, page) {
|
|
1859
|
+
const last = boxes.at(-1);
|
|
1860
|
+
if (!last) return {
|
|
1861
|
+
x: 0,
|
|
1862
|
+
y: 0,
|
|
1863
|
+
width: 0,
|
|
1864
|
+
height: 0
|
|
1865
|
+
};
|
|
1866
|
+
const { width, height } = last;
|
|
1867
|
+
const previous = boxes.at(-2);
|
|
1868
|
+
const step = previous && (previous.x !== last.x || previous.y !== last.y) ? {
|
|
1869
|
+
x: last.x - previous.x,
|
|
1870
|
+
y: last.y - previous.y
|
|
1871
|
+
} : {
|
|
1872
|
+
x: 0,
|
|
1873
|
+
y: height * 1.5
|
|
1874
|
+
};
|
|
1875
|
+
const next = {
|
|
1876
|
+
x: last.x + step.x,
|
|
1877
|
+
y: last.y + step.y,
|
|
1878
|
+
width,
|
|
1879
|
+
height
|
|
1880
|
+
};
|
|
1881
|
+
if (fits(next, page)) return next;
|
|
1882
|
+
const column = Math.abs(step.y) >= Math.abs(step.x);
|
|
1883
|
+
const left = Math.min(...boxes.map((box) => box.x));
|
|
1884
|
+
const top = Math.min(...boxes.map((box) => box.y));
|
|
1885
|
+
const right = Math.max(...boxes.map((box) => box.x + box.width));
|
|
1886
|
+
const bottom = Math.max(...boxes.map((box) => box.y + box.height));
|
|
1887
|
+
const gap = Math.max(column ? Math.abs(step.y) - height : Math.abs(step.x) - width, 0);
|
|
1888
|
+
const restart = column ? {
|
|
1889
|
+
x: right + (gap || width / 2),
|
|
1890
|
+
y: top,
|
|
1891
|
+
width,
|
|
1892
|
+
height
|
|
1893
|
+
} : {
|
|
1894
|
+
x: left,
|
|
1895
|
+
y: bottom + (gap || height / 2),
|
|
1896
|
+
width,
|
|
1897
|
+
height
|
|
1898
|
+
};
|
|
1899
|
+
if (fits(restart, page)) return restart;
|
|
1900
|
+
return {
|
|
1901
|
+
x: Math.min(Math.max(next.x, 0), Math.max(page.width - width, 0)),
|
|
1902
|
+
y: Math.min(Math.max(next.y, 0), Math.max(page.height - height, 0)),
|
|
1903
|
+
width,
|
|
1904
|
+
height
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
//#endregion
|
|
1908
|
+
//#region src/write/fields.ts
|
|
1909
|
+
/**
|
|
1910
|
+
* Building the form: every design verb is one change on the document's queue
|
|
1911
|
+
* (`ctx.changes`). It shows at once (the fields mirror predicts it), it is a
|
|
1912
|
+
* step of the history, and it resolves with the engine's answer. A create
|
|
1913
|
+
* takes its field's and widgets' object numbers first, so their refs are final
|
|
1914
|
+
* from the first frame and the new widget can be selected straight away.
|
|
1915
|
+
*/
|
|
1916
|
+
/**
|
|
1917
|
+
* A widget's look, as `addWidget()` copies it from the field's last widget onto the new one:
|
|
1918
|
+
* the engine's widget style fields (`WIDGET_STYLE_NAMES`), and its turn.
|
|
1919
|
+
*/
|
|
1920
|
+
const LOOK = [
|
|
1921
|
+
"color",
|
|
1922
|
+
"interiorColor",
|
|
1923
|
+
"strokeWidth",
|
|
1924
|
+
"borderStyle",
|
|
1925
|
+
"dashArray",
|
|
1926
|
+
"fontFamily",
|
|
1927
|
+
"fontSize",
|
|
1928
|
+
"fontColor",
|
|
1929
|
+
"textAlign",
|
|
1930
|
+
"caption",
|
|
1931
|
+
"checkStyle",
|
|
1932
|
+
"rotation"
|
|
1933
|
+
];
|
|
1934
|
+
const lookOf = (widget) => {
|
|
1935
|
+
if (!widget) return {};
|
|
1936
|
+
const held = widget;
|
|
1937
|
+
const look = {};
|
|
1938
|
+
for (const name of LOOK) if (held[name] !== void 0 && held[name] !== null) look[name] = held[name];
|
|
1939
|
+
return look;
|
|
1940
|
+
};
|
|
1941
|
+
/**
|
|
1942
|
+
* The export value a field's new widget gets when none is given: a radio group's next free
|
|
1943
|
+
* `Choice<n>` (the radio tool names its first button `Choice1`), a checkbox's own value, so
|
|
1944
|
+
* the boxes check together. Other families take none.
|
|
1945
|
+
*/
|
|
1946
|
+
function exportValueFor(field) {
|
|
1947
|
+
if (field.family === "checkbox") return field.exportValue;
|
|
1948
|
+
if (field.family !== "radio") return void 0;
|
|
1949
|
+
const taken = new Set(field.widgets.map((widget) => widget.exportValue));
|
|
1950
|
+
let n = field.widgets.length + 1;
|
|
1951
|
+
while (taken.has(`Choice${n}`)) n += 1;
|
|
1952
|
+
return `Choice${n}`;
|
|
1953
|
+
}
|
|
1954
|
+
/** Whether the field's dictionary is its only widget's too, which adding a widget splits. */
|
|
1955
|
+
const isMerged = (field) => field.ref.kind === "objectNumber" && field.widgets.some((widget) => widget.objectNumber === field.ref.objectNumber);
|
|
1956
|
+
const MODIFY = "doc.forms.modify";
|
|
1957
|
+
const SCRIPT = "doc.forms.script";
|
|
1958
|
+
function createFieldWrites(ctx, { fields, enqueue }) {
|
|
1959
|
+
const numbers = createObjectNumbers(ctx);
|
|
1960
|
+
/** Refuse up front without `doc.forms.modify`, and without `doc.forms.script` when it writes scripts. */
|
|
1961
|
+
const allow = (operation, scripts = false) => {
|
|
1962
|
+
ctx.assertAllowed(MODIFY, operation);
|
|
1963
|
+
if (scripts) ctx.assertAllowed(SCRIPT, operation);
|
|
1964
|
+
};
|
|
1965
|
+
/** Stage one design op as its own change: it shows at once, and undoing it shows the form as it was. */
|
|
1966
|
+
const stage = (verb, op) => ctx.changes.stage({
|
|
1967
|
+
label: { key: `form.${verb}` },
|
|
1968
|
+
ops: [op],
|
|
1969
|
+
undo: undoLookOf(fields.view(), [op])
|
|
1970
|
+
});
|
|
1971
|
+
/** The engine's answer to a staged design change: its one item. */
|
|
1972
|
+
const answer = async (change, options) => {
|
|
1973
|
+
try {
|
|
1974
|
+
return (await ctx.cancellable(options?.signal, change.result)).items[0];
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
throw (0, _embedpdf_core.toPluginError)("form", error);
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
const create = async (draft, options) => {
|
|
1980
|
+
allow("form.create", (0, _embedpdf_engine_core_runtime.draftWritesScripts)(draft));
|
|
1981
|
+
const count = 1 + (draft.widgets?.length ?? 0);
|
|
1982
|
+
const [objectNumber, ...widgetObjectNumbers] = numbers.takeNow(count) ?? await numbers.take(count);
|
|
1983
|
+
const change = stage("create", {
|
|
1984
|
+
type: "forms.create",
|
|
1985
|
+
draft,
|
|
1986
|
+
objectNumber,
|
|
1987
|
+
widgetObjectNumbers
|
|
1988
|
+
});
|
|
1989
|
+
return { field: (await answer(change, options)).field };
|
|
1990
|
+
};
|
|
1991
|
+
const addWidget = async (ref, placement = {}, options) => {
|
|
1992
|
+
allow("form.addWidget", (0, _embedpdf_engine_core_runtime.writesScripts)(placement.actions));
|
|
1993
|
+
const index = fields.view();
|
|
1994
|
+
const field = fieldByRef(index, ref);
|
|
1995
|
+
if (!field) throw new _embedpdf_core.PluginError("not-found", "form", "no form field has this ref");
|
|
1996
|
+
const rows = field.widgets.map((widget) => widgetRowOf(index, widget.objectNumber)).filter((row) => row !== null);
|
|
1997
|
+
const last = rows.at(-1);
|
|
1998
|
+
const page = placement.page ?? last?.page;
|
|
1999
|
+
if (!page) throw new _embedpdf_core.PluginError("invalid-input", "form", "the field shows on no page: give `page` and `rect`");
|
|
2000
|
+
const size = ctx.getPage(page)?.size;
|
|
2001
|
+
const onPage = rows.filter((row) => row.page.objectNumber === page.objectNumber);
|
|
2002
|
+
const rect = placement.rect ?? placement.box ?? (size && onPage.length > 0 ? nextInLine(onPage.map((row) => row.box), size) : void 0);
|
|
2003
|
+
if (!rect) throw new _embedpdf_core.PluginError("invalid-input", "form", "no widget of the field on that page: give `rect`");
|
|
2004
|
+
const exportValue = placement.exportValue ?? exportValueFor(field);
|
|
2005
|
+
const split = isMerged(field);
|
|
2006
|
+
const [objectNumber, splitObjectNumber] = numbers.takeNow(split ? 2 : 1) ?? await numbers.take(split ? 2 : 1);
|
|
2007
|
+
const change = stage("addWidget", {
|
|
2008
|
+
type: "forms.addWidget",
|
|
2009
|
+
field: field.ref,
|
|
2010
|
+
placement: {
|
|
2011
|
+
...lookOf(last),
|
|
2012
|
+
...placement,
|
|
2013
|
+
page,
|
|
2014
|
+
...placement.box ? {} : { rect },
|
|
2015
|
+
...exportValue === void 0 ? {} : { exportValue }
|
|
2016
|
+
},
|
|
2017
|
+
objectNumber,
|
|
2018
|
+
...split ? { splitObjectNumber } : {}
|
|
2019
|
+
});
|
|
2020
|
+
const item = await answer(change, options);
|
|
2021
|
+
const widget = item.widgets.find((row) => row.ref.kind === "objectNumber" && row.ref.objectNumber === objectNumber) ?? item.widgets.at(-1);
|
|
2022
|
+
return {
|
|
2023
|
+
field: item.field,
|
|
2024
|
+
widget
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
return { api: {
|
|
2028
|
+
create,
|
|
2029
|
+
addWidget,
|
|
2030
|
+
update: async (ref, patch, options) => {
|
|
2031
|
+
allow("form.update", (0, _embedpdf_engine_core_runtime.writesScripts)(patch.actions));
|
|
2032
|
+
const change = stage("update", {
|
|
2033
|
+
type: "forms.update",
|
|
2034
|
+
field: ref,
|
|
2035
|
+
patch
|
|
2036
|
+
});
|
|
2037
|
+
return { field: (await answer(change, options)).field };
|
|
2038
|
+
},
|
|
2039
|
+
reorderCalculations: async (fields, position, options) => {
|
|
2040
|
+
allow("form.reorderCalculations");
|
|
2041
|
+
const change = stage("reorderCalculations", {
|
|
2042
|
+
type: "forms.reorderCalculations",
|
|
2043
|
+
fields,
|
|
2044
|
+
position
|
|
2045
|
+
});
|
|
2046
|
+
return { calculationOrder: (await answer(change, options)).calculationOrder };
|
|
2047
|
+
},
|
|
2048
|
+
delete: async (ref, options) => {
|
|
2049
|
+
allow("form.delete");
|
|
2050
|
+
await answer(stage("delete", {
|
|
2051
|
+
type: "forms.delete",
|
|
2052
|
+
field: ref
|
|
2053
|
+
}), options);
|
|
2054
|
+
},
|
|
2055
|
+
removeWidget: async (ref, widget, options) => {
|
|
2056
|
+
allow("form.removeWidget");
|
|
2057
|
+
const change = stage("removeWidget", {
|
|
2058
|
+
type: "forms.removeWidget",
|
|
2059
|
+
field: ref,
|
|
2060
|
+
widget
|
|
2061
|
+
});
|
|
2062
|
+
return { field: (await answer(change, options)).field };
|
|
2063
|
+
},
|
|
2064
|
+
updateWidget: async (widget, patch, options) => {
|
|
2065
|
+
allow("form.updateWidget");
|
|
2066
|
+
const change = stage("updateWidget", {
|
|
2067
|
+
type: "forms.updateWidget",
|
|
2068
|
+
widget,
|
|
2069
|
+
patch
|
|
2070
|
+
});
|
|
2071
|
+
return { widget: (await answer(change, options)).widget };
|
|
2072
|
+
},
|
|
2073
|
+
deleteWidget: async (widget, options) => {
|
|
2074
|
+
allow("form.deleteWidget");
|
|
2075
|
+
const change = stage("deleteWidget", {
|
|
2076
|
+
type: "forms.deleteWidget",
|
|
2077
|
+
widget
|
|
2078
|
+
});
|
|
2079
|
+
return { field: (await answer(change, options)).field };
|
|
2080
|
+
},
|
|
2081
|
+
repair: async (options) => {
|
|
2082
|
+
allow("form.repair");
|
|
2083
|
+
const { signal, ...repair } = options ?? {};
|
|
2084
|
+
const run = () => options ? ctx.doc.forms.repair(repair) : ctx.doc.forms.repair();
|
|
2085
|
+
return enqueue(() => ctx.cancellable(signal, run()), options);
|
|
2086
|
+
}
|
|
2087
|
+
} };
|
|
2088
|
+
}
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/write/fill.ts
|
|
2091
|
+
/**
|
|
2092
|
+
* Filling the form in. Every fill is one change, whatever started it: a value, a batch of
|
|
2093
|
+
* values, a reset, a widget's activation, a script's or an action's effects.
|
|
2094
|
+
*
|
|
2095
|
+
* 1. What the user did shows at once: the change is held with the user's own ops.
|
|
2096
|
+
* 2. When its turn comes, the form's scripts run against the view, in one realm transaction.
|
|
2097
|
+
* 3. Inside that transaction the change gets every op the scripts asked for, and is sent.
|
|
2098
|
+
*
|
|
2099
|
+
* Fills take turns up to their send, not their answer: the next fill's scripts read this one in
|
|
2100
|
+
* the view while the engine answers it. A validation refusal cancels the change. A change the
|
|
2101
|
+
* engine refuses leaves the view, and the calculations run again against what is left, outside
|
|
2102
|
+
* the history: the fills after it were calculated with its values.
|
|
2103
|
+
*/
|
|
2104
|
+
/** A refused value writes only what the document's open scripts did, outside the history. */
|
|
2105
|
+
const SCRIPTS_LABEL = { key: "form.scripts" };
|
|
2106
|
+
/** The calculations again, outside the history: after a refused fill, or a stale one. */
|
|
2107
|
+
const RECALCULATE_LABEL = { key: "form.recalculate" };
|
|
2108
|
+
/** The changes that, refused, leave calculations made with their values. */
|
|
2109
|
+
const FILLS = /* @__PURE__ */ new Set(["form.fill", "form.reset"]);
|
|
2110
|
+
const sameOp = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
2111
|
+
const sameValue = (left, right) => JSON.stringify(left?.valueEntry ?? null) === JSON.stringify(right?.valueEntry ?? null);
|
|
2112
|
+
function createFill(ctx, { fields, scripting, enqueue }) {
|
|
2113
|
+
const labelOf = (request, index, ops) => request.label ?? {
|
|
2114
|
+
key: "form.fill",
|
|
2115
|
+
count: fieldsNamedBy(index, ops).length
|
|
2116
|
+
};
|
|
2117
|
+
/** Step 1: what the user did shows at once. */
|
|
2118
|
+
const begin = (request) => {
|
|
2119
|
+
const before = fields.view();
|
|
2120
|
+
const shown = request.ops.reduce(predictFormOp, before);
|
|
2121
|
+
if (request.ops.length === 0) return {
|
|
2122
|
+
request,
|
|
2123
|
+
before,
|
|
2124
|
+
shown,
|
|
2125
|
+
hold: null
|
|
2126
|
+
};
|
|
2127
|
+
const hold = ctx.changes.hold(labelOf(request, before, request.ops));
|
|
2128
|
+
hold.set(request.ops, undoLookOf(before, request.ops));
|
|
2129
|
+
return {
|
|
2130
|
+
request,
|
|
2131
|
+
before,
|
|
2132
|
+
shown,
|
|
2133
|
+
hold
|
|
2134
|
+
};
|
|
2135
|
+
};
|
|
2136
|
+
/**
|
|
2137
|
+
* The form the change is undone to: as it is now, with the user's own fields as they were
|
|
2138
|
+
* before the user's ops showed.
|
|
2139
|
+
*/
|
|
2140
|
+
const baseOf = ({ request, before }) => withFieldsFrom(fields.view(), before, fieldsNamedBy(before, request.ops));
|
|
2141
|
+
/** Whether the hold went with the user's ops before the scripts finished. */
|
|
2142
|
+
const sentEarly = ({ hold }) => hold !== null && !hold.open && !!hold.change;
|
|
2143
|
+
/** Whether the form still shows the values the user's ops wrote: the scripts ran for those. */
|
|
2144
|
+
const stillShown = ({ request, shown }) => {
|
|
2145
|
+
const now = fields.view();
|
|
2146
|
+
return fieldsNamedBy(shown, request.ops).every((field) => sameValue(fieldByKey(now, fieldKeyOf(field)), field));
|
|
2147
|
+
};
|
|
2148
|
+
/** Step 3: the change with all its ops, sent. */
|
|
2149
|
+
const send = (begun, ops, script) => {
|
|
2150
|
+
const { request, hold } = begun;
|
|
2151
|
+
if (ops.length === 0) {
|
|
2152
|
+
hold?.cancel();
|
|
2153
|
+
return {
|
|
2154
|
+
script,
|
|
2155
|
+
changes: [],
|
|
2156
|
+
after: fields.view()
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
if (!hold) {
|
|
2160
|
+
const label = labelOf(request, fields.view(), ops);
|
|
2161
|
+
return {
|
|
2162
|
+
script,
|
|
2163
|
+
changes: [ctx.changes.stage({
|
|
2164
|
+
label,
|
|
2165
|
+
ops,
|
|
2166
|
+
undo: undoLookOf(baseOf(begun), ops)
|
|
2167
|
+
})],
|
|
2168
|
+
after: fields.view()
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
if (hold.open) {
|
|
2172
|
+
hold.set(ops, undoLookOf(baseOf(begun), ops));
|
|
2173
|
+
const change = hold.send();
|
|
2174
|
+
return {
|
|
2175
|
+
script,
|
|
2176
|
+
changes: change ? [change] : [],
|
|
2177
|
+
after: fields.view()
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
const sent = hold.change;
|
|
2181
|
+
const rest = ops.filter((op) => !sent?.shows.some((shown) => sameOp(shown, op)));
|
|
2182
|
+
const changes = sent ? [sent] : [];
|
|
2183
|
+
if (rest.length > 0) {
|
|
2184
|
+
const label = labelOf(request, fields.view(), rest);
|
|
2185
|
+
changes.push(ctx.changes.stage({
|
|
2186
|
+
label,
|
|
2187
|
+
ops: rest,
|
|
2188
|
+
undo: undoLookOf(fields.view(), rest)
|
|
2189
|
+
}));
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
script,
|
|
2193
|
+
changes,
|
|
2194
|
+
after: fields.view()
|
|
2195
|
+
};
|
|
2196
|
+
};
|
|
2197
|
+
/** The user's ops went on their own and the scripts refused them: their fields are written back. */
|
|
2198
|
+
const takeBack = ({ request, before }) => {
|
|
2199
|
+
const ops = fieldsNamedBy(before, request.ops).flatMap((field) => {
|
|
2200
|
+
const value = (0, _embedpdf_engine_core_runtime.fieldValueOf)(field);
|
|
2201
|
+
return value ? [{
|
|
2202
|
+
type: "forms.setValue",
|
|
2203
|
+
field: field.ref,
|
|
2204
|
+
value
|
|
2205
|
+
}] : [];
|
|
2206
|
+
});
|
|
2207
|
+
if (ops.length === 0) return;
|
|
2208
|
+
ctx.changes.stage({
|
|
2209
|
+
label: labelOf(request, before, ops),
|
|
2210
|
+
ops,
|
|
2211
|
+
history: false
|
|
2212
|
+
});
|
|
2213
|
+
};
|
|
2214
|
+
/** The calculations again, against what the form shows now: one change, outside the history. */
|
|
2215
|
+
const recalculateNow = async (scripts, origin) => {
|
|
2216
|
+
const shown = fields.view().snapshot;
|
|
2217
|
+
if (!shown) return;
|
|
2218
|
+
const script = await scripts.recalculate(shown);
|
|
2219
|
+
scripting.surface(script, origin);
|
|
2220
|
+
if (script.status !== "accepted" || script.effects.length === 0) return;
|
|
2221
|
+
ctx.changes.stage({
|
|
2222
|
+
label: RECALCULATE_LABEL,
|
|
2223
|
+
ops: (0, _embedpdf_engine_core_runtime.formEffectOps)(script.effects),
|
|
2224
|
+
history: false
|
|
2225
|
+
});
|
|
2226
|
+
};
|
|
2227
|
+
/** Step 2: the scripts, against the view. */
|
|
2228
|
+
const runScripts = (scripts, begun, plan) => {
|
|
2229
|
+
const shown = fields.view().snapshot;
|
|
2230
|
+
if (!shown) return null;
|
|
2231
|
+
switch (plan.kind) {
|
|
2232
|
+
case "commit": {
|
|
2233
|
+
const form = baseOf(begun).snapshot ?? shown;
|
|
2234
|
+
return scripts.commit(form, plan.field, plan.value);
|
|
2235
|
+
}
|
|
2236
|
+
case "activate": return scripts.activate(shown, plan.field, plan.action);
|
|
2237
|
+
case "recalculate": return scripts.recalculate(shown);
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
/** Steps 2 and 3, when the fill's turn comes. */
|
|
2241
|
+
const finish = async (begun) => {
|
|
2242
|
+
const { request, hold } = begun;
|
|
2243
|
+
const plan = request.scripts;
|
|
2244
|
+
const pipeline = !plan ? null : plan.kind === "activate" ? scripting.controller() : scripting.scriptsFor(fields.view());
|
|
2245
|
+
if (!plan || !pipeline) return send(begun, request.ops, null);
|
|
2246
|
+
if (!fields.view().snapshot) await fields.settled();
|
|
2247
|
+
try {
|
|
2248
|
+
return await pipeline.transaction(async (scripts) => {
|
|
2249
|
+
const running = runScripts(scripts, begun, plan);
|
|
2250
|
+
if (!running) return send(begun, request.ops, null);
|
|
2251
|
+
const script = await running;
|
|
2252
|
+
scripting.surface(script, request.origin);
|
|
2253
|
+
const early = sentEarly(begun);
|
|
2254
|
+
if (script.status !== "accepted") {
|
|
2255
|
+
hold?.cancel();
|
|
2256
|
+
if (early) takeBack(begun);
|
|
2257
|
+
if (script.status === "rejected" && script.bootEffects.length > 0) ctx.changes.stage({
|
|
2258
|
+
label: SCRIPTS_LABEL,
|
|
2259
|
+
ops: (0, _embedpdf_engine_core_runtime.formEffectOps)(script.bootEffects),
|
|
2260
|
+
history: false
|
|
2261
|
+
});
|
|
2262
|
+
return {
|
|
2263
|
+
script,
|
|
2264
|
+
changes: [],
|
|
2265
|
+
after: fields.view()
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
if (early && !stillShown(begun)) {
|
|
2269
|
+
await recalculateNow(scripts, request.origin);
|
|
2270
|
+
return {
|
|
2271
|
+
script,
|
|
2272
|
+
changes: [hold.change],
|
|
2273
|
+
after: fields.view()
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
const effects = (0, _embedpdf_engine_core_runtime.formEffectOps)(script.effects);
|
|
2277
|
+
return send(begun, plan.kind === "recalculate" ? [...request.ops, ...effects] : effects, script);
|
|
2278
|
+
});
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
hold?.cancel();
|
|
2281
|
+
throw error;
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
/** What a sent fill did, once the engine answered. */
|
|
2285
|
+
const answered = async (begun, sent) => {
|
|
2286
|
+
const { script } = sent;
|
|
2287
|
+
const reported = {
|
|
2288
|
+
scripted: script !== null,
|
|
2289
|
+
uiEffects: script?.uiEffects ?? [],
|
|
2290
|
+
diagnostics: script?.diagnostics ?? [],
|
|
2291
|
+
...script?.error ? { error: script.error } : {}
|
|
2292
|
+
};
|
|
2293
|
+
const outcome = (status) => ({
|
|
2294
|
+
result: {
|
|
2295
|
+
status,
|
|
2296
|
+
...reported
|
|
2297
|
+
},
|
|
2298
|
+
before: begun.before,
|
|
2299
|
+
after: sent.after
|
|
2300
|
+
});
|
|
2301
|
+
if (script?.status === "failed") return outcome("failed");
|
|
2302
|
+
if (script?.status === "rejected") return outcome("rejected");
|
|
2303
|
+
let undoable = false;
|
|
2304
|
+
for (const change of sent.changes) try {
|
|
2305
|
+
undoable = (await change.result).meta.undoable || undoable;
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
throw (0, _embedpdf_core.toPluginError)("form", error);
|
|
2308
|
+
}
|
|
2309
|
+
return outcome(undoable ? "applied" : "unchanged");
|
|
2310
|
+
};
|
|
2311
|
+
/**
|
|
2312
|
+
* Fill the form in: shown at once, sent when its turn comes; resolves once the engine
|
|
2313
|
+
* answered. A change the engine refuses rejects.
|
|
2314
|
+
*/
|
|
2315
|
+
const fill = async (request, options) => {
|
|
2316
|
+
const begun = begin(request);
|
|
2317
|
+
let sent;
|
|
2318
|
+
try {
|
|
2319
|
+
sent = await enqueue(() => finish(begun), options);
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
begun.hold?.cancel();
|
|
2322
|
+
throw (0, _embedpdf_core.toPluginError)("form", error);
|
|
2323
|
+
}
|
|
2324
|
+
return ctx.cancellable(options?.signal, answered(begun, sent));
|
|
2325
|
+
};
|
|
2326
|
+
/**
|
|
2327
|
+
* Fill the form in without waiting: what a script's effects need, written while its own
|
|
2328
|
+
* transaction still runs. A failure is reported as the scripts' error.
|
|
2329
|
+
*/
|
|
2330
|
+
const fillSoon = (request) => {
|
|
2331
|
+
const begun = begin(request);
|
|
2332
|
+
enqueue(() => finish(begun)).catch((error) => {
|
|
2333
|
+
begun.hold?.cancel();
|
|
2334
|
+
const message = (0, _embedpdf_core.toPluginError)("form", error).message;
|
|
2335
|
+
scripting.surface({
|
|
2336
|
+
uiEffects: [],
|
|
2337
|
+
diagnostics: [],
|
|
2338
|
+
error: require_controller.scriptError(message)
|
|
2339
|
+
}, request.origin);
|
|
2340
|
+
});
|
|
2341
|
+
};
|
|
2342
|
+
let recalculating = false;
|
|
2343
|
+
const recalculate = () => enqueue(async () => {
|
|
2344
|
+
recalculating = false;
|
|
2345
|
+
const pipeline = scripting.controller();
|
|
2346
|
+
if (pipeline) await pipeline.transaction((scripts) => recalculateNow(scripts, "user"));
|
|
2347
|
+
});
|
|
2348
|
+
ctx.changes.onSettled((settled) => {
|
|
2349
|
+
if (settled.status !== "refused" || settled.change.undoOf !== null) return;
|
|
2350
|
+
if (!FILLS.has(settled.change.label.key) || recalculating) return;
|
|
2351
|
+
const order = fields.view().snapshot?.calculationOrder ?? [];
|
|
2352
|
+
if (!scripting.controller() || !order.some((ref) => ref !== null)) return;
|
|
2353
|
+
recalculating = true;
|
|
2354
|
+
recalculate().catch(() => {});
|
|
2355
|
+
});
|
|
2356
|
+
ctx.onSettle(async () => {
|
|
2357
|
+
await enqueue.idle();
|
|
2358
|
+
await ctx.changes.whenSettled();
|
|
2359
|
+
});
|
|
2360
|
+
return {
|
|
2361
|
+
fill,
|
|
2362
|
+
fillSoon
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
/** A fill that failed in the form's own scripts, as the verbs report it. */
|
|
2366
|
+
function failedFill(result) {
|
|
2367
|
+
return new _embedpdf_core.PluginError("operation-failed", "form", result.error?.message ?? result.diagnostics[0]?.message ?? "the write failed");
|
|
2368
|
+
}
|
|
2369
|
+
//#endregion
|
|
2370
|
+
//#region src/write/interchange.ts
|
|
2371
|
+
function createInterchange(ctx, services) {
|
|
2372
|
+
const { enqueue, siblings } = services;
|
|
2373
|
+
const exportBundle = async (selection = {}, options) => {
|
|
2374
|
+
ctx.assertAllowed("doc.forms.read", "form.export");
|
|
2375
|
+
ctx.assertAllowed("doc.download", "form.export");
|
|
2376
|
+
const pages = selection.pages?.map((page) => ctx.pageOf(page).ref);
|
|
2377
|
+
return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.forms.export({
|
|
2378
|
+
...selection.fields ? { fields: selection.fields } : {},
|
|
2379
|
+
...pages ? { pages } : {}
|
|
2380
|
+
})), options);
|
|
2381
|
+
};
|
|
2382
|
+
const importBundle = async (bundle, options = {}) => {
|
|
2383
|
+
const { signal, ...importOptions } = options;
|
|
2384
|
+
ctx.assertAllowed("doc.forms.modify", "form.import");
|
|
2385
|
+
if ((importOptions.attribution ?? "restore") === "restore") ctx.assertAllowed("doc.forms.import", "form.import");
|
|
2386
|
+
return enqueue(async () => {
|
|
2387
|
+
const result = await ctx.cancellable(signal, ctx.doc.forms.import(bundle, importOptions));
|
|
2388
|
+
const annotationHost = siblings.annotation();
|
|
2389
|
+
if (annotationHost) await annotationHost.whenSynced();
|
|
2390
|
+
return {
|
|
2391
|
+
fields: result.fields,
|
|
2392
|
+
refMap: result.refMap,
|
|
2393
|
+
dropped: result.dropped
|
|
2394
|
+
};
|
|
2395
|
+
}, options);
|
|
2396
|
+
};
|
|
2397
|
+
return { api: {
|
|
2398
|
+
export: exportBundle,
|
|
2399
|
+
import: importBundle
|
|
2400
|
+
} };
|
|
2401
|
+
}
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/write/lending.ts
|
|
2404
|
+
/**
|
|
2405
|
+
* Design mode: the form's widgets, lent to the annotation plugin
|
|
2406
|
+
* (`AnnotationLender`, its host contract). While the session may design and
|
|
2407
|
+
* the active tool doesn't fill, the annotation plugin shows the widgets among
|
|
2408
|
+
* its records and edits them with its tools: select, move, resize, restyle,
|
|
2409
|
+
* reorder, delete. Each edit comes back here as the form's ops, in the same
|
|
2410
|
+
* change as the rest of it. While the tool fills, nothing is lent, and the
|
|
2411
|
+
* form layer draws and fills every widget.
|
|
2412
|
+
*/
|
|
2413
|
+
const objectNumberOf = (ref) => ref.kind === "objectNumber" ? ref.objectNumber : -1;
|
|
2414
|
+
function createWidgetLender(ctx, { fields, design, pictures, looks }) {
|
|
2415
|
+
const records = (0, _embedpdf_core.memo)(() => [
|
|
2416
|
+
design.lending(),
|
|
2417
|
+
fields.get().snapshot,
|
|
2418
|
+
fields.view().snapshot
|
|
2419
|
+
], (lending, confirmed, shown) => lending && confirmed && shown ? {
|
|
2420
|
+
confirmed: confirmed.widgets,
|
|
2421
|
+
shown: shown.widgets
|
|
2422
|
+
} : null);
|
|
2423
|
+
/**
|
|
2424
|
+
* Deleted widgets, as the form's ops: a widget leaves its field
|
|
2425
|
+
* (`deleteWidget`), and a field whose every widget goes is deleted whole
|
|
2426
|
+
* (a merged field and widget among them).
|
|
2427
|
+
*/
|
|
2428
|
+
const deleteOps = (deleted) => {
|
|
2429
|
+
const index = fields.view();
|
|
2430
|
+
const ops = [];
|
|
2431
|
+
const byField = /* @__PURE__ */ new Map();
|
|
2432
|
+
for (const row of deleted) {
|
|
2433
|
+
const field = fieldForWidget(index, objectNumberOf(row.ref));
|
|
2434
|
+
if (!field) {
|
|
2435
|
+
ops.push({
|
|
2436
|
+
type: "forms.deleteWidget",
|
|
2437
|
+
widget: row.ref
|
|
2438
|
+
});
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const entry = byField.get(fieldKeyOf(field)) ?? {
|
|
2442
|
+
field,
|
|
2443
|
+
widgets: []
|
|
2444
|
+
};
|
|
2445
|
+
entry.widgets.push(row);
|
|
2446
|
+
byField.set(fieldKeyOf(field), entry);
|
|
2447
|
+
}
|
|
2448
|
+
for (const { field, widgets } of byField.values()) {
|
|
2449
|
+
const going = new Set(widgets.map((row) => objectNumberOf(row.ref)));
|
|
2450
|
+
if (field.widgets.every((widget) => going.has(widget.objectNumber))) ops.push({
|
|
2451
|
+
type: "forms.delete",
|
|
2452
|
+
field: field.ref
|
|
2453
|
+
});
|
|
2454
|
+
else ops.push(...widgets.map((row) => ({
|
|
2455
|
+
type: "forms.deleteWidget",
|
|
2456
|
+
widget: row.ref
|
|
2457
|
+
})));
|
|
2458
|
+
}
|
|
2459
|
+
return ops;
|
|
2460
|
+
};
|
|
2461
|
+
return {
|
|
2462
|
+
owner: "form",
|
|
2463
|
+
records,
|
|
2464
|
+
opsOf(effects, model, before) {
|
|
2465
|
+
const ops = [];
|
|
2466
|
+
const deleted = [];
|
|
2467
|
+
for (const effect of effects) if (effect.type === "patch") {
|
|
2468
|
+
const ref = (0, _embedpdf_core_annotation.refOf)(model.byId[effect.id]);
|
|
2469
|
+
if (!ref) continue;
|
|
2470
|
+
const { subtype: _subtype, ...patch } = effect.patch;
|
|
2471
|
+
ops.push({
|
|
2472
|
+
type: "forms.updateWidget",
|
|
2473
|
+
widget: ref,
|
|
2474
|
+
patch
|
|
2475
|
+
});
|
|
2476
|
+
} else if (effect.type === "delete") {
|
|
2477
|
+
const record = before.byId[effect.id];
|
|
2478
|
+
if (record) deleted.push(record.annotation);
|
|
2479
|
+
}
|
|
2480
|
+
return [...ops, ...deleteOps(deleted)];
|
|
2481
|
+
},
|
|
2482
|
+
undoOf: (ops) => undoLookOf(fields.view(), ops),
|
|
2483
|
+
reorderOps: (page, refs, position) => [{
|
|
2484
|
+
type: "forms.reorderWidgets",
|
|
2485
|
+
page,
|
|
2486
|
+
widgets: [...refs],
|
|
2487
|
+
position
|
|
2488
|
+
}],
|
|
2489
|
+
authority: () => {
|
|
2490
|
+
const may = ctx.allows("doc.forms.modify");
|
|
2491
|
+
return {
|
|
2492
|
+
update: may,
|
|
2493
|
+
delete: may
|
|
2494
|
+
};
|
|
2495
|
+
},
|
|
2496
|
+
pictureEpoch: pictures.epoch,
|
|
2497
|
+
pictureVersion: pictures.versionOf,
|
|
2498
|
+
renderAppearances: pictures.render,
|
|
2499
|
+
pending: looks.isPending,
|
|
2500
|
+
painter: (record) => record.subtype === "widget" ? looks.painterOf(record) : null
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
//#endregion
|
|
13
2504
|
//#region src/field-selection.ts
|
|
14
2505
|
/**
|
|
15
|
-
* ISO field selection, written
|
|
16
|
-
* SubmitForm (Tables 239/240): a
|
|
2506
|
+
* ISO field selection, written once for ResetForm (Tables 241/242) and
|
|
2507
|
+
* SubmitForm (Tables 239/240): a name target selects the field and its
|
|
17
2508
|
* descendants (FQN dot-prefix — `parent` selects `parent.c1`, the rule the
|
|
18
2509
|
* old exact-match resolution violated); an objectNumber target selects that
|
|
19
2510
|
* field dictionary. Include mode keeps the selection; exclude mode keeps
|
|
20
2511
|
* the complement (a name excludes its subtree the same way it includes
|
|
21
|
-
* it). `targets === null` (the key
|
|
22
|
-
* is ignored; `[]` + include selects
|
|
2512
|
+
* it). `targets === null` (the key absent) selects everything and the flag
|
|
2513
|
+
* is ignored; `[]` + include selects nothing while `[]` + exclude selects
|
|
23
2514
|
* everything — presence and emptiness are different states.
|
|
24
2515
|
*
|
|
25
|
-
* `listed` reports whether a field was
|
|
2516
|
+
* `listed` reports whether a field was addressed by the target list
|
|
26
2517
|
* (directly or through its subtree) — the "explicitly listed" predicate the
|
|
27
2518
|
* submit honesty diagnostics key on.
|
|
28
2519
|
*/
|
|
@@ -31,17 +2522,17 @@ const resolveFieldSelection = (all, targets, exclude) => {
|
|
|
31
2522
|
selected: [...all],
|
|
32
2523
|
listed: () => false
|
|
33
2524
|
};
|
|
34
|
-
const names = targets.filter((
|
|
35
|
-
const objectNumbers = new Set(targets.filter((
|
|
36
|
-
const listed = (field) => field.ref.kind === "objectNumber" && objectNumbers.has(field.ref.
|
|
2525
|
+
const names = targets.filter((target) => target.kind === "name").map((target) => target.name);
|
|
2526
|
+
const objectNumbers = new Set(targets.filter((target) => target.kind === "objectNumber").map((target) => target.objectNumber));
|
|
2527
|
+
const listed = (field) => field.ref.kind === "objectNumber" && objectNumbers.has(field.ref.objectNumber) || names.some((name) => field.name === name || field.name.startsWith(`${name}.`));
|
|
37
2528
|
return {
|
|
38
|
-
selected: exclude ? all.filter((
|
|
2529
|
+
selected: exclude ? all.filter((field) => !listed(field)) : all.filter(listed),
|
|
39
2530
|
listed
|
|
40
2531
|
};
|
|
41
2532
|
};
|
|
42
2533
|
/**
|
|
43
2534
|
* Build the resolved submission dataset from a live field list (Tables
|
|
44
|
-
* 239/240 semantics; pure — the capability supplies a
|
|
2535
|
+
* 239/240 semantics; pure — the capability supplies a fresh engine read so
|
|
45
2536
|
* no model-lag class exists):
|
|
46
2537
|
*
|
|
47
2538
|
* - the unconditional **NoExport veto** — even an explicit include cannot
|
|
@@ -49,11 +2540,11 @@ const resolveFieldSelection = (all, targets, exclude) => {
|
|
|
49
2540
|
* observable, silent when swept in implicitly);
|
|
50
2541
|
* - **push-buttons and signatures** never contribute entries in v1 —
|
|
51
2542
|
* ISO's explicit-pushbutton-`/AP`-in-FDF rule is unrepresentable in the
|
|
52
|
-
* current DTO, so an
|
|
2543
|
+
* current DTO, so an explicitly listed one is diagnosed
|
|
53
2544
|
* (`submit-entry-unsupported`), never silently dropped;
|
|
54
2545
|
* - an **unsupported `/V` shape** is always diagnosed (silent omission
|
|
55
2546
|
* would be data loss);
|
|
56
|
-
* - a valueless field becomes a
|
|
2547
|
+
* - a valueless field becomes a name-only entry (`value: null`) only under
|
|
57
2548
|
* IncludeNoValueFields, else it is skipped (the ISO default);
|
|
58
2549
|
* - values are entry-faithful: scalar → string, array (multi-select list
|
|
59
2550
|
* box) → string[]; the submitted name is the fully qualified name.
|
|
@@ -71,7 +2562,7 @@ const buildSubmitEntries = (fields, intent, diagnose) => {
|
|
|
71
2562
|
});
|
|
72
2563
|
continue;
|
|
73
2564
|
}
|
|
74
|
-
if (field.
|
|
2565
|
+
if (field.noExport) {
|
|
75
2566
|
if (explicit) diagnose({
|
|
76
2567
|
code: "submit-entry-unsupported",
|
|
77
2568
|
message: `submit: '${field.name}' carries the NoExport flag — the veto is unconditional (ISO 12.7.6.2), skipped`
|
|
@@ -100,1241 +2591,461 @@ const buildSubmitEntries = (fields, intent, diagnose) => {
|
|
|
100
2591
|
return entries;
|
|
101
2592
|
};
|
|
102
2593
|
//#endregion
|
|
103
|
-
//#region src/
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
geom: {},
|
|
111
|
-
seq: 0
|
|
112
|
-
});
|
|
113
|
-
function index(snapshot) {
|
|
114
|
-
const byKey = {};
|
|
115
|
-
const byWidget = {};
|
|
116
|
-
snapshot.fields.forEach((field, i) => {
|
|
117
|
-
byKey[fieldKeyOf(field)] = i;
|
|
118
|
-
for (const w of field.widgets) if (w.annotObjectNumber > 0) byWidget[w.annotObjectNumber] = i;
|
|
119
|
-
});
|
|
120
|
-
return {
|
|
121
|
-
byKey,
|
|
122
|
-
byWidget
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function update(model, msg) {
|
|
126
|
-
switch (msg.t) {
|
|
127
|
-
case "snapshot": return {
|
|
128
|
-
...model,
|
|
129
|
-
snapshot: msg.snapshot,
|
|
130
|
-
...index(msg.snapshot),
|
|
131
|
-
writing: {},
|
|
132
|
-
seq: model.seq + 1
|
|
133
|
-
};
|
|
134
|
-
case "writeStart": return {
|
|
135
|
-
...model,
|
|
136
|
-
writing: {
|
|
137
|
-
...model.writing,
|
|
138
|
-
[msg.key]: true
|
|
139
|
-
},
|
|
140
|
-
seq: model.seq + 1
|
|
141
|
-
};
|
|
142
|
-
case "writeDone": {
|
|
143
|
-
if (!model.snapshot) return model;
|
|
144
|
-
const i = model.byKey[msg.key];
|
|
145
|
-
if (i === void 0) return model;
|
|
146
|
-
const fields = model.snapshot.fields.slice();
|
|
147
|
-
fields[i] = msg.field;
|
|
148
|
-
const { [msg.key]: _done, ...writing } = model.writing;
|
|
149
|
-
const snapshot = {
|
|
150
|
-
...model.snapshot,
|
|
151
|
-
fields
|
|
152
|
-
};
|
|
153
|
-
return {
|
|
154
|
-
...model,
|
|
155
|
-
snapshot,
|
|
156
|
-
...index(snapshot),
|
|
157
|
-
writing,
|
|
158
|
-
seq: model.seq + 1
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
case "writeFailed": {
|
|
162
|
-
const { [msg.key]: _failed, ...writing } = model.writing;
|
|
163
|
-
return {
|
|
164
|
-
...model,
|
|
165
|
-
writing,
|
|
166
|
-
seq: model.seq + 1
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
case "pageGeom": return {
|
|
170
|
-
...model,
|
|
171
|
-
geom: {
|
|
172
|
-
...model.geom,
|
|
173
|
-
[msg.pageObjectNumber]: msg.boxes
|
|
174
|
-
},
|
|
175
|
-
seq: model.seq + 1
|
|
176
|
-
};
|
|
177
|
-
case "clearGeom": {
|
|
178
|
-
if (msg.pageObjectNumber === void 0) return {
|
|
179
|
-
...model,
|
|
180
|
-
geom: {},
|
|
181
|
-
seq: model.seq + 1
|
|
182
|
-
};
|
|
183
|
-
const geom = { ...model.geom };
|
|
184
|
-
delete geom[msg.pageObjectNumber];
|
|
185
|
-
return {
|
|
186
|
-
...model,
|
|
187
|
-
geom,
|
|
188
|
-
seq: model.seq + 1
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const fieldByKey = (model, key) => {
|
|
194
|
-
const i = model.byKey[key];
|
|
195
|
-
return i === void 0 ? null : model.snapshot?.fields[i] ?? null;
|
|
196
|
-
};
|
|
197
|
-
const fieldForWidget = (model, annotObjectNumber) => {
|
|
198
|
-
const i = model.byWidget[annotObjectNumber];
|
|
199
|
-
return i === void 0 ? null : model.snapshot?.fields[i] ?? null;
|
|
200
|
-
};
|
|
201
|
-
const contains = (b, p) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
|
|
202
|
-
/**
|
|
203
|
-
* The widget under a content-space point, from the model's loaded geometry
|
|
204
|
-
* for that page (null when the page's geometry has not arrived, or nothing
|
|
205
|
-
* is there). Nested widgets resolve to the smallest containing box.
|
|
206
|
-
*/
|
|
207
|
-
const widgetAt = (model, pageObjectNumber, point) => {
|
|
208
|
-
const geom = model.geom[pageObjectNumber];
|
|
209
|
-
if (!geom) return null;
|
|
210
|
-
let best = null;
|
|
211
|
-
for (const [key, box] of Object.entries(geom)) {
|
|
212
|
-
if (!contains(box, point)) continue;
|
|
213
|
-
const annotObjectNumber = Number(key);
|
|
214
|
-
const field = fieldForWidget(model, annotObjectNumber);
|
|
215
|
-
if (!field) continue;
|
|
216
|
-
if (!best || box.width * box.height < best.box.width * best.box.height) best = {
|
|
217
|
-
annotObjectNumber,
|
|
218
|
-
field,
|
|
219
|
-
box
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return best;
|
|
2594
|
+
//#region src/write/reset.ts
|
|
2595
|
+
/** A reset of no fields: what the executor reports as inert. */
|
|
2596
|
+
const NOTHING_RESET = {
|
|
2597
|
+
status: "unchanged",
|
|
2598
|
+
scripted: false,
|
|
2599
|
+
uiEffects: [],
|
|
2600
|
+
diagnostics: []
|
|
223
2601
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
2602
|
+
/** A field ref as an action's target: by object number, or by full name. */
|
|
2603
|
+
const targetOf = (ref) => ref.kind === "objectNumber" ? {
|
|
2604
|
+
kind: "objectNumber",
|
|
2605
|
+
objectNumber: ref.objectNumber
|
|
2606
|
+
} : {
|
|
2607
|
+
kind: "name",
|
|
2608
|
+
name: ref.name
|
|
231
2609
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
2610
|
+
function createResetWrites(_ctx, services, { fill }) {
|
|
2611
|
+
const { fields, rights } = services;
|
|
2612
|
+
/**
|
|
2613
|
+
* Whether a reset puts this field back: the engine refuses push button and
|
|
2614
|
+
* signature refs, and another signer's fields are theirs.
|
|
2615
|
+
*/
|
|
2616
|
+
const resettable = (field) => field.family !== "pushbutton" && field.family !== "signature" && rights.mayFill(field);
|
|
2617
|
+
/**
|
|
2618
|
+
* Reset the selected fields, then recalculate, as one change. `origin` is carried through to
|
|
2619
|
+
* the surfaced recalculation results, so a ResetForm run by a page or hover trigger never
|
|
2620
|
+
* reports its alerts as a user action. `null` when it selects nothing; a refused reset throws:
|
|
2621
|
+
* none of it was written.
|
|
2622
|
+
*/
|
|
2623
|
+
const resetSelection = async (targets, exclude, origin, options) => {
|
|
2624
|
+
if (!fields.view().snapshot) await fields.settled();
|
|
2625
|
+
const { selected } = resolveFieldSelection(fields.view().snapshot?.fields ?? [], targets, exclude);
|
|
2626
|
+
const refs = selected.filter(resettable).map((field) => field.ref);
|
|
2627
|
+
if (refs.length === 0) return null;
|
|
2628
|
+
return fill({
|
|
2629
|
+
label: {
|
|
2630
|
+
key: "form.reset",
|
|
2631
|
+
count: refs.length
|
|
2632
|
+
},
|
|
2633
|
+
ops: [{
|
|
2634
|
+
type: "forms.reset",
|
|
2635
|
+
fields: refs
|
|
2636
|
+
}],
|
|
2637
|
+
scripts: { kind: "recalculate" },
|
|
2638
|
+
origin
|
|
2639
|
+
}, options);
|
|
247
2640
|
};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
value: field.value,
|
|
253
|
-
multiline: field.multiline,
|
|
254
|
-
password: field.password,
|
|
255
|
-
maxLength: field.maxLength,
|
|
256
|
-
comb: field.comb
|
|
257
|
-
};
|
|
258
|
-
case "checkbox": {
|
|
259
|
-
const toggle = field.widgets.find((w) => w.annotObjectNumber === annotObjectNumber);
|
|
260
|
-
return {
|
|
261
|
-
...base,
|
|
262
|
-
control: "toggle",
|
|
263
|
-
kind: "checkbox",
|
|
264
|
-
checked: field.checked,
|
|
265
|
-
onState: toggle && "onState" in toggle ? toggle.onState : "Yes"
|
|
266
|
-
};
|
|
2641
|
+
const reset = async (refs, options) => {
|
|
2642
|
+
for (const ref of refs ?? []) {
|
|
2643
|
+
const field = fieldByRef(fields.view(), ref);
|
|
2644
|
+
if (field) rights.assertMayFill(field, "form.reset");
|
|
267
2645
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
2646
|
+
if (refs && refs.length === 0) return { fields: [] };
|
|
2647
|
+
const outcome = await resetSelection(refs ? refs.map(targetOf) : null, false, "user", options);
|
|
2648
|
+
return { fields: outcome ? fieldsWithChangedValues(outcome.before, outcome.after) : [] };
|
|
2649
|
+
};
|
|
2650
|
+
return { api: {
|
|
2651
|
+
reset,
|
|
2652
|
+
resetFormAction: async (targets, exclude, origin = "user") => {
|
|
2653
|
+
const outcome = await resetSelection(targets, exclude, origin);
|
|
2654
|
+
if (!outcome) return NOTHING_RESET;
|
|
2655
|
+
const { result } = outcome;
|
|
2656
|
+
return result.status === "unchanged" ? {
|
|
2657
|
+
...result,
|
|
2658
|
+
status: "applied"
|
|
2659
|
+
} : result;
|
|
277
2660
|
}
|
|
278
|
-
|
|
279
|
-
...base,
|
|
280
|
-
control: "choice",
|
|
281
|
-
kind: "combo",
|
|
282
|
-
edit: field.edit,
|
|
283
|
-
multi: false,
|
|
284
|
-
options: field.options,
|
|
285
|
-
selected: field.value === "" ? [] : [field.value]
|
|
286
|
-
};
|
|
287
|
-
case "listbox": return {
|
|
288
|
-
...base,
|
|
289
|
-
control: "choice",
|
|
290
|
-
kind: "list",
|
|
291
|
-
edit: false,
|
|
292
|
-
multi: field.multiSelect,
|
|
293
|
-
options: field.options,
|
|
294
|
-
selected: field.selectedValues
|
|
295
|
-
};
|
|
296
|
-
case "pushbutton": return {
|
|
297
|
-
...base,
|
|
298
|
-
control: "button"
|
|
299
|
-
};
|
|
300
|
-
case "signature": return {
|
|
301
|
-
...base,
|
|
302
|
-
control: "signature",
|
|
303
|
-
signed: field.valueEntry.kind !== "none"
|
|
304
|
-
};
|
|
305
|
-
default: return null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Project one page's widgets into fill controls. Widgets whose geometry has
|
|
310
|
-
* not arrived yet (or that are direct objects with no join key) are skipped —
|
|
311
|
-
* the shell re-runs the projection when `pageGeom` lands.
|
|
312
|
-
*/
|
|
313
|
-
function fillItems(model, pageObjectNumber) {
|
|
314
|
-
const snapshot = model.snapshot;
|
|
315
|
-
const geom = model.geom[pageObjectNumber];
|
|
316
|
-
if (!snapshot || !geom) return [];
|
|
317
|
-
const items = [];
|
|
318
|
-
for (const field of snapshot.fields) for (const widget of field.widgets) {
|
|
319
|
-
if (widget.pageObjectNumber !== pageObjectNumber) continue;
|
|
320
|
-
const box = geom[widget.annotObjectNumber];
|
|
321
|
-
if (!box) continue;
|
|
322
|
-
const item = projectWidget(model, field, widget.annotObjectNumber, box);
|
|
323
|
-
if (item) items.push(item);
|
|
324
|
-
}
|
|
325
|
-
return items;
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Project a SINGLE widget by its annotation object number — the join the
|
|
329
|
-
* annotation-plane render layer uses (it owns a live box already, so the
|
|
330
|
-
* model's cached geometry is used only when it happens to be loaded; the
|
|
331
|
-
* item's semantics never depend on it). Null while the snapshot hasn't
|
|
332
|
-
* landed, or for families with no fill control.
|
|
333
|
-
*/
|
|
334
|
-
function fillItemForWidget(model, annotObjectNumber) {
|
|
335
|
-
const field = fieldForWidget(model, annotObjectNumber);
|
|
336
|
-
if (!field) return null;
|
|
337
|
-
const widget = field.widgets.find((w) => w.annotObjectNumber === annotObjectNumber);
|
|
338
|
-
if (!widget) return null;
|
|
339
|
-
const box = model.geom[widget.pageObjectNumber]?.[annotObjectNumber];
|
|
340
|
-
return projectWidget(model, field, annotObjectNumber, box);
|
|
2661
|
+
} };
|
|
341
2662
|
}
|
|
342
2663
|
//#endregion
|
|
343
|
-
//#region src/
|
|
344
|
-
/** The core serial queue under its historical form-plugin name. */
|
|
345
|
-
const createSerialMutationQueue = _embedpdf_core.createSerialQueue;
|
|
346
|
-
//#endregion
|
|
347
|
-
//#region src/capability.ts
|
|
348
|
-
/** PDF user-space rect (y-up) → content-space box (y-down, crop-relative). */
|
|
349
|
-
const toBox = (rect, crop) => ({
|
|
350
|
-
x: rect.left - crop.left,
|
|
351
|
-
y: crop.top - rect.top,
|
|
352
|
-
width: rect.right - rect.left,
|
|
353
|
-
height: rect.top - rect.bottom
|
|
354
|
-
});
|
|
355
|
-
const sameAnnotationRef = (left, right) => {
|
|
356
|
-
if (left.kind !== right.kind || left.pageObjectNumber !== right.pageObjectNumber) return false;
|
|
357
|
-
if (left.kind === "objectNumber" && right.kind === "objectNumber") return left.annotObjectNumber === right.annotObjectNumber;
|
|
358
|
-
if (left.kind === "nm" && right.kind === "nm") return left.nm === right.nm;
|
|
359
|
-
return left.kind === "index" && right.kind === "index" && left.index === right.index && left.revision === right.revision;
|
|
360
|
-
};
|
|
2664
|
+
//#region src/write/script-effects.ts
|
|
361
2665
|
/**
|
|
362
|
-
* The
|
|
363
|
-
*
|
|
364
|
-
*
|
|
2666
|
+
* The actions plugin's seams into the form plugin: the form-effects commit
|
|
2667
|
+
* sink (script `setValue`, `setDisplay` and reset effects are written through
|
|
2668
|
+
* the plugin that owns the fields, as one fill with the calculations they
|
|
2669
|
+
* cause) and the submit dataset resolver.
|
|
365
2670
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
ctx.dispatch({
|
|
370
|
-
type: "SET_MODEL",
|
|
371
|
-
model: update(model(), msg)
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
const refKeyOf = (key) => {
|
|
375
|
-
if (key.startsWith("obj:")) return {
|
|
376
|
-
kind: "objectNumber",
|
|
377
|
-
fieldObjectNumber: Number(key.slice(4))
|
|
378
|
-
};
|
|
379
|
-
return {
|
|
380
|
-
kind: "fqn",
|
|
381
|
-
name: key.slice(4)
|
|
382
|
-
};
|
|
383
|
-
};
|
|
384
|
-
const enqueueMutation = createSerialMutationQueue();
|
|
385
|
-
const actionsHost = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
|
|
386
|
-
const realm = actionsHost?.scriptRealm ?? null;
|
|
387
|
-
const scripting = realm && ctx.doc ? require_scripting.createFormScriptingController({
|
|
388
|
-
doc: ctx.doc,
|
|
389
|
-
document: () => ctx.document(),
|
|
390
|
-
transaction: realm.transaction.bind(realm),
|
|
391
|
-
budget: realm.budget
|
|
392
|
-
}) : null;
|
|
393
|
-
if (scripting) ctx.cleanup(() => scripting.dispose());
|
|
394
|
-
/** Every script surface (UI effects, diagnostics, errors) flows through
|
|
395
|
-
* the actions plugin's ONE port — origin/phase attached (D9). */
|
|
396
|
-
const surfaceViaActions = (result, origin) => {
|
|
397
|
-
actionsHost?.surfaceScriptCommit(result, {
|
|
398
|
-
origin,
|
|
399
|
-
realm: "document"
|
|
400
|
-
});
|
|
401
|
-
};
|
|
402
|
-
const can = (cap) => ctx.doc?.security.allows(cap) ?? false;
|
|
403
|
-
let refreshPromise = null;
|
|
404
|
-
const refresh = async (force = false) => {
|
|
405
|
-
const doc = ctx.doc;
|
|
406
|
-
if (!doc) return;
|
|
407
|
-
if (!can("doc.forms.read")) return;
|
|
408
|
-
if (refreshPromise) {
|
|
409
|
-
await refreshPromise;
|
|
410
|
-
if (!force) return;
|
|
411
|
-
}
|
|
412
|
-
refreshPromise = doc.forms.list().then((snapshot) => apply({
|
|
413
|
-
t: "snapshot",
|
|
414
|
-
snapshot
|
|
415
|
-
})).catch((err) => {
|
|
416
|
-
console.warn("[form] form snapshot failed to load", err);
|
|
417
|
-
}).finally(() => {
|
|
418
|
-
refreshPromise = null;
|
|
419
|
-
});
|
|
420
|
-
return refreshPromise;
|
|
421
|
-
};
|
|
422
|
-
const geomLoading = /* @__PURE__ */ new Set();
|
|
423
|
-
const ensureGeom = (pon) => {
|
|
424
|
-
const doc = ctx.doc;
|
|
425
|
-
if (!doc || geomLoading.has(pon) || model().geom[pon]) return;
|
|
426
|
-
const crop = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;
|
|
427
|
-
if (!crop) return;
|
|
428
|
-
geomLoading.add(pon);
|
|
429
|
-
doc.page(pon).annotations.list().then(({ annotations }) => {
|
|
430
|
-
const boxes = {};
|
|
431
|
-
for (const dto of annotations) {
|
|
432
|
-
if (dto.subtype !== "widget") continue;
|
|
433
|
-
const objectNumber = dto.ref.kind === "objectNumber" ? dto.ref.annotObjectNumber : 0;
|
|
434
|
-
if (objectNumber > 0) boxes[objectNumber] = toBox(dto.rect, crop);
|
|
435
|
-
}
|
|
436
|
-
apply({
|
|
437
|
-
t: "pageGeom",
|
|
438
|
-
pageObjectNumber: pon,
|
|
439
|
-
boxes
|
|
440
|
-
});
|
|
441
|
-
}).finally(() => {
|
|
442
|
-
geomLoading.delete(pon);
|
|
443
|
-
});
|
|
444
|
-
};
|
|
445
|
-
const widgetAt$1 = (pon, point) => {
|
|
446
|
-
const m = model();
|
|
447
|
-
ensureGeom(pon);
|
|
448
|
-
if (m.geom[pon]) return widgetAt(m, pon, point);
|
|
449
|
-
if (!annotationHost) return null;
|
|
450
|
-
let best = null;
|
|
451
|
-
for (const item of annotationHost.pageItems(pon)) {
|
|
452
|
-
if (!item.subtype.startsWith("widget") || item.ref?.kind !== "objectNumber") continue;
|
|
453
|
-
const box = item.box;
|
|
454
|
-
if (!(point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height)) continue;
|
|
455
|
-
const field = fieldForWidget(m, item.ref.annotObjectNumber);
|
|
456
|
-
if (!field) continue;
|
|
457
|
-
if (!best || box.width * box.height < best.box.width * best.box.height) best = {
|
|
458
|
-
annotObjectNumber: item.ref.annotObjectNumber,
|
|
459
|
-
field,
|
|
460
|
-
box
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
return best;
|
|
464
|
-
};
|
|
465
|
-
const fuseFill = (item, fillable) => item === null || fillable || item.disabled ? item : {
|
|
466
|
-
...item,
|
|
467
|
-
disabled: true
|
|
468
|
-
};
|
|
469
|
-
const fillCache = /* @__PURE__ */ new Map();
|
|
470
|
-
const fillItems$1 = (pon) => {
|
|
471
|
-
const m = model();
|
|
472
|
-
const fillable = can("doc.forms.fill");
|
|
473
|
-
const hit = fillCache.get(pon);
|
|
474
|
-
if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.items;
|
|
475
|
-
const items = fillItems(m, pon).map((item) => fuseFill(item, fillable));
|
|
476
|
-
fillCache.set(pon, {
|
|
477
|
-
seq: m.seq,
|
|
478
|
-
fillable,
|
|
479
|
-
items
|
|
480
|
-
});
|
|
481
|
-
return items;
|
|
482
|
-
};
|
|
483
|
-
const fillItemCache = /* @__PURE__ */ new Map();
|
|
484
|
-
const fillItem = (annotObjectNumber) => {
|
|
485
|
-
const m = model();
|
|
486
|
-
const fillable = can("doc.forms.fill");
|
|
487
|
-
const hit = fillItemCache.get(annotObjectNumber);
|
|
488
|
-
if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.item;
|
|
489
|
-
const item = fuseFill(fillItemForWidget(m, annotObjectNumber), fillable);
|
|
490
|
-
fillItemCache.set(annotObjectNumber, {
|
|
491
|
-
seq: m.seq,
|
|
492
|
-
fillable,
|
|
493
|
-
item
|
|
494
|
-
});
|
|
495
|
-
return item;
|
|
496
|
-
};
|
|
497
|
-
const commitValueNow = async (ref, value) => {
|
|
498
|
-
const doc = ctx.doc;
|
|
499
|
-
if (!doc) throw new Error("no document");
|
|
500
|
-
if (scripting) {
|
|
501
|
-
const result = await scripting.commit(ref, value);
|
|
502
|
-
surfaceViaActions(result, "user");
|
|
503
|
-
if (result.effectsResult !== null) await refresh(true);
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
506
|
-
const result = await doc.forms.setValue(ref, value);
|
|
507
|
-
await refresh(true);
|
|
508
|
-
return {
|
|
509
|
-
status: result.changedWidgets.length > 0 ? "applied" : "unchanged",
|
|
510
|
-
scripted: false,
|
|
511
|
-
effectsResult: null,
|
|
512
|
-
uiEffects: [],
|
|
513
|
-
diagnostics: []
|
|
514
|
-
};
|
|
515
|
-
};
|
|
516
|
-
const write = (key, value) => {
|
|
517
|
-
if (!can("doc.forms.fill")) return Promise.reject(new _embedpdf_engine_core_runtime.PermissionDenied("doc.forms.fill", "form.fill"));
|
|
518
|
-
return enqueueMutation(async () => {
|
|
519
|
-
if (!ctx.doc) return;
|
|
520
|
-
apply({
|
|
521
|
-
t: "writeStart",
|
|
522
|
-
key
|
|
523
|
-
});
|
|
524
|
-
try {
|
|
525
|
-
const result = await commitValueNow(refKeyOf(key), value);
|
|
526
|
-
if (result.status === "rejected" || result.status === "failed") apply({
|
|
527
|
-
t: "writeFailed",
|
|
528
|
-
key
|
|
529
|
-
});
|
|
530
|
-
else if (result.effectsResult === null && result.scripted) apply({
|
|
531
|
-
t: "writeFailed",
|
|
532
|
-
key
|
|
533
|
-
});
|
|
534
|
-
} catch (err) {
|
|
535
|
-
apply({
|
|
536
|
-
t: "writeFailed",
|
|
537
|
-
key
|
|
538
|
-
});
|
|
539
|
-
throw err;
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
};
|
|
543
|
-
const annotationHost = ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
|
|
544
|
-
const annotationActivation = async (ref) => {
|
|
545
|
-
const doc = ctx.doc;
|
|
546
|
-
if (!doc) return null;
|
|
547
|
-
const loaded = annotationHost?.get(ref);
|
|
548
|
-
if (loaded?.subtype === "widget") return loaded.actions?.activate ?? null;
|
|
549
|
-
const { annotations } = await doc.page(ref.pageObjectNumber).annotations.list();
|
|
550
|
-
const annotation = annotations.find((candidate) => sameAnnotationRef(candidate.ref, ref));
|
|
551
|
-
return annotation?.subtype === "widget" ? annotation.actions?.activate ?? null : null;
|
|
552
|
-
};
|
|
553
|
-
const activateWidgetNow = async (key, annotationRef) => {
|
|
554
|
-
if (!ctx.doc) throw new Error("no document");
|
|
555
|
-
if (!scripting) return {
|
|
556
|
-
status: "unchanged",
|
|
557
|
-
scripted: false,
|
|
558
|
-
effectsResult: null,
|
|
559
|
-
uiEffects: [],
|
|
560
|
-
diagnostics: []
|
|
561
|
-
};
|
|
562
|
-
const action = await annotationActivation(annotationRef);
|
|
563
|
-
if (!action) return {
|
|
564
|
-
status: "unchanged",
|
|
565
|
-
scripted: true,
|
|
566
|
-
effectsResult: null,
|
|
567
|
-
uiEffects: [],
|
|
568
|
-
diagnostics: []
|
|
569
|
-
};
|
|
570
|
-
const result = await scripting.activate(refKeyOf(key), action);
|
|
571
|
-
surfaceViaActions(result, "user");
|
|
572
|
-
if (result.effectsResult !== null) await refresh(true);
|
|
573
|
-
return result;
|
|
574
|
-
};
|
|
575
|
-
const NOT_SCRIPTED = {
|
|
576
|
-
status: "unchanged",
|
|
577
|
-
scripted: false,
|
|
578
|
-
effectsResult: null,
|
|
579
|
-
uiEffects: [],
|
|
580
|
-
diagnostics: []
|
|
581
|
-
};
|
|
582
|
-
/**
|
|
583
|
-
* The shared reset core: one effects batch → refresh → V/C/F
|
|
584
|
-
* recalculation when the transaction port is present. Ridden by BOTH
|
|
585
|
-
* doors — the /ResetForm executor and the public `reset(key)` — so the
|
|
586
|
-
* two can never diverge again (the "reset() asymmetry" fix). `origin` is
|
|
587
|
-
* PRESERVED through recalculation surfacing: a lifecycle/hover ResetForm
|
|
588
|
-
* can no longer launder its alerts into user origin.
|
|
589
|
-
*/
|
|
590
|
-
const applyResetBatch = async (refs, origin) => {
|
|
591
|
-
const doc = ctx.doc;
|
|
592
|
-
if (!doc) throw new Error("no document");
|
|
593
|
-
if (!doc.forms.applyEffects) {
|
|
594
|
-
const result = {
|
|
595
|
-
...NOT_SCRIPTED,
|
|
596
|
-
diagnostics: [{
|
|
597
|
-
code: "unsupported-api",
|
|
598
|
-
message: "this engine has no form-effects batch door"
|
|
599
|
-
}]
|
|
600
|
-
};
|
|
601
|
-
surfaceViaActions(result, origin);
|
|
602
|
-
return result;
|
|
603
|
-
}
|
|
604
|
-
if (refs.length === 0) return NOT_SCRIPTED;
|
|
605
|
-
const effectsResult = await doc.forms.applyEffects([{
|
|
606
|
-
kind: "reset",
|
|
607
|
-
refs
|
|
608
|
-
}]);
|
|
609
|
-
const resetFailed = effectsResult.results.some((entry) => entry.status === "failed" || entry.status === "rejected");
|
|
610
|
-
await refresh(true);
|
|
611
|
-
if (resetFailed) return {
|
|
612
|
-
status: "failed",
|
|
613
|
-
scripted: false,
|
|
614
|
-
effectsResult,
|
|
615
|
-
uiEffects: [],
|
|
616
|
-
diagnostics: []
|
|
617
|
-
};
|
|
618
|
-
let recalc = null;
|
|
619
|
-
if (scripting) {
|
|
620
|
-
recalc = await scripting.recalculate();
|
|
621
|
-
surfaceViaActions(recalc, origin);
|
|
622
|
-
if (recalc.effectsResult !== null) await refresh(true);
|
|
623
|
-
}
|
|
624
|
-
return {
|
|
625
|
-
status: "applied",
|
|
626
|
-
scripted: recalc !== null,
|
|
627
|
-
effectsResult,
|
|
628
|
-
uiEffects: recalc?.uiEffects ?? [],
|
|
629
|
-
diagnostics: recalc?.diagnostics ?? [],
|
|
630
|
-
...recalc?.error ? { error: recalc.error } : {}
|
|
631
|
-
};
|
|
632
|
-
};
|
|
633
|
-
const resetFormAction = (targets, exclude, origin = "user") => enqueueMutation(async () => {
|
|
634
|
-
const doc = ctx.doc;
|
|
635
|
-
if (!doc) throw new Error("no document");
|
|
636
|
-
const { selected } = resolveFieldSelection((await doc.forms.list()).fields, targets, exclude);
|
|
637
|
-
const resettable = selected.filter((f) => f.family !== "pushbutton" && f.family !== "signature");
|
|
638
|
-
return applyResetBatch(resettable.map((f) => f.ref), origin);
|
|
639
|
-
});
|
|
2671
|
+
/** The fields a script's effects write to. */
|
|
2672
|
+
const refsOf = (effects) => effects.flatMap((effect) => effect.kind === "reset" ? effect.refs : [effect.ref]);
|
|
2673
|
+
function createScriptEffects(ctx, { fields, rights, enqueue }, { fillSoon }) {
|
|
640
2674
|
/**
|
|
641
|
-
* The submit dataset
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
* this door only supplies the live fields and assembles the request.
|
|
2675
|
+
* The submit dataset: the fields as the user sees them once every fill on its way is
|
|
2676
|
+
* answered, then the pure ISO builder. Selection, veto and value semantics live in
|
|
2677
|
+
* `field-selection.ts`; this only supplies the fields and assembles the request.
|
|
645
2678
|
*/
|
|
646
|
-
const resolveSubmitDataset = async (intent,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
2679
|
+
const resolveSubmitDataset = async (intent, actionContext, diagnose) => {
|
|
2680
|
+
await enqueue.idle();
|
|
2681
|
+
await ctx.changes.whenSettled();
|
|
2682
|
+
if (!fields.view().snapshot) await fields.settled();
|
|
650
2683
|
return {
|
|
651
2684
|
url: intent.url,
|
|
652
|
-
method: intent.method,
|
|
653
|
-
format: intent.format,
|
|
654
|
-
flagsRaw: intent.flagsRaw,
|
|
655
|
-
...intent.charSet === void 0 ? {} : { charSet: intent.charSet },
|
|
656
|
-
entries: buildSubmitEntries(snapshot
|
|
657
|
-
origin:
|
|
658
|
-
event:
|
|
659
|
-
};
|
|
660
|
-
};
|
|
661
|
-
const widgetSource = (key, annotationRef) => ({
|
|
662
|
-
kind: "widget",
|
|
663
|
-
field: refKeyOf(key),
|
|
664
|
-
annotation: annotationRef,
|
|
665
|
-
pon: annotationRef.pageObjectNumber
|
|
666
|
-
});
|
|
667
|
-
/** One shared hover pump (Exit→Enter as one ordered pair; intermediates
|
|
668
|
-
* skipped), created on first use against the resolved actions capability. */
|
|
669
|
-
let widgetPump = null;
|
|
670
|
-
const widgetHoverPump = (actions) => {
|
|
671
|
-
widgetPump ?? (widgetPump = (0, _embedpdf_plugin_actions_contract.createHoverPump)(actions.dispatch));
|
|
672
|
-
return widgetPump;
|
|
673
|
-
};
|
|
674
|
-
const widgetHoverFlags = (annotationRef) => {
|
|
675
|
-
const loaded = annotationHost?.get(annotationRef);
|
|
676
|
-
if (loaded?.subtype !== "widget") return null;
|
|
677
|
-
return {
|
|
678
|
-
enter: Boolean(loaded.actions?.cursorEnter?.root),
|
|
679
|
-
exit: Boolean(loaded.actions?.cursorExit?.root)
|
|
680
|
-
};
|
|
681
|
-
};
|
|
682
|
-
const nudgeAnnotations = (pons) => {
|
|
683
|
-
if (!annotationHost) return;
|
|
684
|
-
for (const pon of new Set(pons)) annotationHost.reloadPage(pon);
|
|
685
|
-
};
|
|
686
|
-
/** Content-space box → PDF rect (inverse of `toBox`). */
|
|
687
|
-
const toPdfRect = (box, crop) => ({
|
|
688
|
-
left: box.x + crop.left,
|
|
689
|
-
top: crop.top - box.y,
|
|
690
|
-
right: box.x + crop.left + box.width,
|
|
691
|
-
bottom: crop.top - box.y - box.height
|
|
692
|
-
});
|
|
693
|
-
/** The page's content box (`{0,0,w,h}`), for page-bound placement math. */
|
|
694
|
-
const pageBox = (pon) => {
|
|
695
|
-
const crop = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;
|
|
696
|
-
return crop ? {
|
|
697
|
-
x: 0,
|
|
698
|
-
y: 0,
|
|
699
|
-
width: crop.right - crop.left,
|
|
700
|
-
height: crop.top - crop.bottom
|
|
701
|
-
} : null;
|
|
702
|
-
};
|
|
703
|
-
/** Deterministic, collision-free auto-name: `text_1`, `text_2`, … counted
|
|
704
|
-
* against the CURRENT snapshot (rename in the field panel). */
|
|
705
|
-
const autoName = (family) => {
|
|
706
|
-
const names = new Set((model().snapshot?.fields ?? []).map((f) => f.name));
|
|
707
|
-
let n = 1;
|
|
708
|
-
while (names.has(`${family}_${n}`)) n++;
|
|
709
|
-
return `${family}_${n}`;
|
|
710
|
-
};
|
|
711
|
-
const placeFieldNow = async (input) => {
|
|
712
|
-
const doc = ctx.doc;
|
|
713
|
-
const pon = input.pageObjectNumber;
|
|
714
|
-
const crop = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;
|
|
715
|
-
if (!doc || !crop) throw new Error("[form] placeField: document/page not ready");
|
|
716
|
-
const page = pageBox(pon);
|
|
717
|
-
const x = Math.max(page.x, Math.min(input.box.x, page.width));
|
|
718
|
-
const y = Math.max(page.y, Math.min(input.box.y, page.height));
|
|
719
|
-
const box = {
|
|
720
|
-
x,
|
|
721
|
-
y,
|
|
722
|
-
width: Math.max(0, Math.min(input.box.x + input.box.width, page.width) - x),
|
|
723
|
-
height: Math.max(0, Math.min(input.box.y + input.box.height, page.height) - y)
|
|
724
|
-
};
|
|
725
|
-
if (box.width < 1 || box.height < 1) throw new Error("[form] placeField: degenerate box (size the box before placing)");
|
|
726
|
-
const { family, appearance } = input;
|
|
727
|
-
const name = autoName(family);
|
|
728
|
-
const placement = {
|
|
729
|
-
pageObjectNumber: pon,
|
|
730
|
-
rect: toPdfRect(box, crop),
|
|
731
|
-
...appearance ? { appearance } : {}
|
|
732
|
-
};
|
|
733
|
-
const draft = family === "radio" ? {
|
|
734
|
-
family,
|
|
735
|
-
name,
|
|
736
|
-
widgets: [{
|
|
737
|
-
...placement,
|
|
738
|
-
onState: "option1"
|
|
739
|
-
}]
|
|
740
|
-
} : family === "combobox" || family === "listbox" ? {
|
|
741
|
-
family,
|
|
742
|
-
name,
|
|
743
|
-
widget: placement,
|
|
744
|
-
options: [{
|
|
745
|
-
label: "Option 1",
|
|
746
|
-
value: "Option 1"
|
|
747
|
-
}, {
|
|
748
|
-
label: "Option 2",
|
|
749
|
-
value: "Option 2"
|
|
750
|
-
}]
|
|
751
|
-
} : {
|
|
752
|
-
family,
|
|
753
|
-
name,
|
|
754
|
-
widget: placement
|
|
755
|
-
};
|
|
756
|
-
const result = await doc.forms.createField(draft);
|
|
757
|
-
await refresh(true);
|
|
758
|
-
apply({
|
|
759
|
-
t: "clearGeom",
|
|
760
|
-
pageObjectNumber: pon
|
|
761
|
-
});
|
|
762
|
-
if (annotationHost) await annotationHost.reloadPage(pon);
|
|
763
|
-
const widget = result.field.widgets.find((w) => w.pageObjectNumber === pon) ?? null;
|
|
764
|
-
return {
|
|
765
|
-
field: result.field,
|
|
766
|
-
widget
|
|
2685
|
+
method: intent.method,
|
|
2686
|
+
format: intent.format,
|
|
2687
|
+
flagsRaw: intent.flagsRaw,
|
|
2688
|
+
...intent.charSet === void 0 ? {} : { charSet: intent.charSet },
|
|
2689
|
+
entries: buildSubmitEntries(fields.view().snapshot?.fields ?? [], intent, diagnose),
|
|
2690
|
+
origin: actionContext.origin,
|
|
2691
|
+
event: actionContext.event
|
|
767
2692
|
};
|
|
768
2693
|
};
|
|
769
|
-
|
|
770
|
-
const doc = ctx.doc;
|
|
771
|
-
if (!doc) return;
|
|
772
|
-
await doc.forms.updateField(refKeyOf(key), patch);
|
|
773
|
-
await refresh(true);
|
|
774
|
-
};
|
|
775
|
-
const deleteFieldNow = async (key) => {
|
|
776
|
-
const doc = ctx.doc;
|
|
777
|
-
if (!doc) return;
|
|
778
|
-
const pons = fieldByKey(model(), key)?.widgets.map((w) => w.pageObjectNumber).filter((p) => p > 0) ?? [];
|
|
779
|
-
await doc.forms.deleteField(refKeyOf(key));
|
|
780
|
-
await refresh(true);
|
|
781
|
-
for (const pon of new Set(pons)) apply({
|
|
782
|
-
t: "clearGeom",
|
|
783
|
-
pageObjectNumber: pon
|
|
784
|
-
});
|
|
785
|
-
nudgeAnnotations(pons);
|
|
786
|
-
};
|
|
787
|
-
const detachWidgetNow = async (key, annotObjectNumber) => {
|
|
788
|
-
const doc = ctx.doc;
|
|
789
|
-
if (!doc) return;
|
|
790
|
-
const widget = fieldByKey(model(), key)?.widgets.find((w) => w.annotObjectNumber === annotObjectNumber);
|
|
791
|
-
await doc.forms.detachWidget(refKeyOf(key), {
|
|
792
|
-
annotObjectNumber,
|
|
793
|
-
pageObjectNumber: widget?.pageObjectNumber ?? 0
|
|
794
|
-
});
|
|
795
|
-
await refresh(true);
|
|
796
|
-
if (widget && widget.pageObjectNumber > 0) {
|
|
797
|
-
apply({
|
|
798
|
-
t: "clearGeom",
|
|
799
|
-
pageObjectNumber: widget.pageObjectNumber
|
|
800
|
-
});
|
|
801
|
-
nudgeAnnotations([widget.pageObjectNumber]);
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
refresh();
|
|
805
|
-
return {
|
|
806
|
-
snapshot: () => model().snapshot,
|
|
807
|
-
refresh,
|
|
808
|
-
fillItems: fillItems$1,
|
|
809
|
-
fillItem,
|
|
810
|
-
ensureGeom,
|
|
811
|
-
field: (key) => fieldByKey(model(), key),
|
|
812
|
-
fieldForWidget: (annotObjectNumber) => fieldForWidget(model(), annotObjectNumber),
|
|
813
|
-
widgetAt: widgetAt$1,
|
|
814
|
-
setText: (key, value) => write(key, {
|
|
815
|
-
type: "text",
|
|
816
|
-
value
|
|
817
|
-
}),
|
|
818
|
-
toggle: (key, onState) => write(key, {
|
|
819
|
-
type: "toggle",
|
|
820
|
-
state: onState
|
|
821
|
-
}),
|
|
822
|
-
choose: (key, values) => write(key, {
|
|
823
|
-
type: "choice",
|
|
824
|
-
values
|
|
825
|
-
}),
|
|
826
|
-
reset: (key) => {
|
|
827
|
-
if (!can("doc.forms.fill")) return Promise.reject(new _embedpdf_engine_core_runtime.PermissionDenied("doc.forms.fill", "form.reset"));
|
|
828
|
-
return enqueueMutation(async () => {
|
|
829
|
-
const doc = ctx.doc;
|
|
830
|
-
if (!doc) return;
|
|
831
|
-
apply({
|
|
832
|
-
t: "writeStart",
|
|
833
|
-
key
|
|
834
|
-
});
|
|
835
|
-
try {
|
|
836
|
-
if (doc.forms.applyEffects) {
|
|
837
|
-
const result = await applyResetBatch([refKeyOf(key)], "user");
|
|
838
|
-
if (result.status === "failed") throw new Error(result.effectsResult?.results.find((entry) => entry.status === "failed" || entry.status === "rejected")?.error?.message ?? "reset failed");
|
|
839
|
-
const field = fieldByKey(model(), key);
|
|
840
|
-
if (field) apply({
|
|
841
|
-
t: "writeDone",
|
|
842
|
-
key,
|
|
843
|
-
field
|
|
844
|
-
});
|
|
845
|
-
else apply({
|
|
846
|
-
t: "writeFailed",
|
|
847
|
-
key
|
|
848
|
-
});
|
|
849
|
-
} else {
|
|
850
|
-
const result = await doc.forms.reset(refKeyOf(key));
|
|
851
|
-
apply({
|
|
852
|
-
t: "writeDone",
|
|
853
|
-
key,
|
|
854
|
-
field: result.field
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
} catch (err) {
|
|
858
|
-
apply({
|
|
859
|
-
t: "writeFailed",
|
|
860
|
-
key
|
|
861
|
-
});
|
|
862
|
-
throw err;
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
},
|
|
866
|
-
commitValue: (ref, value) => enqueueMutation(() => commitValueNow(ref, value)),
|
|
867
|
-
activateWidget: async (key, annotationRef) => {
|
|
868
|
-
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
869
|
-
if (actions) {
|
|
870
|
-
const result = await actions.dispatch({
|
|
871
|
-
scope: "activate",
|
|
872
|
-
ref: annotationRef,
|
|
873
|
-
pon: annotationRef.pageObjectNumber,
|
|
874
|
-
source: widgetSource(key, annotationRef)
|
|
875
|
-
});
|
|
876
|
-
if (!(result.status === "inert" && result.steps.length === 0 && result.diagnostics.length === 0)) return {
|
|
877
|
-
kind: "dispatched",
|
|
878
|
-
result
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
return {
|
|
882
|
-
kind: "form",
|
|
883
|
-
result: await enqueueMutation(() => activateWidgetNow(key, annotationRef))
|
|
884
|
-
};
|
|
885
|
-
},
|
|
886
|
-
notifyWidgetEvent: (key, annotationRef, event) => {
|
|
887
|
-
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
|
|
888
|
-
if (!actions) return;
|
|
889
|
-
const source = widgetSource(key, annotationRef);
|
|
890
|
-
if (event === "cursorEnter" || event === "cursorExit") {
|
|
891
|
-
if (event === "cursorExit") {
|
|
892
|
-
widgetHoverPump(actions).hover(null);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const flags = widgetHoverFlags(annotationRef);
|
|
896
|
-
if (flags && !flags.enter && !flags.exit) return;
|
|
897
|
-
widgetHoverPump(actions).hover({
|
|
898
|
-
ref: annotationRef,
|
|
899
|
-
pon: annotationRef.pageObjectNumber,
|
|
900
|
-
source,
|
|
901
|
-
...flags ? { events: flags } : {}
|
|
902
|
-
});
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
actions.dispatch({
|
|
906
|
-
scope: "annotation",
|
|
907
|
-
event,
|
|
908
|
-
ref: annotationRef,
|
|
909
|
-
pon: annotationRef.pageObjectNumber,
|
|
910
|
-
source
|
|
911
|
-
});
|
|
912
|
-
},
|
|
913
|
-
resetFormAction,
|
|
2694
|
+
return { api: {
|
|
914
2695
|
resolveSubmitDataset,
|
|
915
2696
|
commitScriptFormEffects: async (effects) => {
|
|
916
|
-
const doc = ctx.doc;
|
|
917
|
-
if (!doc?.forms.applyEffects) return {
|
|
918
|
-
results: effects.map((_, index) => ({
|
|
919
|
-
index,
|
|
920
|
-
status: "failed",
|
|
921
|
-
fields: [],
|
|
922
|
-
changedWidgets: [],
|
|
923
|
-
error: {
|
|
924
|
-
code: "NotSupported",
|
|
925
|
-
message: "no form-effects batch door"
|
|
926
|
-
}
|
|
927
|
-
})),
|
|
928
|
-
changedWidgets: [],
|
|
929
|
-
meta: null
|
|
930
|
-
};
|
|
931
|
-
let result;
|
|
932
2697
|
try {
|
|
933
|
-
|
|
2698
|
+
for (const ref of refsOf(effects)) {
|
|
2699
|
+
const field = fieldByRef(fields.view(), ref);
|
|
2700
|
+
if (field) rights.assertMayFill(field, "form.fill");
|
|
2701
|
+
}
|
|
2702
|
+
const recalculates = effects.some((effect) => effect.kind === "setValue" || effect.kind === "reset");
|
|
2703
|
+
fillSoon({
|
|
2704
|
+
ops: (0, _embedpdf_engine_core_runtime.formEffectOps)(effects),
|
|
2705
|
+
scripts: recalculates ? { kind: "recalculate" } : null,
|
|
2706
|
+
origin: "user"
|
|
2707
|
+
});
|
|
2708
|
+
return { status: "applied" };
|
|
934
2709
|
} catch (error) {
|
|
935
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
936
2710
|
return {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
status: "failed",
|
|
940
|
-
fields: [],
|
|
941
|
-
changedWidgets: [],
|
|
942
|
-
error: {
|
|
943
|
-
code: "Refused",
|
|
944
|
-
message
|
|
945
|
-
}
|
|
946
|
-
})),
|
|
947
|
-
changedWidgets: [],
|
|
948
|
-
meta: null
|
|
2711
|
+
status: "failed",
|
|
2712
|
+
error: (0, _embedpdf_engine_core_runtime.serializeError)(error).message
|
|
949
2713
|
};
|
|
950
2714
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const pons = new Set(result.changedWidgets.map((widget) => widget.pageObjectNumber).filter((pon) => pon > 0));
|
|
954
|
-
for (const pon of pons) await annotationHost.reloadPage(pon);
|
|
955
|
-
}
|
|
956
|
-
return result;
|
|
957
|
-
},
|
|
958
|
-
setValue: (ref, value) => enqueueMutation(async () => {
|
|
959
|
-
const doc = ctx.doc;
|
|
960
|
-
if (!doc) throw new Error("no document");
|
|
961
|
-
const result = await doc.forms.setValue(ref, value);
|
|
962
|
-
await refresh(true);
|
|
963
|
-
return result;
|
|
964
|
-
}),
|
|
965
|
-
exportData: async (format = "xfdf") => {
|
|
966
|
-
const doc = ctx.doc;
|
|
967
|
-
if (!doc) throw new Error("no document");
|
|
968
|
-
return doc.forms.exportData(format);
|
|
969
|
-
},
|
|
970
|
-
importData: (data, format) => enqueueMutation(async () => {
|
|
971
|
-
const doc = ctx.doc;
|
|
972
|
-
if (!doc) throw new Error("no document");
|
|
973
|
-
const result = await doc.forms.importData(data, format);
|
|
974
|
-
apply({
|
|
975
|
-
t: "snapshot",
|
|
976
|
-
snapshot: result.snapshot
|
|
977
|
-
});
|
|
978
|
-
return result;
|
|
979
|
-
}),
|
|
980
|
-
repair: (repairOptions) => enqueueMutation(async () => {
|
|
981
|
-
const doc = ctx.doc;
|
|
982
|
-
if (!doc) throw new Error("no document");
|
|
983
|
-
const result = await doc.forms.repair(repairOptions);
|
|
984
|
-
await refresh(true);
|
|
985
|
-
return result;
|
|
986
|
-
}),
|
|
987
|
-
placeField: (input) => enqueueMutation(() => placeFieldNow(input)),
|
|
988
|
-
pageBox,
|
|
989
|
-
updateField: (key, patch) => enqueueMutation(() => updateFieldNow(key, patch)),
|
|
990
|
-
deleteField: (key) => enqueueMutation(() => deleteFieldNow(key)),
|
|
991
|
-
detachWidget: (key, annotObjectNumber) => enqueueMutation(() => detachWidgetNow(key, annotObjectNumber)),
|
|
992
|
-
canRead: () => can("doc.forms.read"),
|
|
993
|
-
canFill: () => can("doc.forms.fill"),
|
|
994
|
-
canDesign: () => can("doc.forms.modify")
|
|
995
|
-
};
|
|
2715
|
+
}
|
|
2716
|
+
} };
|
|
996
2717
|
}
|
|
997
2718
|
//#endregion
|
|
998
|
-
//#region src/
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
if (!doc) return;
|
|
1010
|
-
let refreshing = false;
|
|
1011
|
-
const refresh = async () => {
|
|
1012
|
-
if (refreshing) return;
|
|
1013
|
-
refreshing = true;
|
|
1014
|
-
try {
|
|
1015
|
-
const snapshot = await doc.forms.list();
|
|
1016
|
-
ctx.dispatch({
|
|
1017
|
-
type: "SET_MODEL",
|
|
1018
|
-
model: update(ctx.getState().model, {
|
|
1019
|
-
t: "snapshot",
|
|
1020
|
-
snapshot
|
|
1021
|
-
})
|
|
1022
|
-
});
|
|
1023
|
-
} finally {
|
|
1024
|
-
refreshing = false;
|
|
1025
|
-
}
|
|
2719
|
+
//#region src/write/typing.ts
|
|
2720
|
+
function createTyping(ctx, { keyOf }, setText) {
|
|
2721
|
+
/** The text typed in each field and not written yet, by field key. */
|
|
2722
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
2723
|
+
/** Write the field's draft, if it has one; `null` when there is nothing to write. */
|
|
2724
|
+
const commitDraftText = async (field) => {
|
|
2725
|
+
const key = keyOf(field);
|
|
2726
|
+
const draft = drafts.get(key);
|
|
2727
|
+
if (!draft) return null;
|
|
2728
|
+
drafts.delete(key);
|
|
2729
|
+
return setText(draft.field, draft.text);
|
|
1026
2730
|
};
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
refresh();
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
if (event.type === "document.versioned" || event.type === "signature.completed") {
|
|
1033
|
-
refresh();
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (!event.type.startsWith("form.") || !("origin" in event)) return;
|
|
1037
|
-
const structural = STRUCTURAL.has(event.type);
|
|
1038
|
-
if (event.origin.kind !== "remote" && !structural) return;
|
|
1039
|
-
if (structural) ctx.dispatch({
|
|
1040
|
-
type: "SET_MODEL",
|
|
1041
|
-
model: update(ctx.getState().model, { t: "clearGeom" })
|
|
1042
|
-
});
|
|
1043
|
-
refresh();
|
|
2731
|
+
ctx.onSettle(async () => {
|
|
2732
|
+
await Promise.all([...drafts.values()].map((draft) => commitDraftText(draft.field)));
|
|
1044
2733
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const FIELD_CHROME = {
|
|
1052
|
-
interiorColor: "#ffffff",
|
|
1053
|
-
color: "#6b7280",
|
|
1054
|
-
strokeWidth: 1
|
|
1055
|
-
};
|
|
1056
|
-
const FORM_TOOLS = [
|
|
1057
|
-
{
|
|
1058
|
-
id: "form-text",
|
|
1059
|
-
family: "text",
|
|
1060
|
-
visualKind: "widget-text",
|
|
1061
|
-
clickCreate: {
|
|
1062
|
-
width: 160,
|
|
1063
|
-
height: 24
|
|
1064
|
-
},
|
|
1065
|
-
defaults: {
|
|
1066
|
-
...FIELD_CHROME,
|
|
1067
|
-
fontSize: 12
|
|
1068
|
-
},
|
|
1069
|
-
cursor: "crosshair"
|
|
1070
|
-
},
|
|
1071
|
-
{
|
|
1072
|
-
id: "form-checkbox",
|
|
1073
|
-
family: "checkbox",
|
|
1074
|
-
visualKind: "widget-toggle",
|
|
1075
|
-
clickCreate: {
|
|
1076
|
-
width: 18,
|
|
1077
|
-
height: 18
|
|
1078
|
-
},
|
|
1079
|
-
defaults: FIELD_CHROME,
|
|
1080
|
-
cursor: "crosshair"
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
id: "form-radio",
|
|
1084
|
-
family: "radio",
|
|
1085
|
-
visualKind: "widget-toggle",
|
|
1086
|
-
clickCreate: {
|
|
1087
|
-
width: 18,
|
|
1088
|
-
height: 18
|
|
1089
|
-
},
|
|
1090
|
-
defaults: FIELD_CHROME,
|
|
1091
|
-
cursor: "crosshair"
|
|
1092
|
-
},
|
|
1093
|
-
{
|
|
1094
|
-
id: "form-combobox",
|
|
1095
|
-
family: "combobox",
|
|
1096
|
-
visualKind: "widget-choice",
|
|
1097
|
-
clickCreate: {
|
|
1098
|
-
width: 140,
|
|
1099
|
-
height: 24
|
|
1100
|
-
},
|
|
1101
|
-
defaults: {
|
|
1102
|
-
...FIELD_CHROME,
|
|
1103
|
-
fontSize: 12
|
|
1104
|
-
},
|
|
1105
|
-
cursor: "crosshair"
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
id: "form-listbox",
|
|
1109
|
-
family: "listbox",
|
|
1110
|
-
visualKind: "widget-choice",
|
|
1111
|
-
clickCreate: {
|
|
1112
|
-
width: 140,
|
|
1113
|
-
height: 72
|
|
1114
|
-
},
|
|
1115
|
-
defaults: {
|
|
1116
|
-
...FIELD_CHROME,
|
|
1117
|
-
fontSize: 12
|
|
1118
|
-
},
|
|
1119
|
-
cursor: "crosshair"
|
|
1120
|
-
},
|
|
1121
|
-
{
|
|
1122
|
-
id: "form-signature",
|
|
1123
|
-
family: "signature",
|
|
1124
|
-
visualKind: "widget-box",
|
|
1125
|
-
clickCreate: {
|
|
1126
|
-
width: 160,
|
|
1127
|
-
height: 48
|
|
2734
|
+
return { api: {
|
|
2735
|
+
draftText: (field, text) => {
|
|
2736
|
+
drafts.set(keyOf(field), {
|
|
2737
|
+
field,
|
|
2738
|
+
text
|
|
2739
|
+
});
|
|
1128
2740
|
},
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
2741
|
+
commitDraftText,
|
|
2742
|
+
discardDraftText: (field) => {
|
|
2743
|
+
drafts.delete(keyOf(field));
|
|
2744
|
+
}
|
|
2745
|
+
} };
|
|
2746
|
+
}
|
|
1134
2747
|
//#endregion
|
|
1135
|
-
//#region src/
|
|
1136
|
-
const rectFrom = (a, b) => ({
|
|
1137
|
-
x: Math.min(a.x, b.x),
|
|
1138
|
-
y: Math.min(a.y, b.y),
|
|
1139
|
-
width: Math.abs(b.x - a.x),
|
|
1140
|
-
height: Math.abs(b.y - a.y)
|
|
1141
|
-
});
|
|
2748
|
+
//#region src/write/values.ts
|
|
1142
2749
|
/**
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
* the commit creates field + widget through `doc.forms.placeField`, styled
|
|
1147
|
-
* from the tool's live defaults. The tool stays active for repeat placement
|
|
1148
|
-
* (v2 rubber-stamp feel) and the fresh widget is auto-selected.
|
|
1149
|
-
*
|
|
1150
|
-
* Gesture rules match the annotation handlers: page-anchored via the sample
|
|
1151
|
-
* projection (the box keeps sizing along the edge when the cursor overshoots
|
|
1152
|
-
* or crosses a gap), the UP sample is the final point, and a CLICK means
|
|
1153
|
-
* width AND height under the shared threshold — a thin 100×2 drag is a drag.
|
|
2750
|
+
* Value writes, each one fill (write/fill.ts): one field through the form's scripts (when the
|
|
2751
|
+
* `validation` setting runs them), and the batch verbs, whose plain values go as one change
|
|
2752
|
+
* with one recalculation, as a script's `field.value` assignments do in Acrobat.
|
|
1154
2753
|
*/
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
2754
|
+
/** A batch that stops when its caller cancels it. */
|
|
2755
|
+
function throwIfCancelled(options) {
|
|
2756
|
+
if (options?.signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "form", "operation cancelled", { cause: options.signal.reason });
|
|
2757
|
+
}
|
|
2758
|
+
const setValueOp = (field, value) => ({
|
|
2759
|
+
type: "forms.setValue",
|
|
2760
|
+
field,
|
|
2761
|
+
value
|
|
2762
|
+
});
|
|
2763
|
+
function createValueWrites(_ctx, services, { fill }) {
|
|
2764
|
+
const { validationRejected } = services.events;
|
|
2765
|
+
const { fields, rights } = services;
|
|
2766
|
+
/** One field's value through its scripts: shown at once, written as one change. */
|
|
2767
|
+
const commit = async (ref, value, operation, options) => {
|
|
2768
|
+
const known = fieldByRef(fields.view(), ref);
|
|
2769
|
+
if (known) rights.assertMayFill(known, operation);
|
|
2770
|
+
const { result } = await fill({
|
|
2771
|
+
ops: [setValueOp(ref, value)],
|
|
2772
|
+
scripts: {
|
|
2773
|
+
kind: "commit",
|
|
2774
|
+
field: ref,
|
|
2775
|
+
value
|
|
2776
|
+
},
|
|
2777
|
+
origin: "user"
|
|
2778
|
+
}, options);
|
|
2779
|
+
if (result.status === "failed") throw failedFill(result);
|
|
2780
|
+
const field = fieldByRef(fields.get(), ref);
|
|
2781
|
+
if (!field) throw new _embedpdf_core.PluginError("not-found", "form", "no form field has this ref");
|
|
2782
|
+
if (result.status === "rejected") {
|
|
2783
|
+
validationRejected.emit({
|
|
2784
|
+
field,
|
|
2785
|
+
issues: result.diagnostics
|
|
2786
|
+
});
|
|
2787
|
+
return {
|
|
2788
|
+
field,
|
|
2789
|
+
status: "rejected"
|
|
1168
2790
|
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const
|
|
1187
|
-
if (!
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
})
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2791
|
+
}
|
|
2792
|
+
return {
|
|
2793
|
+
field,
|
|
2794
|
+
status: result.status
|
|
2795
|
+
};
|
|
2796
|
+
};
|
|
2797
|
+
/**
|
|
2798
|
+
* Plain values for several fields, as one change with one recalculation. A value its field
|
|
2799
|
+
* can't take, or a field the session may not fill in, fails on its own; the rest go together,
|
|
2800
|
+
* and fail together when the engine refuses them.
|
|
2801
|
+
*/
|
|
2802
|
+
const fillPlain = async (entries, options) => {
|
|
2803
|
+
if (!fields.view().snapshot) await fields.settled();
|
|
2804
|
+
const index = fields.view();
|
|
2805
|
+
const accepted = [];
|
|
2806
|
+
const failed = [];
|
|
2807
|
+
for (const entry of entries) try {
|
|
2808
|
+
const field = fieldByRef(index, entry.ref);
|
|
2809
|
+
if (!field) throw new _embedpdf_core.PluginError("not-found", "form", "no form field has this ref");
|
|
2810
|
+
rights.assertMayFill(field, "form.setValues");
|
|
2811
|
+
(0, _embedpdf_engine_core_runtime.fieldValueWriteOf)(field, entry.value);
|
|
2812
|
+
accepted.push({
|
|
2813
|
+
ref: field.ref,
|
|
2814
|
+
value: entry.value
|
|
2815
|
+
});
|
|
2816
|
+
} catch (error) {
|
|
2817
|
+
failed.push({
|
|
2818
|
+
ref: entry.ref,
|
|
2819
|
+
error: (0, _embedpdf_core.toPluginErrorInfo)((0, _embedpdf_core.toPluginError)("form", error))
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
if (accepted.length === 0) return {
|
|
2823
|
+
applied: [],
|
|
2824
|
+
failed
|
|
2825
|
+
};
|
|
2826
|
+
throwIfCancelled(options);
|
|
2827
|
+
try {
|
|
2828
|
+
const { result } = await fill({
|
|
2829
|
+
ops: accepted.map((entry) => setValueOp(entry.ref, entry.value)),
|
|
2830
|
+
scripts: { kind: "recalculate" },
|
|
2831
|
+
origin: "user"
|
|
2832
|
+
}, options);
|
|
2833
|
+
if (result.status === "failed") throw failedFill(result);
|
|
2834
|
+
return {
|
|
2835
|
+
applied: accepted.map((entry) => entry.ref),
|
|
2836
|
+
failed
|
|
2837
|
+
};
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
const info = (0, _embedpdf_core.toPluginErrorInfo)((0, _embedpdf_core.toPluginError)("form", error));
|
|
2840
|
+
return {
|
|
2841
|
+
applied: [],
|
|
2842
|
+
failed: [...failed, ...accepted.map(({ ref }) => ({
|
|
2843
|
+
ref,
|
|
2844
|
+
error: info
|
|
2845
|
+
}))]
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
const setValues = async (entries, options) => {
|
|
2850
|
+
throwIfCancelled(options);
|
|
2851
|
+
const { applied, failed } = await fillPlain(entries, options);
|
|
2852
|
+
return {
|
|
2853
|
+
applied,
|
|
2854
|
+
skipped: [],
|
|
2855
|
+
failed
|
|
2856
|
+
};
|
|
2857
|
+
};
|
|
2858
|
+
const importValues = async (values, options) => {
|
|
2859
|
+
throwIfCancelled(options);
|
|
2860
|
+
if (!fields.view().snapshot) await fields.settled();
|
|
2861
|
+
const index = fields.view();
|
|
2862
|
+
const skipped = [];
|
|
2863
|
+
const entries = [];
|
|
2864
|
+
const names = /* @__PURE__ */ new Map();
|
|
2865
|
+
for (const [name, plain] of Object.entries(values)) {
|
|
2866
|
+
const field = fieldByRef(index, {
|
|
2867
|
+
kind: "fqn",
|
|
2868
|
+
name
|
|
2869
|
+
});
|
|
2870
|
+
if (!field) {
|
|
2871
|
+
skipped.push({
|
|
2872
|
+
ref: name,
|
|
2873
|
+
reason: "the form has no field with this name"
|
|
2874
|
+
});
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (!rights.mayFill(field)) {
|
|
2878
|
+
skipped.push({
|
|
2879
|
+
ref: name,
|
|
2880
|
+
reason: "you may not fill in this field"
|
|
2881
|
+
});
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const value = writeOfPlainValue(field, plain);
|
|
2885
|
+
if (!value) {
|
|
2886
|
+
skipped.push({
|
|
2887
|
+
ref: name,
|
|
2888
|
+
reason: `a ${field.family} field can't take this value`
|
|
1206
2889
|
});
|
|
1207
|
-
|
|
1208
|
-
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
entries.push({
|
|
2893
|
+
ref: field.ref,
|
|
2894
|
+
value
|
|
1209
2895
|
});
|
|
2896
|
+
names.set(JSON.stringify(field.ref), name);
|
|
1210
2897
|
}
|
|
2898
|
+
const { applied, failed } = await fillPlain(entries, options);
|
|
2899
|
+
return {
|
|
2900
|
+
applied,
|
|
2901
|
+
skipped,
|
|
2902
|
+
failed: failed.map(({ ref, error }) => ({
|
|
2903
|
+
ref: names.get(JSON.stringify(ref)) ?? "",
|
|
2904
|
+
error
|
|
2905
|
+
}))
|
|
2906
|
+
};
|
|
1211
2907
|
};
|
|
1212
|
-
|
|
1213
|
-
/**
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
width: policy.width,
|
|
1221
|
-
height: policy.height
|
|
2908
|
+
return {
|
|
2909
|
+
/** A text field's write, for the text being typed (write/typing.ts). */
|
|
2910
|
+
setText: (ref, text) => commit(ref, { value: text }, "form.setValue"),
|
|
2911
|
+
api: {
|
|
2912
|
+
setValue: (ref, value, options) => commit(ref, value, "form.setValue", options),
|
|
2913
|
+
setValues,
|
|
2914
|
+
importValues
|
|
2915
|
+
}
|
|
1222
2916
|
};
|
|
1223
2917
|
}
|
|
1224
2918
|
//#endregion
|
|
1225
|
-
//#region src/
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
2919
|
+
//#region src/controller.ts
|
|
2920
|
+
/**
|
|
2921
|
+
* The form controller: the composition root. It builds the plugin's services
|
|
2922
|
+
* once, wires each area with the services it declares, and assembles the
|
|
2923
|
+
* host capability from the areas' API slices. No behavior lives here: every
|
|
2924
|
+
* verb and read has a home in `read/`, `write/` or `sync/`.
|
|
2925
|
+
*/
|
|
2926
|
+
function createFormController(ctx) {
|
|
2927
|
+
const services = createServices(ctx);
|
|
2928
|
+
const { events, rights } = services;
|
|
2929
|
+
const settings = ctx.settings();
|
|
2930
|
+
const fields = createFieldReads(ctx, services);
|
|
2931
|
+
const widgets = createWidgetReads(ctx, services);
|
|
2932
|
+
const settingsReads = createFieldSettings(services);
|
|
2933
|
+
const fills = createFill(ctx, services);
|
|
2934
|
+
const values = createValueWrites(ctx, services, fills);
|
|
2935
|
+
const typing = createTyping(ctx, services, values.setText);
|
|
2936
|
+
const resets = createResetWrites(ctx, services, fills);
|
|
2937
|
+
const design = createFieldWrites(ctx, services);
|
|
2938
|
+
const interchange = createInterchange(ctx, services);
|
|
2939
|
+
const activation = createActivation(ctx, services, fills);
|
|
2940
|
+
const scripts = createScriptEffects(ctx, services, fills);
|
|
2941
|
+
const api = (0, _embedpdf_core.composeApi)("form", [
|
|
2942
|
+
settings.api,
|
|
2943
|
+
fields.api,
|
|
2944
|
+
widgets.api,
|
|
2945
|
+
settingsReads.api,
|
|
2946
|
+
values.api,
|
|
2947
|
+
typing.api,
|
|
2948
|
+
resets.api,
|
|
2949
|
+
design.api,
|
|
2950
|
+
interchange.api,
|
|
2951
|
+
activation.api,
|
|
2952
|
+
scripts.api,
|
|
2953
|
+
{
|
|
2954
|
+
canRead: () => ctx.allows("doc.forms.read"),
|
|
2955
|
+
canFill: (ref) => {
|
|
2956
|
+
const field = fieldByRef(services.fields.view(), ref);
|
|
2957
|
+
return field !== null && rights.mayFill(field);
|
|
2958
|
+
},
|
|
2959
|
+
canDesign: () => ctx.allows("doc.forms.modify"),
|
|
2960
|
+
canRestoreAttribution: () => ctx.allows("doc.forms.import"),
|
|
2961
|
+
canWriteScripts: () => ctx.allows("doc.forms.script"),
|
|
2962
|
+
onValueChanged: events.valueChanged.on,
|
|
2963
|
+
onFieldCreated: events.fieldCreated.on,
|
|
2964
|
+
onFieldUpdated: events.fieldUpdated.on,
|
|
2965
|
+
onFieldDeleted: events.fieldDeleted.on,
|
|
2966
|
+
onValidationRejected: events.validationRejected.on,
|
|
2967
|
+
onResynced: events.resynced.on
|
|
2968
|
+
}
|
|
2969
|
+
]);
|
|
2970
|
+
return {
|
|
2971
|
+
api,
|
|
2972
|
+
connect: () => connectForm(ctx, api, createWidgetLender(ctx, services))
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
1229
2975
|
//#endregion
|
|
1230
2976
|
//#region src/form.plugin.ts
|
|
1231
2977
|
/**
|
|
1232
|
-
* The form plugin: the
|
|
1233
|
-
* interaction hub.
|
|
1234
|
-
* geometry is read from the engine's widget
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
* widgets
|
|
2978
|
+
* The form plugin: the field plane. Document-scoped; requires the
|
|
2979
|
+
* interaction hub. Filling works without the annotation plugin, because
|
|
2980
|
+
* widget geometry is read from the engine's widget records. The active tool
|
|
2981
|
+
* is the mode switch: while it carries 'form-fill' (the built-in pointer and
|
|
2982
|
+
* pan tools do, so filling is the resting state), the form layer draws and
|
|
2983
|
+
* fills every widget. Design mode is the 'form-edit' and palette tools: with
|
|
2984
|
+
* the annotation plugin present, the form lends it the widgets, which its
|
|
2985
|
+
* tools then select, move, restyle and delete. The render plugin draws the
|
|
2986
|
+
* widgets' pictures in both modes. `config` is the settings the app
|
|
2987
|
+
* registers, over {@link FORM_DEFAULTS}.
|
|
1241
2988
|
*/
|
|
1242
|
-
const formPlugin = (
|
|
2989
|
+
const formPlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
1243
2990
|
id: "form",
|
|
1244
|
-
token:
|
|
2991
|
+
token: require_contract_host.FormToken,
|
|
1245
2992
|
scope: "document",
|
|
1246
|
-
requires: [
|
|
1247
|
-
optional: [
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (annotation) annotation.registerBehavior({
|
|
1281
|
-
id: "form-widgets",
|
|
1282
|
-
matches: (a) => a.subtype.startsWith("widget"),
|
|
1283
|
-
engaged: () => interaction.activeTool()?.enables.has("form-fill") ?? false
|
|
1284
|
-
});
|
|
1285
|
-
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
|
|
1286
|
-
if (actions) {
|
|
1287
|
-
ctx.cleanup(actions.registerFormCommitSink((effects) => {
|
|
1288
|
-
const formHost = ctx.tryGet(require_types.FormToken);
|
|
1289
|
-
if (!formHost) return Promise.resolve({
|
|
1290
|
-
results: effects.map((_, index) => ({
|
|
1291
|
-
index,
|
|
1292
|
-
status: "failed",
|
|
1293
|
-
fields: [],
|
|
1294
|
-
changedWidgets: []
|
|
1295
|
-
})),
|
|
1296
|
-
changedWidgets: [],
|
|
1297
|
-
meta: null
|
|
1298
|
-
});
|
|
1299
|
-
return formHost.commitScriptFormEffects(effects);
|
|
1300
|
-
}));
|
|
1301
|
-
ctx.cleanup(actions.registerSubmitResolver((intent, actionCtx, diagnose) => {
|
|
1302
|
-
const formHost = ctx.tryGet(require_types.FormToken);
|
|
1303
|
-
if (!formHost) throw new Error("form plugin unavailable");
|
|
1304
|
-
return formHost.resolveSubmitDataset(intent, actionCtx, diagnose);
|
|
1305
|
-
}));
|
|
1306
|
-
ctx.cleanup(actions.registerExecutor("reset-form", async (node, actionCtx) => {
|
|
1307
|
-
if (node.type !== "reset-form") return {
|
|
1308
|
-
status: "inert",
|
|
1309
|
-
reason: "not a reset-form node"
|
|
1310
|
-
};
|
|
1311
|
-
const formHost = ctx.tryGet(require_types.FormToken);
|
|
1312
|
-
if (!formHost) return {
|
|
1313
|
-
status: "inert",
|
|
1314
|
-
reason: "form plugin unavailable"
|
|
1315
|
-
};
|
|
1316
|
-
const result = await formHost.resetFormAction(node.fields, node.exclude, actionCtx.origin);
|
|
1317
|
-
if (result.status === "failed") return {
|
|
1318
|
-
status: "failed",
|
|
1319
|
-
error: result.error?.message ?? "reset failed"
|
|
1320
|
-
};
|
|
1321
|
-
if (result.effectsResult === null) return {
|
|
1322
|
-
status: "inert",
|
|
1323
|
-
reason: result.diagnostics[0]?.message ?? "reset-form resolved no fields"
|
|
1324
|
-
};
|
|
1325
|
-
return { status: "executed" };
|
|
1326
|
-
}));
|
|
1327
|
-
}
|
|
2993
|
+
requires: [_embedpdf_plugin_interaction_contract_host.InteractionToken],
|
|
2994
|
+
optional: [
|
|
2995
|
+
_embedpdf_plugin_annotation_contract.AnnotationToken,
|
|
2996
|
+
_embedpdf_plugin_actions_contract.ActionsToken,
|
|
2997
|
+
_embedpdf_plugin_render_contract.RenderToken
|
|
2998
|
+
],
|
|
2999
|
+
state: initialFormState,
|
|
3000
|
+
settings: {
|
|
3001
|
+
defaults: require_contract.FORM_DEFAULTS,
|
|
3002
|
+
registered: config
|
|
3003
|
+
},
|
|
3004
|
+
create: createFormController
|
|
3005
|
+
});
|
|
3006
|
+
//#endregion
|
|
3007
|
+
//#region src/state.ts
|
|
3008
|
+
/**
|
|
3009
|
+
* The form page's State table as code: what `useFormState()` returns, and the
|
|
3010
|
+
* same fields in every other framework. One field's value has its own hook
|
|
3011
|
+
* (`useFormValue(ref)`), so a total in your own UI re-renders only when that
|
|
3012
|
+
* field changes.
|
|
3013
|
+
*/
|
|
3014
|
+
const NO_FIELDS = Object.freeze([]);
|
|
3015
|
+
const formState = (0, _embedpdf_core.defineState)(require_contract.FormToken, {
|
|
3016
|
+
read: (form) => ({
|
|
3017
|
+
fields: form.list(),
|
|
3018
|
+
status: form.getStatus(),
|
|
3019
|
+
formKind: form.getFormKind(),
|
|
3020
|
+
selectedField: form.getSelectedField()
|
|
3021
|
+
}),
|
|
3022
|
+
empty: {
|
|
3023
|
+
fields: NO_FIELDS,
|
|
3024
|
+
status: "idle",
|
|
3025
|
+
formKind: "none",
|
|
3026
|
+
selectedField: null
|
|
1328
3027
|
}
|
|
1329
3028
|
});
|
|
1330
3029
|
//#endregion
|
|
3030
|
+
exports.FORM_DEFAULTS = require_contract.FORM_DEFAULTS;
|
|
1331
3031
|
exports.FORM_TOOLS = FORM_TOOLS;
|
|
1332
3032
|
exports.FORM_TOOL_BY_ID = FORM_TOOL_BY_ID;
|
|
1333
|
-
exports.FormScriptingController =
|
|
3033
|
+
exports.FormScriptingController = require_controller.FormScriptingController;
|
|
1334
3034
|
exports.FormToken = require_contract.FormToken;
|
|
1335
|
-
exports
|
|
1336
|
-
|
|
1337
|
-
|
|
3035
|
+
Object.defineProperty(exports, "FormTransfer", {
|
|
3036
|
+
enumerable: true,
|
|
3037
|
+
get: function() {
|
|
3038
|
+
return _embedpdf_engine_core_public.FormTransfer;
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
exports.createFormScriptingController = require_controller.createFormScriptingController;
|
|
1338
3042
|
exports.formPlugin = formPlugin;
|
|
3043
|
+
exports.formState = formState;
|
|
3044
|
+
Object.defineProperty(exports, "toFieldRef", {
|
|
3045
|
+
enumerable: true,
|
|
3046
|
+
get: function() {
|
|
3047
|
+
return _embedpdf_engine_core_runtime.toFieldRef;
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
1339
3050
|
|
|
1340
3051
|
//# sourceMappingURL=index.cjs.map
|