Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
23e1821
M12: XFA enter and exit scripts run whenever a field gains or loses f…
EricKaden Oct 24, 2026
5641420
M12: docstrings for the helpers the focus-script change touches
EricKaden Oct 24, 2026
66c70c0
M12: the focus-script check's comment lists only the ways the focus c…
EricKaden Oct 24, 2026
be08886
M12: a focus script that changes the field being edited shows in its …
EricKaden Oct 24, 2026
7e00c61
M12: the editor follows each focus script in turn, so an exit then an…
EricKaden Oct 24, 2026
2e8720c
M12: an exit skipped after a value change is skipped only until the n…
EricKaden Oct 24, 2026
3257bac
M12: a batched focus event that changes nothing keeps the batch's edi…
EricKaden Oct 24, 2026
e3a4ef8
M12: docstrings for the functions next to the focus-script change
EricKaden Oct 24, 2026
756a7fb
M12: XFA check boxes and radio buttons run their click scripts when a…
EricKaden Oct 24, 2026
262013a
M12: a form script event can't be part of a batch of edits
EricKaden Oct 24, 2026
e550622
M12: a check box or radio button set to the value it had runs no clic…
EricKaden Oct 24, 2026
629ba85
M12: docstrings for the functions next to the click-script change
EricKaden Oct 24, 2026
8e52d9e
M12: comments for the closures the click-script change adds
EricKaden Oct 24, 2026
9776ced
Merge remote-tracking branch 'upstream/main' into fix/xfa-check-box-c…
EricKaden Oct 24, 2026
0c88cc9
M12: the click-script UI test waits for the form as main's tests now do
EricKaden Oct 24, 2026
3ae1baa
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 24, 2026
a32b418
Merge remote-tracking branch 'upstream/main' into fix/xfa-check-box-c…
EricKaden Oct 24, 2026
9c5c9a5
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 24, 2026
c84be71
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 24, 2026
d25ad8b
Merge remote-tracking branch 'upstream/main' into fix/xfa-check-box-c…
EricKaden Oct 24, 2026
8e9b734
M6: a check box a batch sets twice is clicked once
EricKaden Oct 24, 2026
3f6471e
M6: a radio group's change and click scripts run before its button's,…
EricKaden Oct 24, 2026
6a29250
M12: a radio group's own enter and exit scripts run when the focus en…
EricKaden Oct 24, 2026
0a6d8fe
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 24, 2026
8ae36ad
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 24, 2026
aad694b
M12: a widget index the field doesn't have runs no focus scripts
EricKaden Oct 24, 2026
d8ab397
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 24, 2026
4f9ac46
M12: a radio button the group doesn't have runs none of the group's f…
EricKaden Oct 24, 2026
6a1e089
M12: committing the form runs the focus scripts of a move not yet run
EricKaden Oct 25, 2026
6236a6a
Merge remote-tracking branch 'origin/fix/xfa-check-box-click-scripts'…
EricKaden Oct 25, 2026
ab152be
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 25, 2026
af27c3a
M12: Revert drops the focus without running the field's exit
EricKaden Oct 25, 2026
343a866
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 25, 2026
f104429
M12: check box and radio exit scripts wait for the focus to leave
EricKaden Oct 25, 2026
1591b6b
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 25, 2026
149dd4e
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 25, 2026
9bb0bd5
M12: a focus script that runs away stops the rest without an error, a…
EricKaden Oct 25, 2026
5cf5957
Merge branch 'fix/xfa-radio-group-focus' into fix/xfa-toggle-exit-on-…
EricKaden Oct 25, 2026
3e339bb
M12: Revert forgets which check box or radio group had the focus
EricKaden Oct 25, 2026
a51feb4
M12: the parity notes say what focus handling still lacks
EricKaden Oct 25, 2026
9bb4ac1
M12: a focus move builds the live form once to find its scripts
EricKaden Oct 25, 2026
820e754
Merge branch 'fix/xfa-radio-group-focus' into fix/xfa-toggle-exit-on-…
EricKaden Oct 25, 2026
f098d19
Merge remote-tracking branch 'upstream/main' into fix/xfa-check-box-c…
EricKaden Oct 25, 2026
470a89b
Merge remote-tracking branch 'upstream/main' into fix/xfa-enter-exit-…
EricKaden Oct 25, 2026
9d385c2
Merge branch 'fix/xfa-enter-exit-on-focus' into fix/xfa-radio-group-f…
EricKaden Oct 25, 2026
aca60f7
Merge remote-tracking branch 'origin/fix/xfa-check-box-click-scripts'…
EricKaden Oct 25, 2026
110cb28
Merge remote-tracking branch 'origin/fix/xfa-radio-group-focus' into …
EricKaden Oct 25, 2026
db13a3a
M12: XFA fields validate before they exit, and a list box validates a…
EricKaden Oct 25, 2026
c419cbb
M12: the focus-script lookup runs behind the guard, focus calculation…
EricKaden Oct 25, 2026
19ca4a9
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 26, 2026
7d574fb
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 27, 2026
ef3a4a1
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 28, 2026
7f01d1c
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 28, 2026
8c0edc5
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 28, 2026
1c9f9a1
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 29, 2026
44a2b8f
Merge remote-tracking branch 'upstream/main' into fix/xfa-validate-be…
EricKaden Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crates/automation/src/a11y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,13 +733,17 @@ impl Automation {
Ok(json!({ "scripts": list }))
}

/// `js_set_document_script`: set the document-level script `name` to `script`, or remove it
/// when `script` is left out, as one undoable edit; returns the document's scripts.
pub(crate) fn js_set_document_script(&mut self, a: &Args) -> Result<Value> {
let edit = pdfcraft_engine::Edit::SetDocumentScript { name: a.str("name")?.into(), script: a.opt_str("script")?.map(str::to_string) };
let id = self.doc(a)?.id;
self.session.apply(id, edit).map_err(failed)?;
self.js_document_scripts(a)
}

/// `form_set_script`: set field `field`'s JavaScript for `event` to `script`, or remove it when
/// `script` is left out, as one undoable edit; returns the console output and errors.
pub(crate) fn form_set_script(&mut self, a: &Args) -> Result<Value> {
let edit = pdfcraft_engine::Edit::SetFieldScript {
name: a.str("field")?.into(),
Expand All @@ -752,6 +756,36 @@ impl Automation {
Ok(json!({ "field": a.str("field")?, "event": a.str("event")?, "console": out.console, "errors": out.errors }))
}

/// `form_focus`: field `field` (its 1-based `widget`) gains (`enter`) or loses (`exit`)
/// focus, running its XFA scripts for that event (a radio group's own too, unless
/// `within_group`: the focus moves between two of its buttons); returns whether the form
/// changed and what the scripts showed or asked for.
pub(crate) fn form_focus(&mut self, a: &Args) -> Result<Value> {
use pdfcraft_engine::js::FieldFocus;
let focus = match a.str("event")? {
"enter" => FieldFocus::Enter,
"exit" => FieldFocus::Exit,
other => return Err(ToolError::InvalidArgs(format!("event must be enter or exit, not {other:?}"))),
};
let name = a.str("field")?;
let doc = self.doc(a)?;
let field = doc.form.iter().find(|f| f.name == name).ok_or_else(|| failed(format!("there is no field named {name:?} (see form_fields)")))?;
let widget = a.opt_int("widget")?.unwrap_or(1);
let widget = usize::try_from(widget)
.ok()
.and_then(|w| w.checked_sub(1))
.filter(|w| *w < field.widgets.len().max(1))
.ok_or_else(|| ToolError::InvalidArgs(format!("widget must be 1 to {} for {name:?}", field.widgets.len().max(1))))?;
let id = doc.id;
let crossing = !a.opt_bool("within_group")?.unwrap_or(false);
let changed = self.session.field_focus_crossing(id, name, widget, focus, crossing).map_err(failed)?;
let out = self.session.take_js_output(id);
let reqs: Vec<Value> = out.requests.iter().map(request_json).collect();
Ok(json!({ "changed": changed, "alerts": out.alerts, "console": out.console, "requests": reqs, "errors": out.errors }))
}

/// `form_merge_data`: merge the form data files at `paths` into one CSV at `path`, a row per
/// file; returns the path and the row and column counts.
pub(crate) fn form_merge_data(&mut self, a: &Args) -> Result<Value> {
let mut files = Vec::new();
for p in a.strs("paths")? {
Expand Down
1 change: 1 addition & 0 deletions crates/automation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ impl Automation {
}
"form_fields" => self.form_fields(&a)?,
"form_fill" => self.form_fill(&a)?,
"form_focus" => self.form_focus(&a)?,
"form_set_image" => {
let path = self.resolve(a.str("path")?, false)?;
let bytes = read_input(&path)?;
Expand Down
15 changes: 15 additions & 0 deletions crates/automation/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,21 @@ pub fn tools() -> Vec<ToolDef> {
"Set several fields at once (one undo step). values maps field names (from form_fields) to: a string for text fields, combo boxes and radio groups (an option), true/false for check boxes, an array of strings for multi-select lists. Appearances are regenerated so every viewer shows the values. A value whose characters the field's font can't draw (Chinese in a Helvetica field) is still saved; `warnings` in the result names the field and character: PdfCraft draws `?` there and asks other viewers to redraw the field from its value.",
)
.with(schema(json!({ "doc": doc(), "values": { "type": "object", "description": "Field name → value." } }), &["doc", "values"])),
t(
"form_focus",
"Move the focus into or out of a field",
"A field gains (event enter) or loses (event exit) focus, as when someone clicks or tabs into or out of it, and the dynamic XFA form's enter or exit scripts for it run (other forms have none, and nothing happens). widget picks the radio button or copy of the field (1-based, default 1; see form_fields). What the scripts change is one undoable edit, and none when they change nothing. A check box or radio group form_fill sets while the focus is on it runs its exit scripts here, when it loses the focus, not with the change. Changing a field's value (form_fill) runs its exit scripts already, so the next exit of that field doesn't run them again. For a radio button the focus also enters or leaves its group, whose own enter scripts run before the button's and exit scripts after; set within_group when the focus moves between two buttons of one group, which runs only theirs. Returns changed, and the scripts' alerts, console output, requests and errors.",
)
.with(schema(
json!({
"doc": doc(),
"field": { "type": "string", "description": "Field name (from form_fields)." },
"event": { "type": "string", "enum": ["enter", "exit"] },
"widget": { "type": "integer", "minimum": 1, "description": "Widget of the field (1-based, default 1): a radio button of the group, or a copy of the field." },
"within_group": { "type": "boolean", "description": "The focus moves between two buttons of one radio group (default false): the group's own enter and exit scripts don't run." },
}),
&["doc", "field", "event"],
)),
t("form_reset", "Clear form", "Reset fields to their default values: all of them, or only those listed. Undoable.")
.destructive()
.with(schema(json!({ "doc": doc(), "fields": { "type": "array", "items": { "type": "string" } } }), &["doc"])),
Expand Down
129 changes: 129 additions & 0 deletions crates/automation/tests/it/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7379,6 +7379,8 @@ fn growable_xfa_text_paginates_after_fill_save_and_print() {
let _ = std::fs::remove_dir_all(dir);
}

/// A print button repeated on every page of an XFA form runs its own XFA click script (a print
/// request) through js_run, from each copy, before and after saving and reopening.
#[test]
fn repeated_xfa_print_buttons_work_through_tools_and_after_reopen() {
let dir = workdir("xfa-print-buttons");
Expand Down Expand Up @@ -7406,6 +7408,73 @@ fn repeated_xfa_print_buttons_work_through_tools_and_after_reopen() {
check(&mut a, doc);
}

/// form_focus runs a radio group's own enter and exit scripts when the focus enters or leaves the
/// group, and not when within_group says it moves between two of its buttons.
#[test]
fn a_radio_groups_own_focus_scripts_run_through_form_focus() {
let dir = workdir("xfa-group-focus");
std::fs::write(dir.join("group.pdf"), pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::group_focus_template())).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "group.pdf" }))["doc"].as_u64().unwrap();
let fields = ok(&mut a, "form_fields", json!({ "doc": doc }));
let group = fields["fields"].as_array().unwrap().iter().find(|f| f["type"] == "radio").unwrap()["name"].clone();
let log = |a: &mut Automation| {
let fields = ok(a, "form_fields", json!({ "doc": doc }));
fields["fields"].as_array().unwrap().iter().find(|f| f["name"] == "log").map(|f| f["value"].clone()).unwrap_or(Value::Null)
};
ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "event": "enter" }));
ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "event": "exit", "within_group": true }));
ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "widget": 2, "event": "enter", "within_group": true }));
ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "widget": 2, "event": "exit" }));
assert_eq!(log(&mut a), json!("grp enter|r1 enter|r1 exit|r2 enter|r2 exit|grp exit"));
let _ = std::fs::remove_dir_all(&dir);
}

/// form_focus runs a field's XFA enter and exit scripts on every focus change; scripts that
/// change nothing leave no edit, a radio button's own scripts run for its widget, and unknown
/// fields, widgets and events are refused.
#[test]
fn xfa_focus_scripts_run_through_form_focus() {
let dir = workdir("xfa-focus");
std::fs::write(dir.join("focus.pdf"), pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::focus_template())).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "focus.pdf" }))["doc"].as_u64().unwrap();
let value = |a: &mut Automation, n: &str| {
let f = ok(a, "form_fields", json!({ "doc": doc }));
f["fields"].as_array().unwrap().iter().find(|f| f["name"] == n).map(|f| f["value"].clone()).unwrap()
};
for _ in 0..3 {
assert_eq!(ok(&mut a, "form_focus", json!({ "doc": doc, "field": "probe", "event": "enter" }))["changed"], true);
assert_eq!(ok(&mut a, "form_focus", json!({ "doc": doc, "field": "probe", "event": "exit" }))["changed"], true);
}
assert_eq!((value(&mut a, "enters"), value(&mut a, "exits")), (json!("3"), json!("3")));
ok(&mut a, "edit_undo", json!({ "doc": doc }));
assert_eq!(value(&mut a, "exits"), "2", "each run is one undo step");
// A script that only shows a message changes nothing: no edit (the document stays saved and
// what was undone can still be redone), and the message comes back.
ok(&mut a, "doc_save", json!({ "doc": doc, "path": "saved.pdf" }));
let quiet = ok(&mut a, "form_focus", json!({ "doc": doc, "field": "quiet", "event": "exit" }));
assert_eq!((quiet["changed"].clone(), quiet["alerts"].clone()), (json!(false), json!(["left quiet"])), "{quiet}");
let state = &ok(&mut a, "doc_info", json!({ "doc": doc }))["document"];
assert_eq!((state["dirty"].clone(), state["redo"].clone()), (json!(false), json!("Run form script")), "{state}");
// The second radio button has an enter script of its own.
let fields = ok(&mut a, "form_fields", json!({ "doc": doc }));
let group = fields["fields"].as_array().unwrap().iter().find(|f| f["type"] == "radio").unwrap()["name"].clone();
assert_eq!(ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "event": "enter" }))["changed"], false);
assert_eq!(ok(&mut a, "form_focus", json!({ "doc": doc, "field": group, "event": "enter", "widget": 2 }))["changed"], true);
assert_eq!(value(&mut a, "picks"), "1");
for bad in [
json!({ "doc": doc, "field": "nowhere", "event": "enter" }),
json!({ "doc": doc, "field": group, "event": "enter", "widget": 3 }),
json!({ "doc": doc, "field": "probe", "event": "enter", "widget": 0 }),
json!({ "doc": doc, "field": "probe", "event": "click" }),
] {
assert!(a.call("form_focus", &bad).is_err(), "{bad}");
}
}

/// XFA scripts run through the tools: initialize and calculate on opening, calculations after
/// form_fill, and a button's click script through js_run, adding a row that undo takes away.
#[test]
fn xfa_scripts_run_for_buttons_and_field_changes_through_tools() {
let dir = workdir("xfa-scripts");
Expand Down Expand Up @@ -7437,6 +7506,66 @@ fn xfa_scripts_run_for_buttons_and_field_changes_through_tools() {
assert_eq!(r["alerts"], json!(["Hello 4"]));
}

/// Filling an XFA check box or radio group through form_fill clicks it, as the app does: the
/// change scripts, then the click scripts, a radio group's before those of the button now on.
#[test]
fn xfa_check_box_and_radio_clicks_run_through_form_fill() {
let dir = workdir("xfa-toggle");
std::fs::write(dir.join("toggle.pdf"), pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::toggle_template())).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "toggle.pdf" }))["doc"].as_u64().unwrap();
// What the scripts have written to the form's log field so far.
let log = |a: &mut Automation| {
let f = ok(a, "form_fields", json!({ "doc": doc }));
f["fields"]
.as_array()
.unwrap()
.iter()
.find(|f| f["name"] == "log")
.map(|f| f["value"].as_str().unwrap_or_default().to_string())
.unwrap_or_default()
};
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "agree": true } }));
assert_eq!(log(&mut a), "change 1;click 1;");
let fields = ok(&mut a, "form_fields", json!({ "doc": doc }));
let answer = fields["fields"].as_array().unwrap().iter().find(|f| f["name"] == "answer").cloned().expect("the radio group");
let no = answer["options"].as_array().and_then(|o| o.get(1)).and_then(|o| o.as_str().map(str::to_string)).unwrap_or_else(|| panic!("{answer}"));
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "answer": no } }));
assert!(log(&mut a).ends_with("group click;no click;no exit;group exit;"), "{}", log(&mut a));
}

/// A check box filled while form_focus has put the focus on it keeps its exit for the focus
/// loss: filled twice, it runs its exit once, on form_focus exit. Filled without the focus on
/// it, its exit follows the change.
#[test]
fn a_check_box_filled_with_the_focus_runs_its_exit_on_form_focus_exit() {
let dir = workdir("xfa-toggle-focus");
std::fs::write(dir.join("toggle.pdf"), pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::toggle_focus_template())).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "toggle.pdf" }))["doc"].as_u64().unwrap();
let log = |a: &mut Automation| {
let f = ok(a, "form_fields", json!({ "doc": doc }));
f["fields"]
.as_array()
.unwrap()
.iter()
.find(|f| f["name"] == "log")
.map(|f| f["value"].as_str().unwrap_or_default().to_string())
.unwrap_or_default()
};
ok(&mut a, "form_focus", json!({ "doc": doc, "field": "cb", "event": "enter" }));
for on in [true, false] {
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "cb": on } }));
}
assert_eq!(log(&mut a), "cb enter 0|cb change 1|cb click 1|cb change 0|cb click 0", "no exit while it has the focus");
ok(&mut a, "form_focus", json!({ "doc": doc, "field": "cb", "event": "exit" }));
assert!(log(&mut a).ends_with("|cb click 0|cb exit 0"), "{}", log(&mut a));
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "cb": true } }));
assert!(log(&mut a).ends_with("|cb exit 0|cb change 1|cb click 1|cb exit 1"), "without the focus: {}", log(&mut a));
let _ = std::fs::remove_dir_all(&dir);
}

/// A form whose opening script runs away has its scripts turned off, and doc_info says so.
#[test]
fn active_xfa_skips_container_fallback_scripts_after_save_and_reopen() {
let dir = workdir("xfa-container-scripts");
Expand Down
75 changes: 75 additions & 0 deletions crates/engine/src/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ pub use pdfcraft_js::{Limits, Outcome, Panel, Request};

use crate::{DocId, Edit, EditError, Session};

/// A field gaining or losing focus ([`Session::field_focus`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FieldFocus {
/// The field gained focus: its `enter` scripts run.
Enter,
/// The field lost focus: its `exit` scripts run.
Exit,
}

/// What scripts produced for the viewer since it last looked.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct JsOutput {
Expand Down Expand Up @@ -260,6 +269,72 @@ impl Session {
self.docs.iter_mut().find(|d| d.id == id).map(|d| std::mem::take(&mut d.js_output)).unwrap_or_default()
}

/// Widget `widget` of field `name` in document `id` gained or lost focus: on a laid-out XFA form,
/// run the field's `enter` or `exit` scripts, as one undoable edit when they change something
/// (and none when they don't). An exit that the field's new value already ran isn't run a
/// second time: a text field's change runs it, as does a value set without the focus on the
/// field, while a check box or radio group set while it has the focus runs it only here. For
/// a radio button the focus also enters or leaves its group
/// ([`Session::field_focus_crossing`]). Returns whether the document changed; what the
/// scripts showed or asked for is in [`Session::take_js_output`].
pub fn field_focus(&mut self, id: DocId, name: &str, widget: usize, focus: FieldFocus) -> Result<bool, EditError> {
self.field_focus_crossing(id, name, widget, focus, true)
}

/// [`Session::field_focus`], saying whether the focus also enters or leaves the radio group
/// whose button is widget `widget` (`crossing`); it doesn't when it moves between two buttons
/// of one group. As another viewer runs them, entering a group runs the group's enter scripts
/// before the button's and leaving it runs the group's exit scripts after the button's, while a
/// move between its buttons runs only theirs. Each object's scripts are an undoable edit of
/// their own when they change something.
pub fn field_focus_crossing(&mut self, id: DocId, name: &str, widget: usize, focus: FieldFocus, crossing: bool) -> Result<bool, EditError> {
if self.js_off {
return Ok(false);
}
let doc = self.doc_mut(id)?;
let Some(tpl) = doc.xfa_template.clone().filter(|t| pdfcraft_xfa::has_focus_scripts(t)) else { return Ok(false) };
let Some(cos) = doc.editor.as_ref().map(|e| &e.cos) else { return Ok(false) };
let Some(som) = doc.form.iter().find(|f| f.name == name).and_then(|f| crate::xfa::widget_som(cos, f, widget)) else { return Ok(false) };
Comment thread
EricKaden marked this conversation as resolved.
Resolved
let activity = match focus {
FieldFocus::Enter => "enter",
FieldFocus::Exit => "exit",
};
// Where the focus is now: a check box or radio group set while it has the focus runs its
// exit scripts when it loses it, not with the change.
doc.xfa_focus = (focus == FieldFocus::Enter).then(|| som.clone());
// A list box picked in while it had the focus validates as it loses it, before its exit;
// gaining focus starts afresh.
let validate = match focus {
FieldFocus::Enter => {
doc.xfa_unvalidated.clear();
false
}
FieldFocus::Exit => doc.xfa_unvalidated.remove(&som),
};
// Which of them run, decided before any does: each edit forgets the exits a value's
// change ran. Gaining focus starts afresh; losing it runs an exit unless the change just did.
let mut run = Vec::new();
for (som, has) in crate::xfa::focus_targets(cos, &tpl, &som, activity, crossing) {
let ran = doc.xfa_exited.remove(&som);
if has && !(ran && focus == FieldFocus::Exit) {
run.push(som);
}
}
let before = doc.generation;
if validate {
self.apply(id, Edit::XfaEvent { som: som.clone(), activity: "validate".into() })?;
}
for som in run {
// A script that ran away turned the form's scripts off (the group's, say): the rest
// don't run, and the notice that says so is in the output.
if self.get(id).is_some_and(|d| d.xfa_template.is_none()) {
break;
}
self.apply(id, Edit::XfaEvent { som, activity: activity.into() })?;
}
Ok(self.get(id).is_some_and(|d| d.generation != before))
}

/// Run `script` in document `id`, for a button (`target`: the field, event "Mouse Up") or
/// the console (no target). Field changes and `resetForm` are applied as one undoable step;
/// everything else the script asked for is returned.
Expand Down
Loading
Loading