@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/contract/host.cjs +3 -0
- package/dist/contract/host.d.cts +2 -0
- package/dist/contract/host.d.ts +2 -0
- package/dist/contract/host.js +2 -0
- package/dist/contract.cjs +7 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +8 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +7 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.cjs +467 -600
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -239
- package/dist/index.d.ts +10 -239
- package/dist/index.js +455 -588
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +4 -0
- package/dist/internal.d.cts +3 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +3 -0
- package/dist/scripting.cjs +537 -0
- package/dist/scripting.cjs.map +1 -0
- package/dist/scripting.d.cts +60 -0
- package/dist/scripting.d.ts +60 -0
- package/dist/scripting.js +534 -0
- package/dist/scripting.js.map +1 -0
- package/dist/types-BlXdnNRf.d.ts +277 -0
- package/dist/types-Blr4QQM5.d.cts +277 -0
- package/dist/types-C1uIIYfn.cjs +16 -0
- package/dist/types-C1uIIYfn.cjs.map +1 -0
- package/dist/types-DkQmmcTG.js +12 -0
- package/dist/types-DkQmmcTG.js.map +1 -0
- package/package.json +25 -8
package/dist/index.js
CHANGED
|
@@ -1,8 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
/**
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
})).
|
|
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(
|
|
851
|
-
|
|
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) =>
|
|
866
|
-
if (!
|
|
867
|
-
|
|
868
|
-
|
|
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: "
|
|
479
|
+
t: "writeStart",
|
|
884
480
|
key
|
|
885
481
|
});
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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(
|
|
919
|
-
|
|
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) =>
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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: "
|
|
1077
|
-
key,
|
|
1078
|
-
field: result.field
|
|
1079
|
-
});
|
|
1080
|
-
} catch (err) {
|
|
1081
|
-
apply({
|
|
1082
|
-
t: "writeFailed",
|
|
789
|
+
t: "writeStart",
|
|
1083
790
|
key
|
|
1084
791
|
});
|
|
1085
|
-
|
|
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) =>
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|