@embedpdf/plugin-form 3.0.0-next.14 → 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.
Files changed (47) hide show
  1. package/dist/contract/host.cjs +29 -2
  2. package/dist/contract/host.cjs.map +1 -0
  3. package/dist/contract/host.d.cts +3 -2
  4. package/dist/contract/host.d.ts +3 -2
  5. package/dist/contract/host.js +14 -2
  6. package/dist/contract/host.js.map +1 -0
  7. package/dist/contract-BwcgtCOD.d.ts +566 -0
  8. package/dist/contract-CU0jrI_D.cjs +56 -0
  9. package/dist/contract-CU0jrI_D.cjs.map +1 -0
  10. package/dist/contract-D-yQxw3H.d.cts +566 -0
  11. package/dist/contract-DcncvPcF.js +45 -0
  12. package/dist/contract-DcncvPcF.js.map +1 -0
  13. package/dist/contract.cjs +17 -6
  14. package/dist/contract.d.cts +2 -8
  15. package/dist/contract.d.ts +2 -8
  16. package/dist/contract.js +2 -7
  17. package/dist/controller-CS6tV2m1.cjs +514 -0
  18. package/dist/controller-CS6tV2m1.cjs.map +1 -0
  19. package/dist/controller-Sr5M_8G8.js +497 -0
  20. package/dist/controller-Sr5M_8G8.js.map +1 -0
  21. package/dist/host-contract-1epLfK1S.d.cts +87 -0
  22. package/dist/host-contract-BRYUbpT_.d.ts +87 -0
  23. package/dist/index.cjs +2916 -1205
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +46 -17
  26. package/dist/index.d.ts +46 -17
  27. package/dist/index.js +2895 -1206
  28. package/dist/index.js.map +1 -1
  29. package/dist/scripting.cjs +3 -484
  30. package/dist/scripting.d.cts +58 -22
  31. package/dist/scripting.d.ts +58 -22
  32. package/dist/scripting.js +1 -482
  33. package/package.json +14 -13
  34. package/dist/contract.cjs.map +0 -1
  35. package/dist/contract.js.map +0 -1
  36. package/dist/internal.cjs +0 -4
  37. package/dist/internal.d.cts +0 -3
  38. package/dist/internal.d.ts +0 -3
  39. package/dist/internal.js +0 -3
  40. package/dist/scripting.cjs.map +0 -1
  41. package/dist/scripting.js.map +0 -1
  42. package/dist/types-Bpc32Qmx.d.cts +0 -309
  43. package/dist/types-C1uIIYfn.cjs +0 -16
  44. package/dist/types-C1uIIYfn.cjs.map +0 -1
  45. package/dist/types-DRirhZKr.d.ts +0 -309
  46. package/dist/types-DkQmmcTG.js +0 -12
  47. 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 require_scripting = require("./scripting.cjs");
3
- const require_types = require("./types-C1uIIYfn.cjs");
4
- const require_contract = require("./contract.cjs");
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 _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
10
- let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
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 _embedpdf_plugin_annotation_authoring = require("@embedpdf/plugin-annotation/authoring");
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 ONCE for ResetForm (Tables 241/242) and
16
- * SubmitForm (Tables 239/240): a NAME target selects the field AND its
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 ABSENT) selects everything and the flag
22
- * is ignored; `[]` + include selects NOTHING while `[]` + exclude 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 ADDRESSED by the target list
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((t) => t.kind === "name").map((t) => t.name);
35
- const objectNumbers = new Set(targets.filter((t) => t.kind === "objectNumber").map((t) => t.objectNumber));
36
- const listed = (field) => field.ref.kind === "objectNumber" && objectNumbers.has(field.ref.fieldObjectNumber) || names.some((name) => field.name === name || field.name.startsWith(`${name}.`));
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((f) => !listed(f)) : all.filter(listed),
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 FRESH engine read so
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 EXPLICITLY listed one is diagnosed
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 NAME-ONLY entry (`value: null`) only under
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.flags.noExport) {
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/core/model.ts
104
- const fieldKeyOf = (field) => field.fieldObjectNumber > 0 ? `obj:${field.fieldObjectNumber}` : `fqn:${field.name}`;
105
- const initialModel = () => ({
106
- snapshot: null,
107
- byKey: {},
108
- byWidget: {},
109
- writing: {},
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
- //#endregion
225
- //#region src/core/fill-items.ts
226
- const ZERO_BOX = {
227
- x: 0,
228
- y: 0,
229
- width: 0,
230
- height: 0
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
- * Project ONE widget of a field into a fill control. `box` is supplied by the
234
- * caller: the page projection reads the model's widget geometry; a consumer
235
- * that already owns a live box (the annotation plane's RenderItem) passes it —
236
- * or nothing, when only the semantics matter. Null for families with no fill
237
- * control (unknown: rendered by the annotation plane only).
238
- */
239
- function projectWidget(model, field, annotObjectNumber, box = ZERO_BOX) {
240
- const key = fieldKeyOf(field);
241
- const base = {
242
- key,
243
- annotObjectNumber,
244
- box,
245
- disabled: field.flags.readOnly || model.writing[key] === true,
246
- label: field.alternateName ?? field.name
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
- switch (field.family) {
249
- case "text": return {
250
- ...base,
251
- control: "text",
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
- case "radio": {
269
- const toggle = field.widgets.find((w) => w.annotObjectNumber === annotObjectNumber);
270
- return {
271
- ...base,
272
- control: "toggle",
273
- kind: "radio",
274
- checked: toggle && "checked" in toggle ? toggle.checked : false,
275
- onState: toggle && "onState" in toggle ? toggle.onState : ""
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
- case "combobox": return {
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/mutationQueue.ts
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 form shell. Pure `update` runs here; the resulting model is dispatched
363
- * to the store; engine calls happen around it. Every read the frameworks do
364
- * goes through memoized projections keyed on `model.seq`.
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
- function createFormCapability(ctx, options = {}) {
367
- const model = () => ctx.getState().model;
368
- const apply = (msg) => {
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 resolver (Phase 4, D7): a FRESH engine read — never
642
- * the cached model, so no staleness class exists — then the pure ISO
643
- * builder. Selection/veto/value semantics live in `field-selection.ts`;
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, actionCtx, diagnose) => {
647
- const doc = ctx.doc;
648
- if (!doc) throw new Error("no document");
649
- const snapshot = await doc.forms.list();
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.fields, intent, diagnose),
657
- origin: actionCtx.origin,
658
- event: actionCtx.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
- const updateFieldNow = async (key, patch) => {
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
- result = await doc.forms.applyEffects(effects);
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
- results: effects.map((_, index) => ({
938
- index,
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
- await refresh(true);
952
- if (annotationHost) {
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/effects.ts
999
- const STRUCTURAL = /* @__PURE__ */ new Set([
1000
- "form.fieldCreated",
1001
- "form.fieldUpdated",
1002
- "form.fieldDeleted",
1003
- "form.widgetAttached",
1004
- "form.widgetDetached",
1005
- "form.repaired"
1006
- ]);
1007
- function registerFormEffects(ctx) {
1008
- const doc = ctx.doc;
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
- const unsubscribe = doc.events.subscribe((event) => {
1028
- if (event.type === "stream.desynced") {
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
- ctx.cleanup(unsubscribe);
1046
- }
1047
- //#endregion
1048
- //#region src/tools.ts
1049
- /** Palette tools keep widgets editable right after placement. */
1050
- const PLACE_TAGS = ["form-place", "annotation-edit"];
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
- defaults: FIELD_CHROME,
1130
- cursor: "crosshair"
1131
- }
1132
- ];
1133
- const FORM_TOOL_BY_ID = new Map(FORM_TOOLS.map((t) => [t.id, t]));
2741
+ commitDraftText,
2742
+ discardDraftText: (field) => {
2743
+ drafts.delete(keyOf(field));
2744
+ }
2745
+ } };
2746
+ }
1134
2747
  //#endregion
1135
- //#region src/handler.ts
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
- * Draw-to-place: drag a box (live placement preview through the annotation
1144
- * plane's ghost pipeline) or just click (the tool's `clickCreate` policy,
1145
- * resolved by the SAME `resolveClickPlacement` the annotation core uses) —
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
- function createPlaceHandler(form, interaction, annotation) {
1156
- let origin = null;
1157
- return {
1158
- id: "form-place",
1159
- priority: 95,
1160
- enabledFor: (t) => t.enables.has("form-place"),
1161
- onDown: (s) => {
1162
- if (!s.page || !FORM_TOOL_BY_ID.has(interaction.activeToolId())) return false;
1163
- if (!form.canDesign()) return false;
1164
- origin = {
1165
- pon: s.page.pon,
1166
- start: s.page.point,
1167
- last: s.page.point
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
- return true;
1170
- },
1171
- onMove: (s) => {
1172
- if (!origin) return;
1173
- const point = (0, _embedpdf_plugin_interaction_contract.samplePointOn)(s, origin.pon);
1174
- if (!point) return;
1175
- origin.last = point;
1176
- const box = rectFrom(origin.start, point);
1177
- if (annotation) if (Math.max(box.width, box.height) >= _embedpdf_plugin_annotation_authoring.MIN_DRAG) annotation.setPlacementPreview(interaction.activeToolId(), origin.pon, box);
1178
- else annotation.clearPlacementPreview();
1179
- },
1180
- onUp: (s) => {
1181
- if (!origin) return;
1182
- const o = origin;
1183
- origin = null;
1184
- annotation?.clearPlacementPreview();
1185
- const toolId = interaction.activeToolId();
1186
- const tool = FORM_TOOL_BY_ID.get(toolId);
1187
- if (!tool) return;
1188
- const end = (0, _embedpdf_plugin_interaction_contract.samplePointOn)(s, o.pon) ?? o.last;
1189
- const dragged = rectFrom(o.start, end);
1190
- const isClick = dragged.width < _embedpdf_plugin_annotation_authoring.MIN_DRAG && dragged.height < _embedpdf_plugin_annotation_authoring.MIN_DRAG;
1191
- const pageBox = form.pageBox(o.pon);
1192
- const box = isClick ? boxOfClick(o.start, tool.clickCreate, pageBox) : dragged;
1193
- form.placeField({
1194
- family: tool.family,
1195
- pageObjectNumber: o.pon,
1196
- box,
1197
- appearance: (0, _embedpdf_plugin_annotation_authoring.widgetAppearanceFromProps)(annotation ? annotation.currentDefaults(toolId) : tool.defaults)
1198
- }).then((placed) => {
1199
- if (!annotation || interaction.activeToolId() !== toolId) return;
1200
- const widget = placed.widget;
1201
- if (!widget || widget.annotObjectNumber <= 0) return;
1202
- annotation.select({
1203
- kind: "objectNumber",
1204
- annotObjectNumber: widget.annotObjectNumber,
1205
- pageObjectNumber: o.pon
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
- }).catch((err) => {
1208
- globalThis.console?.error("[form] placeField failed:", err);
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
- /** The click-create box through the SHARED placement layer (fields are boxes;
1214
- * the policy anchor + page clamp are exactly the annotation click's). */
1215
- function boxOfClick(point, policy, pageBox) {
1216
- const placement = (0, _embedpdf_plugin_annotation_authoring.resolveClickPlacement)(point, policy, { pageBox: pageBox ?? void 0 });
1217
- return placement.kind === "box" ? placement.rect : {
1218
- x: point.x,
1219
- y: point.y,
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/reducer.ts
1226
- const initialFormState = () => ({ model: initialModel() });
1227
- /** Dumb store: the pure core computes the next model; the shell dispatches it. */
1228
- const formReducer = (state, action) => action.type === "SET_MODEL" ? { model: action.model } : state;
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 FIELD plane. Document-scoped; requires the
1233
- * interaction hub. Fill mode works with no annotation plugin at all —
1234
- * geometry is read from the engine's widget DTOs. When the annotation
1235
- * plugin IS present, a Behavior keeps widgets geometry-inert while the
1236
- * active tool carries 'form-fill' (the built-in pointer/pan tools do, so
1237
- * filling is the resting state) and stands the fill controls down under
1238
- * every other tool — the single-active-tool hub IS the mode switch.
1239
- * Design mode = the 'form-edit' / palette tools: no 'form-fill' tag, so
1240
- * widgets become ordinary editable annotations.
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 = (options = {}) => (0, _embedpdf_core.definePlugin)({
2989
+ const formPlugin = (config) => (0, _embedpdf_core.definePlugin)({
1243
2990
  id: "form",
1244
- token: require_types.FormToken,
2991
+ token: require_contract_host.FormToken,
1245
2992
  scope: "document",
1246
- requires: [_embedpdf_plugin_interaction_contract.InteractionToken],
1247
- optional: [_embedpdf_plugin_annotation_contract.AnnotationToken, _embedpdf_plugin_actions_contract.ActionsToken],
1248
- initialState: initialFormState,
1249
- reduce: formReducer,
1250
- capability: (ctx) => createFormCapability(ctx, options),
1251
- effects: registerFormEffects,
1252
- init: (ctx) => {
1253
- const interaction = ctx.get(_embedpdf_plugin_interaction_contract.InteractionToken);
1254
- const form = ctx.get(require_types.FormToken);
1255
- interaction.registerTool({
1256
- id: "form-fill",
1257
- cursor: "default",
1258
- enables: /* @__PURE__ */ new Set(["form-fill", "link-nav"])
1259
- });
1260
- interaction.registerTool({
1261
- id: "form-edit",
1262
- cursor: "default",
1263
- enables: /* @__PURE__ */ new Set(["annotation-edit", "annotation-marquee"])
1264
- });
1265
- const annotation = ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
1266
- for (const t of FORM_TOOLS) if (annotation) annotation.registerTool({
1267
- id: t.id,
1268
- subtype: t.visualKind,
1269
- cursor: t.cursor,
1270
- enables: [...PLACE_TAGS],
1271
- clickCreate: t.clickCreate,
1272
- defaults: t.defaults
1273
- });
1274
- else interaction.registerTool({
1275
- id: t.id,
1276
- cursor: t.cursor,
1277
- enables: new Set(PLACE_TAGS)
1278
- });
1279
- interaction.registerHandler(createPlaceHandler(form, interaction, annotation));
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 = require_scripting.FormScriptingController;
3033
+ exports.FormScriptingController = require_controller.FormScriptingController;
1334
3034
  exports.FormToken = require_contract.FormToken;
1335
- exports.createFormScriptingController = require_scripting.createFormScriptingController;
1336
- exports.createSerialMutationQueue = createSerialMutationQueue;
1337
- exports.fieldKeyOf = fieldKeyOf;
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