@embedpdf/plugin-form 3.0.0-next.13 → 3.0.0-next.15

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