@embedpdf/plugin-form 3.0.0-next.14 → 3.0.0-next.16

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