@embedpdf/plugin-form 3.0.0-next.1 → 3.0.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,104 @@
1
- import { createCapabilityToken, definePlugin } from "@embedpdf/core";
2
- import { AnnotationToken, MIN_DRAG, resolveClickPlacement, widgetAppearanceFromProps } from "@embedpdf/plugin-annotation";
3
- import { AnnotationToken as AnnotationToken$1 } from "@embedpdf/plugin-annotation/internal";
4
- import { InteractionToken, samplePointOn } from "@embedpdf/plugin-interaction";
5
- import { DEFAULT_SCRIPT_BUDGET, javaScriptProgramFromActionTree, scriptFieldsFromSnapshot } from "@embedpdf/core-acrojs";
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";
5
+ import { ActionsToken, createHoverPump } from "@embedpdf/plugin-actions/contract";
6
+ import { ActionsToken as ActionsToken$1 } from "@embedpdf/plugin-actions/contract/host";
7
+ import { AnnotationToken } from "@embedpdf/plugin-annotation/contract";
8
+ 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
+ //#region src/field-selection.ts
13
+ /**
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
16
+ * descendants (FQN dot-prefix — `parent` selects `parent.c1`, the rule the
17
+ * old exact-match resolution violated); an objectNumber target selects that
18
+ * field dictionary. Include mode keeps the selection; exclude mode keeps
19
+ * 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
22
+ * everything — presence and emptiness are different states.
23
+ *
24
+ * `listed` reports whether a field was ADDRESSED by the target list
25
+ * (directly or through its subtree) — the "explicitly listed" predicate the
26
+ * submit honesty diagnostics key on.
27
+ */
28
+ const resolveFieldSelection = (all, targets, exclude) => {
29
+ if (targets === null) return {
30
+ selected: [...all],
31
+ listed: () => false
32
+ };
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}.`));
36
+ return {
37
+ selected: exclude ? all.filter((f) => !listed(f)) : all.filter(listed),
38
+ listed
39
+ };
40
+ };
41
+ /**
42
+ * Build the resolved submission dataset from a live field list (Tables
43
+ * 239/240 semantics; pure — the capability supplies a FRESH engine read so
44
+ * no model-lag class exists):
45
+ *
46
+ * - the unconditional **NoExport veto** — even an explicit include cannot
47
+ * override it (diagnosed when explicitly listed, so the veto is
48
+ * observable, silent when swept in implicitly);
49
+ * - **push-buttons and signatures** never contribute entries in v1 —
50
+ * ISO's explicit-pushbutton-`/AP`-in-FDF rule is unrepresentable in the
51
+ * current DTO, so an EXPLICITLY listed one is diagnosed
52
+ * (`submit-entry-unsupported`), never silently dropped;
53
+ * - an **unsupported `/V` shape** is always diagnosed (silent omission
54
+ * would be data loss);
55
+ * - a valueless field becomes a NAME-ONLY entry (`value: null`) only under
56
+ * IncludeNoValueFields, else it is skipped (the ISO default);
57
+ * - values are entry-faithful: scalar → string, array (multi-select list
58
+ * box) → string[]; the submitted name is the fully qualified name.
59
+ */
60
+ const buildSubmitEntries = (fields, intent, diagnose) => {
61
+ const { selected, listed } = resolveFieldSelection(fields, intent.fields, intent.exclude);
62
+ const explicitInclude = intent.fields !== null && !intent.exclude;
63
+ const entries = [];
64
+ for (const field of selected) {
65
+ const explicit = explicitInclude && listed(field);
66
+ if (field.family === "pushbutton" || field.family === "signature") {
67
+ if (explicit) diagnose({
68
+ code: "submit-entry-unsupported",
69
+ message: `submit: '${field.name}' (${field.family}) has no representable submission value in v1 — skipped`
70
+ });
71
+ continue;
72
+ }
73
+ if (field.flags.noExport) {
74
+ if (explicit) diagnose({
75
+ code: "submit-entry-unsupported",
76
+ message: `submit: '${field.name}' carries the NoExport flag — the veto is unconditional (ISO 12.7.6.2), skipped`
77
+ });
78
+ continue;
79
+ }
80
+ const entry = field.valueEntry;
81
+ if (entry.kind === "scalar") entries.push({
82
+ name: field.name,
83
+ value: entry.value
84
+ });
85
+ else if (entry.kind === "array") entries.push({
86
+ name: field.name,
87
+ value: entry.values
88
+ });
89
+ else if (entry.kind === "none") {
90
+ if (intent.includeNoValueFields) entries.push({
91
+ name: field.name,
92
+ value: null
93
+ });
94
+ } else diagnose({
95
+ code: "submit-entry-unsupported",
96
+ message: `submit: '${field.name}' has an unsupported /V shape — skipped (would otherwise be silent data loss)`
97
+ });
98
+ }
99
+ return entries;
100
+ };
101
+ //#endregion
6
102
  //#region src/core/model.ts
7
103
  const fieldKeyOf = (field) => field.fieldObjectNumber > 0 ? `obj:${field.fieldObjectNumber}` : `fqn:${field.name}`;
8
104
  const initialModel = () => ({
@@ -216,507 +312,8 @@ function fillItemForWidget(model, annotObjectNumber) {
216
312
  }
217
313
  //#endregion
218
314
  //#region src/mutationQueue.ts
219
- /** Small promise-tail queue: failures never poison later mutations. */
220
- function createSerialMutationQueue() {
221
- let tail = Promise.resolve();
222
- return (operation) => {
223
- const result = tail.then(operation, operation);
224
- tail = result.then(() => void 0, () => void 0);
225
- return result;
226
- };
227
- }
228
- //#endregion
229
- //#region src/scripting.ts
230
- const refKey = (ref) => ref.kind === "objectNumber" ? `obj:${ref.fieldObjectNumber}` : `fqn:${ref.name}`;
231
- const sameRef = (left, right) => refKey(left) === refKey(right);
232
- const cloneValue = (value) => Array.isArray(value) ? [...value] : value;
233
- const sameValue = (left, right) => Array.isArray(left) && Array.isArray(right) ? left.length === right.length && left.every((value, index) => value === right[index]) : left === right;
234
- function cloneFields(fields) {
235
- return fields.map((field) => ({
236
- ...field,
237
- ref: { ...field.ref },
238
- value: cloneValue(field.value),
239
- defaultValue: cloneValue(field.defaultValue),
240
- ...field.options ? { options: field.options.map((option) => ({ ...option })) } : {}
241
- }));
242
- }
243
- function fieldByRef(fields, ref) {
244
- return fields.find((field) => sameRef(field.ref, ref));
245
- }
246
- function snapshotField(snapshot, ref) {
247
- return snapshot.fields.find((field) => sameRef(field.ref, ref));
248
- }
249
- function scriptValueFromFormValue(field, value) {
250
- if (field.family === "text" && value.type === "text") return value.value;
251
- if ((field.family === "checkbox" || field.family === "radio") && value.type === "toggle") return value.state ?? "Off";
252
- if (field.family === "combobox" && value.type === "choice") return value.values[0] ?? "";
253
- if (field.family === "listbox" && value.type === "choice") return [...value.values];
254
- throw new Error(`Form value type '${value.type}' does not match field family '${field.family}'`);
255
- }
256
- function formValueFromScriptValue(field) {
257
- switch (field.family) {
258
- case "text": return {
259
- type: "text",
260
- value: String(field.value ?? "")
261
- };
262
- case "checkbox":
263
- case "radio": return {
264
- type: "toggle",
265
- state: field.value === null || field.value === void 0 || String(field.value) === "Off" ? null : String(field.value)
266
- };
267
- case "combobox": return {
268
- type: "choice",
269
- values: Array.isArray(field.value) ? field.value.slice(0, 1) : [String(field.value ?? "")]
270
- };
271
- case "listbox": return {
272
- type: "choice",
273
- values: Array.isArray(field.value) ? [...field.value] : [String(field.value ?? "")]
274
- };
275
- default: return null;
276
- }
277
- }
278
- function applyEffects(overlay, effects) {
279
- for (const effect of effects) {
280
- if (effect.kind === "reset") {
281
- for (const ref of effect.refs) {
282
- const field = fieldByRef(overlay.fields, ref);
283
- if (!field) continue;
284
- field.value = cloneValue(field.defaultValue);
285
- overlay.resetKeys.add(refKey(ref));
286
- }
287
- continue;
288
- }
289
- const field = fieldByRef(overlay.fields, effect.ref);
290
- if (!field) continue;
291
- const key = refKey(effect.ref);
292
- if (effect.kind === "setValue") {
293
- field.value = scriptValueFromFormValue(field, effect.value);
294
- overlay.resetKeys.delete(key);
295
- } else if (effect.kind === "setDisplay") field.display = effect.display;
296
- else overlay.appearances.set(key, {
297
- ref: effect.ref,
298
- text: effect.text
299
- });
300
- }
301
- }
302
- function canonicalEffects(overlay) {
303
- const effects = [];
304
- const resets = [];
305
- for (const field of overlay.fields) {
306
- const original = fieldByRef(overlay.original, field.ref);
307
- if (!original) continue;
308
- if (!sameValue(original.value, field.value)) {
309
- const key = refKey(field.ref);
310
- if (overlay.resetKeys.has(key) && sameValue(field.value, field.defaultValue)) resets.push(field.ref);
311
- else {
312
- const value = formValueFromScriptValue(field);
313
- if (value) effects.push({
314
- kind: "setValue",
315
- ref: field.ref,
316
- value
317
- });
318
- }
319
- }
320
- if (original.display !== field.display) effects.push({
321
- kind: "setDisplay",
322
- ref: field.ref,
323
- display: field.display
324
- });
325
- }
326
- if (resets.length > 0) effects.unshift({
327
- kind: "reset",
328
- refs: resets
329
- });
330
- effects.push(...Array.from(overlay.appearances.values(), ({ ref, text }) => ({
331
- kind: "setAppearanceText",
332
- ref,
333
- text
334
- })));
335
- return effects;
336
- }
337
- function scriptError(message) {
338
- return {
339
- kind: "invalid-output",
340
- message
341
- };
342
- }
343
- function utf8Length(value) {
344
- let length = 0;
345
- for (let index = 0; index < value.length; index += 1) {
346
- const code = value.charCodeAt(index);
347
- if (code < 128) length += 1;
348
- else if (code < 2048) length += 2;
349
- else if (code >= 55296 && code <= 56319 && index + 1 < value.length) {
350
- length += 4;
351
- index += 1;
352
- } else length += 3;
353
- }
354
- return length;
355
- }
356
- function seedFrom(documentId, sequence) {
357
- let hash = 2166136261;
358
- for (const char of `${documentId}:${sequence}`) {
359
- hash ^= char.charCodeAt(0);
360
- hash = Math.imul(hash, 16777619);
361
- }
362
- return hash >>> 0;
363
- }
364
- function resolvedIdentity(doc, config) {
365
- const claims = doc.security.identity;
366
- const supplied = typeof config.identity === "function" ? config.identity() : config.identity ?? {};
367
- return {
368
- name: supplied.name ?? claims?.display_name ?? claims?.user_id ?? "",
369
- loginName: supplied.loginName ?? claims?.user_id ?? "",
370
- corporation: supplied.corporation ?? claims?.group_id ?? "",
371
- email: supplied.email ?? ""
372
- };
373
- }
374
- function pageNumberFor(meta, field) {
375
- const pon = field.widgets.find((widget) => widget.pageObjectNumber > 0)?.pageObjectNumber;
376
- if (!meta || pon === void 0) return 0;
377
- const index = meta.pages.findIndex((page) => page.pageObjectNumber === pon);
378
- return Math.max(0, index);
379
- }
380
- function statusFromEffects(result) {
381
- if (result.results.some(({ status }) => status === "failed" || status === "skipped")) return "failed";
382
- if (result.results.some(({ status }) => status === "rejected")) return "failed";
383
- return result.results.some(({ status }) => status === "applied") ? "applied" : "unchanged";
384
- }
385
- var FormScriptingController = class {
386
- constructor(options) {
387
- this.options = options;
388
- this.sandboxPromise = null;
389
- this.booted = false;
390
- this.disposed = false;
391
- this.sequence = 0;
392
- }
393
- dispose() {
394
- if (this.disposed) return;
395
- this.disposed = true;
396
- this.sandboxPromise?.then((sandbox) => sandbox.dispose()).catch(() => void 0);
397
- }
398
- async commit(snapshot, ref, proposed) {
399
- return this.transact(snapshot, ref, proposed);
400
- }
401
- /** Execute one originating widget's activation action (`/A`, including `/Next`). */
402
- async activate(snapshot, ref, action) {
403
- return this.transact(snapshot, ref, void 0, action);
404
- }
405
- /** Run lazy document boot plus the `/CO` chain without a user field change. */
406
- async recalculate(snapshot) {
407
- const target = snapshot.calculationOrder.find((ref) => ref !== null && snapshotField(snapshot, ref) !== void 0) ?? snapshot.fields[0]?.ref;
408
- if (!target) return {
409
- status: "unchanged",
410
- scripted: true,
411
- effectsResult: null,
412
- uiEffects: [],
413
- diagnostics: []
414
- };
415
- return this.transact(snapshot, target);
416
- }
417
- async transact(snapshot, ref, proposed, activation) {
418
- if (this.disposed) throw new Error("Form scripting controller is disposed");
419
- const target = snapshotField(snapshot, ref);
420
- if (!target) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' no longer exists`));
421
- const original = scriptFieldsFromSnapshot(snapshot);
422
- const overlay = {
423
- original,
424
- fields: cloneFields(original),
425
- resetKeys: /* @__PURE__ */ new Set(),
426
- appearances: /* @__PURE__ */ new Map()
427
- };
428
- const targetInput = fieldByRef(overlay.fields, ref);
429
- if (!targetInput) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' has no script view`));
430
- const hasProposedValue = proposed !== void 0;
431
- let proposedValue = cloneValue(targetInput.value);
432
- if (proposed) try {
433
- proposedValue = scriptValueFromFormValue(targetInput, proposed);
434
- } catch (error) {
435
- return this.failed([], [], scriptError(error instanceof Error ? error.message : String(error)));
436
- }
437
- const uiEffects = [];
438
- const diagnostics = [];
439
- const meta = this.options.document();
440
- const nowMs = this.options.config.now?.() ?? Date.now();
441
- const inputBase = {
442
- document: {
443
- id: this.options.doc.id,
444
- fileName: this.options.config.fileName?.() ?? meta?.name ?? this.options.doc.id,
445
- pageCount: meta?.pageCount ?? 0,
446
- pageNumber: pageNumberFor(meta, target)
447
- },
448
- identity: resolvedIdentity(this.options.doc, this.options.config),
449
- environment: {
450
- nowMs,
451
- utcOffsetMinutes: this.options.config.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
452
- randomSeed: this.options.config.randomSeed?.() ?? seedFrom(this.options.doc.id, this.sequence++)
453
- }
454
- };
455
- const budget = this.options.config.budget ?? DEFAULT_SCRIPT_BUDGET;
456
- let executionMs = 0;
457
- let aggregateOutputSize = 0;
458
- const consume = (output, phase) => {
459
- aggregateOutputSize += utf8Length(JSON.stringify(output));
460
- uiEffects.push(...output.uiEffects.map((effect) => ({
461
- ...effect,
462
- phase
463
- })));
464
- diagnostics.push(...output.diagnostics);
465
- if (aggregateOutputSize > budget.maxOutputBytes) return {
466
- kind: "budget",
467
- message: `Form script transaction output exceeded ${budget.maxOutputBytes} bytes`
468
- };
469
- return output.error ?? null;
470
- };
471
- const remainingBudget = () => {
472
- const remaining = budget.maxExecutionMs - executionMs;
473
- return remaining <= 0 ? null : {
474
- ...budget,
475
- maxExecutionMs: remaining
476
- };
477
- };
478
- const run = async (kind, source, input) => {
479
- const remaining = remainingBudget();
480
- if (!remaining) return { error: {
481
- kind: "budget",
482
- message: "Form script transaction timed out"
483
- } };
484
- let output;
485
- try {
486
- const sandbox = await this.sandbox();
487
- const startedAt = Date.now();
488
- output = kind === "boot" ? sandbox.boot(source, input, remaining) : sandbox.run(source, input, remaining);
489
- executionMs += Date.now() - startedAt;
490
- } catch (error) {
491
- return { error: scriptError(error instanceof Error ? error.message : String(error)) };
492
- }
493
- const error = consume(output, kind === "boot" ? "boot" : "user");
494
- return error ? { error } : { output };
495
- };
496
- if (!this.booted) {
497
- this.booted = true;
498
- let sources = [];
499
- let bootError = null;
500
- try {
501
- sources = (this.options.doc.actions ? await this.options.doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => javaScriptProgramFromActionTree(action)) ?? [];
502
- } catch (error) {
503
- bootError = scriptError(error instanceof Error ? error.message : String(error));
504
- }
505
- if (!bootError && sources.length > 0) {
506
- const boot = await run("boot", sources, {
507
- ...inputBase,
508
- fields: overlay.fields,
509
- event: { kind: "name-tree-boot" }
510
- });
511
- if (boot.error) bootError = boot.error;
512
- else applyEffects(overlay, boot.output.formEffects);
513
- }
514
- if (bootError) diagnostics.push({
515
- code: "script-error",
516
- message: `Document boot script failed (continuing without it): ${bootError.message}`
517
- });
518
- }
519
- const bootEffects = canonicalEffects(overlay);
520
- if (activation) {
521
- let program;
522
- try {
523
- program = javaScriptProgramFromActionTree(activation);
524
- } catch (error) {
525
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
526
- }
527
- if (program) {
528
- const activated = await run("run", program, {
529
- ...inputBase,
530
- fields: overlay.fields,
531
- event: {
532
- kind: "widget-activate",
533
- target: ref,
534
- source: ref,
535
- value: targetInput.value
536
- }
537
- });
538
- if (activated.error) return this.failed(uiEffects, diagnostics, activated.error);
539
- applyEffects(overlay, activated.output.formEffects);
540
- }
541
- }
542
- if (!activation && hasProposedValue && target.actions?.keystroke && target.family === "text") {
543
- let program;
544
- try {
545
- program = javaScriptProgramFromActionTree(target.actions.keystroke);
546
- } catch (error) {
547
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
548
- }
549
- if (program) {
550
- const oldValue = String(targetInput.value ?? "");
551
- const key = await run("run", program, {
552
- ...inputBase,
553
- fields: overlay.fields,
554
- event: {
555
- kind: "field-keystroke",
556
- target: ref,
557
- source: ref,
558
- value: oldValue,
559
- change: String(proposedValue ?? ""),
560
- selStart: 0,
561
- selEnd: oldValue.length,
562
- willCommit: true
563
- }
564
- });
565
- if (key.error) return this.failed(uiEffects, diagnostics, key.error);
566
- if (!key.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
567
- applyEffects(overlay, key.output.formEffects);
568
- const event = key.output.event;
569
- const base = String(event.value ?? "");
570
- proposedValue = base.slice(0, event.selStart) + event.change + base.slice(Math.max(event.selStart, event.selEnd));
571
- }
572
- }
573
- if (!activation && hasProposedValue) {
574
- targetInput.value = cloneValue(proposedValue);
575
- overlay.resetKeys.delete(refKey(ref));
576
- }
577
- if (!activation && hasProposedValue && target.actions?.validate) {
578
- let program;
579
- try {
580
- program = javaScriptProgramFromActionTree(target.actions.validate);
581
- } catch (error) {
582
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
583
- }
584
- if (program) {
585
- const validation = await run("run", program, {
586
- ...inputBase,
587
- fields: overlay.fields,
588
- event: {
589
- kind: "field-validate",
590
- target: ref,
591
- source: ref,
592
- value: proposedValue,
593
- willCommit: true
594
- }
595
- });
596
- if (validation.error) return this.failed(uiEffects, diagnostics, validation.error);
597
- if (!validation.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
598
- targetInput.value = cloneValue(validation.output.event.value);
599
- applyEffects(overlay, validation.output.formEffects);
600
- }
601
- }
602
- const formatRefs = !activation && hasProposedValue ? [ref] : [];
603
- for (const calculationRef of activation ? [] : snapshot.calculationOrder) {
604
- if (!calculationRef) continue;
605
- const calculated = snapshotField(snapshot, calculationRef);
606
- const tree = calculated?.actions?.calculate;
607
- if (!calculated || !tree) continue;
608
- let program;
609
- try {
610
- program = javaScriptProgramFromActionTree(tree);
611
- } catch (error) {
612
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
613
- }
614
- const calculatedInput = fieldByRef(overlay.fields, calculationRef);
615
- if (program && calculatedInput) {
616
- const calculation = await run("run", program, {
617
- ...inputBase,
618
- fields: overlay.fields,
619
- event: {
620
- kind: "field-calculate",
621
- target: calculationRef,
622
- ...hasProposedValue ? { source: ref } : {},
623
- value: calculatedInput.value
624
- }
625
- });
626
- if (calculation.error) return this.failed(uiEffects, diagnostics, calculation.error);
627
- applyEffects(overlay, calculation.output.formEffects);
628
- }
629
- if (!formatRefs.some((candidate) => sameRef(candidate, calculationRef))) formatRefs.push(calculationRef);
630
- }
631
- for (const formatRef of formatRefs) {
632
- const field = snapshotField(snapshot, formatRef);
633
- const tree = field?.actions?.format;
634
- const overlayField = fieldByRef(overlay.fields, formatRef);
635
- if (!field || !tree || !overlayField) continue;
636
- let program;
637
- try {
638
- program = javaScriptProgramFromActionTree(tree);
639
- } catch (error) {
640
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
641
- }
642
- if (!program) continue;
643
- const format = await run("run", program, {
644
- ...inputBase,
645
- fields: overlay.fields,
646
- event: {
647
- kind: "field-format",
648
- target: formatRef,
649
- ...hasProposedValue ? { source: ref } : {},
650
- value: overlayField.value
651
- }
652
- });
653
- if (format.error) return this.failed(uiEffects, diagnostics, format.error);
654
- applyEffects(overlay, format.output.formEffects);
655
- }
656
- const effects = canonicalEffects(overlay);
657
- if (effects.length + uiEffects.length > budget.maxEffects) return this.failed(uiEffects, diagnostics, {
658
- kind: "budget",
659
- message: `Form script transaction exceeded ${budget.maxEffects} effects`
660
- });
661
- if (effects.length === 0) return {
662
- status: "unchanged",
663
- scripted: true,
664
- effectsResult: null,
665
- uiEffects,
666
- diagnostics
667
- };
668
- if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("This engine does not support batched form effects"));
669
- const effectsResult = await this.options.doc.forms.applyEffects(effects);
670
- const status = statusFromEffects(effectsResult);
671
- return {
672
- status,
673
- scripted: true,
674
- effectsResult,
675
- uiEffects,
676
- diagnostics,
677
- ...status === "failed" ? { error: scriptError("One or more native form effects failed") } : {}
678
- };
679
- }
680
- async sandbox() {
681
- this.sandboxPromise ?? (this.sandboxPromise = this.options.sandboxFactory().then((sandbox) => {
682
- if (this.disposed) sandbox.dispose();
683
- return sandbox;
684
- }));
685
- return this.sandboxPromise;
686
- }
687
- failed(uiEffects, diagnostics, error) {
688
- return {
689
- status: "failed",
690
- scripted: true,
691
- effectsResult: null,
692
- uiEffects,
693
- diagnostics,
694
- error
695
- };
696
- }
697
- async finishRejected(bootEffects, uiEffects, diagnostics) {
698
- if (bootEffects.length === 0) return {
699
- status: "rejected",
700
- scripted: true,
701
- effectsResult: null,
702
- uiEffects,
703
- diagnostics
704
- };
705
- if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("Batched form effects are unavailable"));
706
- const effectsResult = await this.options.doc.forms.applyEffects(bootEffects);
707
- if (statusFromEffects(effectsResult) === "failed") return this.failed(uiEffects, diagnostics, scriptError("A name-tree boot effect failed"));
708
- return {
709
- status: "rejected",
710
- scripted: true,
711
- effectsResult,
712
- uiEffects,
713
- diagnostics
714
- };
715
- }
716
- };
717
- function createFormScriptingController(options) {
718
- return new FormScriptingController(options);
719
- }
315
+ /** The core serial queue under its historical form-plugin name. */
316
+ const createSerialMutationQueue = createSerialQueue;
720
317
  //#endregion
721
318
  //#region src/capability.ts
722
319
  /** PDF user-space rect (y-up) → content-space box (y-down, crop-relative). */
@@ -756,34 +353,36 @@ function createFormCapability(ctx, options = {}) {
756
353
  };
757
354
  };
758
355
  const enqueueMutation = createSerialMutationQueue();
759
- const scripting = options.scripting?.enabled && ctx.doc ? createFormScriptingController({
356
+ const actionsHost = ctx.tryGet(ActionsToken$1);
357
+ const scriptPort = actionsHost?.scriptTransaction?.bind(actionsHost) ?? null;
358
+ const scripting = scriptPort && ctx.doc ? createFormScriptingController({
760
359
  doc: ctx.doc,
761
360
  document: () => ctx.document(),
762
- config: options.scripting,
763
- sandboxFactory: options.scripting.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()))
361
+ transaction: scriptPort
764
362
  }) : null;
765
363
  if (scripting) ctx.cleanup(() => scripting.dispose());
766
- let uiEffectProvider = null;
767
- const surfaceScriptingResult = (result) => {
768
- const observe = (callback) => {
769
- if (!callback) return;
770
- try {
771
- callback();
772
- } catch (error) {
773
- globalThis.console?.error("[form] scripting observer failed:", error);
774
- }
775
- };
776
- for (const effect of result.uiEffects) {
777
- observe(options.scripting?.onUiEffect ? () => options.scripting.onUiEffect(effect) : void 0);
778
- observe(uiEffectProvider ? () => uiEffectProvider(effect) : void 0);
364
+ /** Every script surface (UI effects, diagnostics, errors) flows through
365
+ * the actions plugin's ONE port — origin/phase attached (D9). */
366
+ const surfaceViaActions = (result, origin) => {
367
+ if (!actionsHost) return;
368
+ for (const phase of ["boot", "user"]) {
369
+ const uiEffects = result.uiEffects.filter((effect) => effect.phase === phase);
370
+ if (uiEffects.length === 0 && phase === "boot") continue;
371
+ actionsHost.surfaceScriptResult({
372
+ uiEffects,
373
+ diagnostics: phase === "user" ? result.diagnostics : [],
374
+ ...phase === "user" && result.error ? { error: result.error } : {},
375
+ origin,
376
+ phase
377
+ });
779
378
  }
780
- for (const diagnostic of result.diagnostics) observe(options.scripting?.onDiagnostic ? () => options.scripting.onDiagnostic(diagnostic) : void 0);
781
- if (result.error) observe(options.scripting?.onError ? () => options.scripting.onError(result.error) : void 0);
782
379
  };
380
+ const can = (cap) => ctx.doc?.security.allows(cap) ?? false;
783
381
  let refreshPromise = null;
784
382
  const refresh = async (force = false) => {
785
383
  const doc = ctx.doc;
786
384
  if (!doc) return;
385
+ if (!can("doc.forms.read")) return;
787
386
  if (refreshPromise) {
788
387
  await refreshPromise;
789
388
  if (!force) return;
@@ -791,7 +390,9 @@ function createFormCapability(ctx, options = {}) {
791
390
  refreshPromise = doc.forms.list().then((snapshot) => apply({
792
391
  t: "snapshot",
793
392
  snapshot
794
- })).finally(() => {
393
+ })).catch((err) => {
394
+ console.warn("[form] form snapshot failed to load", err);
395
+ }).finally(() => {
795
396
  refreshPromise = null;
796
397
  });
797
398
  return refreshPromise;
@@ -819,14 +420,20 @@ function createFormCapability(ctx, options = {}) {
819
420
  geomLoading.delete(pon);
820
421
  });
821
422
  };
423
+ const fuseFill = (item, fillable) => item === null || fillable || item.disabled ? item : {
424
+ ...item,
425
+ disabled: true
426
+ };
822
427
  const fillCache = /* @__PURE__ */ new Map();
823
428
  const fillItems$1 = (pon) => {
824
429
  const m = model();
430
+ const fillable = can("doc.forms.fill");
825
431
  const hit = fillCache.get(pon);
826
- if (hit && hit.seq === m.seq) return hit.items;
827
- const items = fillItems(m, pon);
432
+ if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.items;
433
+ const items = fillItems(m, pon).map((item) => fuseFill(item, fillable));
828
434
  fillCache.set(pon, {
829
435
  seq: m.seq,
436
+ fillable,
830
437
  items
831
438
  });
832
439
  return items;
@@ -834,11 +441,13 @@ function createFormCapability(ctx, options = {}) {
834
441
  const fillItemCache = /* @__PURE__ */ new Map();
835
442
  const fillItem = (annotObjectNumber) => {
836
443
  const m = model();
444
+ const fillable = can("doc.forms.fill");
837
445
  const hit = fillItemCache.get(annotObjectNumber);
838
- if (hit && hit.seq === m.seq) return hit.item;
839
- const item = fillItemForWidget(m, annotObjectNumber);
446
+ if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.item;
447
+ const item = fuseFill(fillItemForWidget(m, annotObjectNumber), fillable);
840
448
  fillItemCache.set(annotObjectNumber, {
841
449
  seq: m.seq,
450
+ fillable,
842
451
  item
843
452
  });
844
453
  return item;
@@ -847,8 +456,8 @@ function createFormCapability(ctx, options = {}) {
847
456
  const doc = ctx.doc;
848
457
  if (!doc) throw new Error("no document");
849
458
  if (scripting) {
850
- const result = await scripting.commit(await doc.forms.list(), ref, value);
851
- surfaceScriptingResult(result);
459
+ const result = await scripting.commit(ref, value);
460
+ surfaceViaActions(result, "user");
852
461
  if (result.effectsResult !== null) await refresh(true);
853
462
  return result;
854
463
  }
@@ -862,31 +471,33 @@ function createFormCapability(ctx, options = {}) {
862
471
  diagnostics: []
863
472
  };
864
473
  };
865
- const write = (key, value) => enqueueMutation(async () => {
866
- if (!ctx.doc) return;
867
- apply({
868
- t: "writeStart",
869
- key
870
- });
871
- try {
872
- const result = await commitValueNow(refKeyOf(key), value);
873
- if (result.status === "rejected" || result.status === "failed") apply({
874
- t: "writeFailed",
875
- key
876
- });
877
- else if (result.effectsResult === null && result.scripted) apply({
878
- t: "writeFailed",
879
- key
880
- });
881
- } catch (err) {
474
+ const write = (key, value) => {
475
+ if (!can("doc.forms.fill")) return Promise.reject(new PermissionDenied("doc.forms.fill", "form.fill"));
476
+ return enqueueMutation(async () => {
477
+ if (!ctx.doc) return;
882
478
  apply({
883
- t: "writeFailed",
479
+ t: "writeStart",
884
480
  key
885
481
  });
886
- throw err;
887
- }
888
- });
889
- const can = (cap) => ctx.doc?.security.allows(cap) ?? false;
482
+ try {
483
+ const result = await commitValueNow(refKeyOf(key), value);
484
+ if (result.status === "rejected" || result.status === "failed") apply({
485
+ t: "writeFailed",
486
+ key
487
+ });
488
+ else if (result.effectsResult === null && result.scripted) apply({
489
+ t: "writeFailed",
490
+ key
491
+ });
492
+ } catch (err) {
493
+ apply({
494
+ t: "writeFailed",
495
+ key
496
+ });
497
+ throw err;
498
+ }
499
+ });
500
+ };
890
501
  const annotationHost = ctx.tryGet(AnnotationToken$1);
891
502
  const annotationActivation = async (ref) => {
892
503
  const doc = ctx.doc;
@@ -898,8 +509,7 @@ function createFormCapability(ctx, options = {}) {
898
509
  return annotation?.subtype === "widget" ? annotation.actions?.activate ?? null : null;
899
510
  };
900
511
  const activateWidgetNow = async (key, annotationRef) => {
901
- const doc = ctx.doc;
902
- if (!doc) throw new Error("no document");
512
+ if (!ctx.doc) throw new Error("no document");
903
513
  if (!scripting) return {
904
514
  status: "unchanged",
905
515
  scripted: false,
@@ -915,11 +525,118 @@ function createFormCapability(ctx, options = {}) {
915
525
  uiEffects: [],
916
526
  diagnostics: []
917
527
  };
918
- const result = await scripting.activate(await doc.forms.list(), refKeyOf(key), action);
919
- surfaceScriptingResult(result);
528
+ const result = await scripting.activate(refKeyOf(key), action);
529
+ surfaceViaActions(result, "user");
920
530
  if (result.effectsResult !== null) await refresh(true);
921
531
  return result;
922
532
  };
533
+ const NOT_SCRIPTED = {
534
+ status: "unchanged",
535
+ scripted: false,
536
+ effectsResult: null,
537
+ uiEffects: [],
538
+ diagnostics: []
539
+ };
540
+ /**
541
+ * The shared reset core: one effects batch → refresh → V/C/F
542
+ * recalculation when the transaction port is present. Ridden by BOTH
543
+ * doors — the /ResetForm executor and the public `reset(key)` — so the
544
+ * two can never diverge again (the "reset() asymmetry" fix). `origin` is
545
+ * PRESERVED through recalculation surfacing: a lifecycle/hover ResetForm
546
+ * can no longer launder its alerts into user origin.
547
+ */
548
+ const applyResetBatch = async (refs, origin) => {
549
+ const doc = ctx.doc;
550
+ if (!doc) throw new Error("no document");
551
+ if (!doc.forms.applyEffects) {
552
+ const result = {
553
+ ...NOT_SCRIPTED,
554
+ diagnostics: [{
555
+ code: "unsupported-api",
556
+ message: "this engine has no form-effects batch door"
557
+ }]
558
+ };
559
+ surfaceViaActions(result, origin);
560
+ return result;
561
+ }
562
+ if (refs.length === 0) return NOT_SCRIPTED;
563
+ const effectsResult = await doc.forms.applyEffects([{
564
+ kind: "reset",
565
+ refs
566
+ }]);
567
+ const resetFailed = effectsResult.results.some((entry) => entry.status === "failed" || entry.status === "rejected");
568
+ await refresh(true);
569
+ if (resetFailed) return {
570
+ status: "failed",
571
+ scripted: false,
572
+ effectsResult,
573
+ uiEffects: [],
574
+ diagnostics: []
575
+ };
576
+ let recalc = null;
577
+ if (scripting) {
578
+ recalc = await scripting.recalculate();
579
+ surfaceViaActions(recalc, origin);
580
+ if (recalc.effectsResult !== null) await refresh(true);
581
+ }
582
+ return {
583
+ status: "applied",
584
+ scripted: recalc !== null,
585
+ effectsResult,
586
+ uiEffects: recalc?.uiEffects ?? [],
587
+ diagnostics: recalc?.diagnostics ?? [],
588
+ ...recalc?.error ? { error: recalc.error } : {}
589
+ };
590
+ };
591
+ const resetFormAction = (targets, exclude, origin = "user") => enqueueMutation(async () => {
592
+ const doc = ctx.doc;
593
+ if (!doc) throw new Error("no document");
594
+ const { selected } = resolveFieldSelection((await doc.forms.list()).fields, targets, exclude);
595
+ const resettable = selected.filter((f) => f.family !== "pushbutton" && f.family !== "signature");
596
+ return applyResetBatch(resettable.map((f) => f.ref), origin);
597
+ });
598
+ /**
599
+ * The submit dataset resolver (Phase 4, D7): a FRESH engine read — never
600
+ * the cached model, so no staleness class exists — then the pure ISO
601
+ * builder. Selection/veto/value semantics live in `field-selection.ts`;
602
+ * this door only supplies the live fields and assembles the request.
603
+ */
604
+ const resolveSubmitDataset = async (intent, actionCtx, diagnose) => {
605
+ const doc = ctx.doc;
606
+ if (!doc) throw new Error("no document");
607
+ const snapshot = await doc.forms.list();
608
+ return {
609
+ url: intent.url,
610
+ method: intent.method,
611
+ format: intent.format,
612
+ flagsRaw: intent.flagsRaw,
613
+ ...intent.charSet === void 0 ? {} : { charSet: intent.charSet },
614
+ entries: buildSubmitEntries(snapshot.fields, intent, diagnose),
615
+ origin: actionCtx.origin,
616
+ event: actionCtx.event
617
+ };
618
+ };
619
+ const widgetSource = (key, annotationRef) => ({
620
+ kind: "widget",
621
+ field: refKeyOf(key),
622
+ annotation: annotationRef,
623
+ pon: annotationRef.pageObjectNumber
624
+ });
625
+ /** One shared hover pump (Exit→Enter as one ordered pair; intermediates
626
+ * skipped), created on first use against the resolved actions capability. */
627
+ let widgetPump = null;
628
+ const widgetHoverPump = (actions) => {
629
+ widgetPump ?? (widgetPump = createHoverPump(actions.dispatch));
630
+ return widgetPump;
631
+ };
632
+ const widgetHoverFlags = (annotationRef) => {
633
+ const loaded = annotationHost?.get(annotationRef);
634
+ if (loaded?.subtype !== "widget") return null;
635
+ return {
636
+ enter: Boolean(loaded.actions?.cursorEnter?.root),
637
+ exit: Boolean(loaded.actions?.cursorExit?.root)
638
+ };
639
+ };
923
640
  const nudgeAnnotations = (pons) => {
924
641
  if (!annotationHost) return;
925
642
  for (const pon of new Set(pons)) annotationHost.reloadPage(pon);
@@ -1063,32 +780,137 @@ function createFormCapability(ctx, options = {}) {
1063
780
  type: "choice",
1064
781
  values
1065
782
  }),
1066
- reset: (key) => enqueueMutation(async () => {
1067
- const doc = ctx.doc;
1068
- if (!doc) return;
1069
- apply({
1070
- t: "writeStart",
1071
- key
1072
- });
1073
- try {
1074
- const result = await doc.forms.reset(refKeyOf(key));
783
+ reset: (key) => {
784
+ if (!can("doc.forms.fill")) return Promise.reject(new PermissionDenied("doc.forms.fill", "form.reset"));
785
+ return enqueueMutation(async () => {
786
+ const doc = ctx.doc;
787
+ if (!doc) return;
1075
788
  apply({
1076
- t: "writeDone",
1077
- key,
1078
- field: result.field
1079
- });
1080
- } catch (err) {
1081
- apply({
1082
- t: "writeFailed",
789
+ t: "writeStart",
1083
790
  key
1084
791
  });
1085
- throw err;
1086
- }
1087
- }),
792
+ try {
793
+ if (doc.forms.applyEffects) {
794
+ const result = await applyResetBatch([refKeyOf(key)], "user");
795
+ if (result.status === "failed") throw new Error(result.effectsResult?.results.find((entry) => entry.status === "failed" || entry.status === "rejected")?.error?.message ?? "reset failed");
796
+ const field = fieldByKey(model(), key);
797
+ if (field) apply({
798
+ t: "writeDone",
799
+ key,
800
+ field
801
+ });
802
+ else apply({
803
+ t: "writeFailed",
804
+ key
805
+ });
806
+ } else {
807
+ const result = await doc.forms.reset(refKeyOf(key));
808
+ apply({
809
+ t: "writeDone",
810
+ key,
811
+ field: result.field
812
+ });
813
+ }
814
+ } catch (err) {
815
+ apply({
816
+ t: "writeFailed",
817
+ key
818
+ });
819
+ throw err;
820
+ }
821
+ });
822
+ },
1088
823
  commitValue: (ref, value) => enqueueMutation(() => commitValueNow(ref, value)),
1089
- activateWidget: (key, annotationRef) => enqueueMutation(() => activateWidgetNow(key, annotationRef)),
1090
- setUiEffectProvider: (provider) => {
1091
- uiEffectProvider = provider;
824
+ activateWidget: async (key, annotationRef) => {
825
+ const actions = ctx.tryGet(ActionsToken);
826
+ if (actions) {
827
+ const result = await actions.dispatch({
828
+ scope: "activate",
829
+ ref: annotationRef,
830
+ pon: annotationRef.pageObjectNumber,
831
+ source: widgetSource(key, annotationRef)
832
+ });
833
+ if (!(result.status === "inert" && result.steps.length === 0 && result.diagnostics.length === 0)) return {
834
+ kind: "dispatched",
835
+ result
836
+ };
837
+ }
838
+ return {
839
+ kind: "form",
840
+ result: await enqueueMutation(() => activateWidgetNow(key, annotationRef))
841
+ };
842
+ },
843
+ notifyWidgetEvent: (key, annotationRef, event) => {
844
+ const actions = ctx.tryGet(ActionsToken);
845
+ if (!actions) return;
846
+ const source = widgetSource(key, annotationRef);
847
+ if (event === "cursorEnter" || event === "cursorExit") {
848
+ if (event === "cursorExit") {
849
+ widgetHoverPump(actions).hover(null);
850
+ return;
851
+ }
852
+ const flags = widgetHoverFlags(annotationRef);
853
+ if (flags && !flags.enter && !flags.exit) return;
854
+ widgetHoverPump(actions).hover({
855
+ ref: annotationRef,
856
+ pon: annotationRef.pageObjectNumber,
857
+ source,
858
+ ...flags ? { events: flags } : {}
859
+ });
860
+ return;
861
+ }
862
+ actions.dispatch({
863
+ scope: "annotation",
864
+ event,
865
+ ref: annotationRef,
866
+ pon: annotationRef.pageObjectNumber,
867
+ source
868
+ });
869
+ },
870
+ resetFormAction,
871
+ resolveSubmitDataset,
872
+ commitScriptFormEffects: async (effects) => {
873
+ const doc = ctx.doc;
874
+ if (!doc?.forms.applyEffects) return {
875
+ results: effects.map((_, index) => ({
876
+ index,
877
+ status: "failed",
878
+ fields: [],
879
+ changedWidgets: [],
880
+ error: {
881
+ code: "NotSupported",
882
+ message: "no form-effects batch door"
883
+ }
884
+ })),
885
+ changedWidgets: [],
886
+ meta: null
887
+ };
888
+ let result;
889
+ try {
890
+ result = await doc.forms.applyEffects(effects);
891
+ } catch (error) {
892
+ const message = error instanceof Error ? error.message : String(error);
893
+ return {
894
+ results: effects.map((_, index) => ({
895
+ index,
896
+ status: "failed",
897
+ fields: [],
898
+ changedWidgets: [],
899
+ error: {
900
+ code: "Refused",
901
+ message
902
+ }
903
+ })),
904
+ changedWidgets: [],
905
+ meta: null
906
+ };
907
+ }
908
+ await refresh(true);
909
+ if (annotationHost) {
910
+ const pons = new Set(result.changedWidgets.map((widget) => widget.pageObjectNumber).filter((pon) => pon > 0));
911
+ for (const pon of pons) await annotationHost.reloadPage(pon);
912
+ }
913
+ return result;
1092
914
  },
1093
915
  setValue: (ref, value) => enqueueMutation(async () => {
1094
916
  const doc = ctx.doc;
@@ -1124,8 +946,9 @@ function createFormCapability(ctx, options = {}) {
1124
946
  updateField: (key, patch) => enqueueMutation(() => updateFieldNow(key, patch)),
1125
947
  deleteField: (key) => enqueueMutation(() => deleteFieldNow(key)),
1126
948
  detachWidget: (key, annotObjectNumber) => enqueueMutation(() => detachWidgetNow(key, annotObjectNumber)),
949
+ canRead: () => can("doc.forms.read"),
1127
950
  canFill: () => can("doc.forms.fill"),
1128
- canModify: () => can("doc.forms.modify")
951
+ canDesign: () => can("doc.forms.modify")
1129
952
  };
1130
953
  }
1131
954
  //#endregion
@@ -1159,7 +982,11 @@ function registerFormEffects(ctx) {
1159
982
  }
1160
983
  };
1161
984
  const unsubscribe = doc.events.subscribe((event) => {
1162
- if (!event.type.startsWith("form.")) return;
985
+ if (event.type === "stream.desynced") {
986
+ refresh();
987
+ return;
988
+ }
989
+ if (!event.type.startsWith("form.") || !("origin" in event)) return;
1163
990
  const structural = STRUCTURAL.has(event.type);
1164
991
  if (event.origin.kind !== "remote" && !structural) return;
1165
992
  if (structural) ctx.dispatch({
@@ -1275,7 +1102,7 @@ function createPlaceHandler(form, interaction, annotation) {
1275
1102
  enabledFor: (t) => t.enables.has("form-place"),
1276
1103
  onDown: (s) => {
1277
1104
  if (!s.page || !FORM_TOOL_BY_ID.has(interaction.activeToolId())) return false;
1278
- if (!form.canModify()) return false;
1105
+ if (!form.canDesign()) return false;
1279
1106
  origin = {
1280
1107
  pon: s.page.pon,
1281
1108
  start: s.page.point,
@@ -1342,9 +1169,6 @@ const initialFormState = () => ({ model: initialModel() });
1342
1169
  /** Dumb store: the pure core computes the next model; the shell dispatches it. */
1343
1170
  const formReducer = (state, action) => action.type === "SET_MODEL" ? { model: action.model } : state;
1344
1171
  //#endregion
1345
- //#region src/types.ts
1346
- const FormToken = createCapabilityToken("form");
1347
- //#endregion
1348
1172
  //#region src/form.plugin.ts
1349
1173
  /**
1350
1174
  * The form plugin: the FIELD plane. Document-scoped; requires the
@@ -1359,17 +1183,17 @@ const FormToken = createCapabilityToken("form");
1359
1183
  */
1360
1184
  const formPlugin = (options = {}) => definePlugin({
1361
1185
  id: "form",
1362
- token: FormToken,
1186
+ token: FormToken$1,
1363
1187
  scope: "document",
1364
1188
  requires: [InteractionToken],
1365
- optional: [AnnotationToken],
1189
+ optional: [AnnotationToken, ActionsToken],
1366
1190
  initialState: initialFormState,
1367
1191
  reduce: formReducer,
1368
1192
  capability: (ctx) => createFormCapability(ctx, options),
1369
1193
  effects: registerFormEffects,
1370
1194
  init: (ctx) => {
1371
1195
  const interaction = ctx.get(InteractionToken);
1372
- const form = ctx.get(FormToken);
1196
+ const form = ctx.get(FormToken$1);
1373
1197
  interaction.registerTool({
1374
1198
  id: "form-fill",
1375
1199
  cursor: "default",
@@ -1400,6 +1224,49 @@ const formPlugin = (options = {}) => definePlugin({
1400
1224
  matches: (a) => a.subtype.startsWith("widget"),
1401
1225
  engaged: () => interaction.activeTool()?.enables.has("form-fill") ?? false
1402
1226
  });
1227
+ const actions = ctx.tryGet(ActionsToken$1);
1228
+ if (actions) {
1229
+ ctx.cleanup(actions.registerFormCommitSink((effects) => {
1230
+ const formHost = ctx.tryGet(FormToken$1);
1231
+ if (!formHost) return Promise.resolve({
1232
+ results: effects.map((_, index) => ({
1233
+ index,
1234
+ status: "failed",
1235
+ fields: [],
1236
+ changedWidgets: []
1237
+ })),
1238
+ changedWidgets: [],
1239
+ meta: null
1240
+ });
1241
+ return formHost.commitScriptFormEffects(effects);
1242
+ }));
1243
+ ctx.cleanup(actions.registerSubmitResolver((intent, actionCtx, diagnose) => {
1244
+ const formHost = ctx.tryGet(FormToken$1);
1245
+ if (!formHost) throw new Error("form plugin unavailable");
1246
+ return formHost.resolveSubmitDataset(intent, actionCtx, diagnose);
1247
+ }));
1248
+ ctx.cleanup(actions.registerExecutor("reset-form", async (node, actionCtx) => {
1249
+ if (node.type !== "reset-form") return {
1250
+ status: "inert",
1251
+ reason: "not a reset-form node"
1252
+ };
1253
+ const formHost = ctx.tryGet(FormToken$1);
1254
+ if (!formHost) return {
1255
+ status: "inert",
1256
+ reason: "form plugin unavailable"
1257
+ };
1258
+ const result = await formHost.resetFormAction(node.fields, node.exclude, actionCtx.origin);
1259
+ if (result.status === "failed") return {
1260
+ status: "failed",
1261
+ error: result.error?.message ?? "reset failed"
1262
+ };
1263
+ if (result.effectsResult === null) return {
1264
+ status: "inert",
1265
+ reason: result.diagnostics[0]?.message ?? "reset-form resolved no fields"
1266
+ };
1267
+ return { status: "executed" };
1268
+ }));
1269
+ }
1403
1270
  }
1404
1271
  });
1405
1272
  //#endregion