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