diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ed53ad2a50..2dd652263d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9507,3 +9507,26 @@ fn repeated_backdrop_artwork_survives_background_undo_redo_and_both_save_modes() s.apply(rewritten, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap(); black(&s, rewritten); } + +/// A reset in an opening script reaches a field of a row an earlier opening script added, before +/// the form is laid out again with the row's widgets: its data goes back to its default and stays +/// so when the form is saved. +#[test] +fn an_opening_reset_reaches_a_row_added_while_opening() { + let tpl = pdfcraft_xfa::fixtures::scripted_template().replace( + "if (qty.rawValue === null) qty.rawValue = 2;", + r#"if (qty.rawValue === null) qty.rawValue = 2; table._row.addInstance(1);"#, + ) + .replace( + r#""#, + r#""#, + ); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("rows.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&tpl)), None).unwrap(); + let out = s.take_js_output(id); + assert!(out.errors.is_empty(), "{:?}", out.errors); + let cos = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap(); + let data = pdfcraft_xfa::data_of(&cos).unwrap(); + assert_eq!(data.count(&pdfcraft_xfa::som_to_path("form[0].page1[0].table[0]"), "row"), 2, "the row was added"); + assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path("form[0].page1[0].table[0].row[1].item[0]")).unwrap_or(""), "", "reset in the data"); +} diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index fd1249df2d..d0b674bc04 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -590,7 +590,34 @@ impl<'a, 'b> Runner<'a, 'b> { } else { Some(by_som.iter().filter(|(som, _)| names.iter().any(|n| som_matches(som, n))).map(|(_, f)| f.clone()).collect()) }; + // Fields of the live form with no widget yet (a row an opening script added, before the + // form is laid out again): their data goes back to the template's default too, or the + // value would stay and be laid out and saved. + let unshown: Vec = { + let live = self.live(); + let mut out = Vec::new(); + let mut stack: Vec<&pdfcraft_xfa::FormNode> = vec![&live.root]; + while let Some(n) = stack.pop() { + if n.kind == Some(pdfcraft_xfa::NodeKind::Field) + && !by_som.contains_key(&n.som) + && (names.is_empty() || names.iter().any(|name| som_matches(&n.som, name))) + { + out.push(n.som.clone()); + } + stack.extend(n.children.iter()); + } + out + }; + for som in unshown { + let Some(text) = pdfcraft_xfa::reset_default(self.tpl, &som) else { continue }; + let path = pdfcraft_xfa::data_path(self.tpl, &som); + if self.data.as_ref().and_then(|d| d.text_at(&path)).unwrap_or("") != text { + self.data_op(pdfcraft_xfa::DataOp::Value { path, text }); + self.changes.values += 1; + } + } if listed.as_ref().is_some_and(Vec::is_empty) { + self.invalidate(); return Ok(()); } pdfcraft_forms::reset(self.doc, listed.as_deref()).map_err(|e| e.to_string())?; diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index 7e63218c90..811e2f4de3 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -103,7 +103,7 @@ pub use pdf::{CLICK_KEY, LAYOUT_KEY, NODE_KEY, SOM_KEY, Stripped, XfaShape, exis pub use picture::typed_date; pub use script::{ FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, field_grows_with_text, - fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, set_overrides, template_of, + fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, reset_default, set_overrides, template_of, }; #[derive(Debug, thiserror::Error)] diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 6946aa9b1c..f0906a603a 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -575,6 +575,17 @@ pub fn fields_by_som(doc: &Document) -> HashMap { out } +/// What a reset gives the field at `som` in the data: its template default, or nothing when it has +/// none. `None` for an object a reset restores no value of: not a field of the template, or a +/// button, signature or picture field. +pub fn reset_default(tpl: &Template, som: &str) -> Option { + let f = crate::data::template_field(tpl, som)?; + if matches!(f.ui, crate::model::Ui::Button | crate::model::Ui::Signature | crate::model::Ui::ImageEdit) { + return None; + } + Some(f.value.plain().unwrap_or_default()) +} + /// Lay the form out again from its template, data and overrides, replacing the pages and the /// generated fields (a script added a row or hid a subform). pub fn rerender(doc: &mut Document, tpl: &Template) -> Result {