mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 04:15:31 +00:00
M12: a focus script that changes the field being edited shows in its editor, so leaving it keeps the script's value
This commit is contained in:
@@ -474,7 +474,9 @@ impl PdfCraftApp {
|
||||
/// however it moved (a click, Tab or Shift+Tab, Escape, a click on the page): the
|
||||
/// field it left runs its exit, then the field it reached runs its enter. The engine skips an
|
||||
/// exit the field's committed change already ran. A tab that closes runs nothing: its
|
||||
/// document is going away, and a script changing it then would only ask to save it.
|
||||
/// document is going away, and a script changing it then would only ask to save it. A script
|
||||
/// that changes the field being edited (an enter clearing a placeholder) shows in its editor
|
||||
/// ([`crate::forms_ui::refresh_draft`]), so leaving the field doesn't write the old value back.
|
||||
fn run_focus_scripts(&mut self, i: usize) {
|
||||
let Some(view) = self.views.get_mut(i) else { return };
|
||||
let now = view.forms.focus.as_ref().map(|f| (f.name.clone(), f.widget));
|
||||
@@ -483,6 +485,9 @@ impl PdfCraftApp {
|
||||
}
|
||||
let left = std::mem::replace(&mut view.forms.focused, now.clone());
|
||||
let id = view.id;
|
||||
// The edited field's value before the scripts, to tell its untouched draft from typing.
|
||||
let before: Option<Vec<String>> =
|
||||
now.as_ref().and_then(|(name, _)| self.session.get(id)?.form.iter().find(|f| &f.name == name).map(|f| f.value.clone()));
|
||||
use pdfcraft_engine::js::FieldFocus;
|
||||
for ((name, widget), focus) in [left.map(|l| (l, FieldFocus::Exit)), now.map(|n| (n, FieldFocus::Enter))].into_iter().flatten() {
|
||||
match self.session.field_focus(id, &name, widget, focus) {
|
||||
@@ -492,6 +497,12 @@ impl PdfCraftApp {
|
||||
&& let Some(view) = self.views.get_mut(i)
|
||||
{
|
||||
view.document_changed(&info);
|
||||
if let Some(before) = &before
|
||||
&& let Some(draft) = view.forms.focus.as_mut()
|
||||
&& let Some(f) = self.session.get(id).and_then(|d| d.form.iter().find(|f| f.name == draft.name))
|
||||
{
|
||||
crate::forms_ui::refresh_draft(draft, before, &f.value);
|
||||
}
|
||||
}
|
||||
let out = self.session.take_js_output(id);
|
||||
self.handle_js(id, out);
|
||||
|
||||
@@ -108,6 +108,16 @@ fn open_focus(f: &FormField, widget: usize) -> Focus {
|
||||
}
|
||||
}
|
||||
|
||||
/// After a script changed the value of the field being edited from `before` to `after`: the
|
||||
/// editor shows `after` (as [`open_focus`] would), unless the person has typed there already
|
||||
/// (the draft no longer holds `before`), whose typing then stays theirs to commit.
|
||||
pub(crate) fn refresh_draft(focus: &mut Focus, before: &[String], after: &[String]) {
|
||||
if focus.picked.as_slice() == before && focus.text == before.first().map(String::as_str).unwrap_or_default() {
|
||||
focus.text = after.first().cloned().unwrap_or_default();
|
||||
focus.picked = after.to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
/// Select a radio button, if it isn't selected already.
|
||||
fn select_edit(f: &FormField, w: &FormWidget) -> Option<Edit> {
|
||||
(w.on_state.as_ref() != f.value.first()).then(|| Edit::SetFieldValue { name: f.name.clone(), value: FieldValue::Radio(w.on_state.clone()) })
|
||||
@@ -812,3 +822,42 @@ pub fn field_screen_rect(view: &DocView, info: &DocInfo, f: &FormField, widget:
|
||||
let xf = view.page_xform(page)?;
|
||||
Some(widget_rect(&xf, info, page, w.rect))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A draft as the editor opens it on a field holding `value`.
|
||||
fn draft(value: &[&str]) -> Focus {
|
||||
Focus {
|
||||
name: "hint".into(),
|
||||
widget: 0,
|
||||
text: value.first().map(|s| s.to_string()).unwrap_or_default(),
|
||||
picked: value.iter().map(|s| s.to_string()).collect(),
|
||||
request_focus: true,
|
||||
select_all: false,
|
||||
press: None,
|
||||
calendar: None,
|
||||
calendar_rect: None,
|
||||
calendar_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// An untouched draft takes the value a script gave the field; typing is kept.
|
||||
#[test]
|
||||
fn a_draft_follows_a_scripts_change_until_something_is_typed() {
|
||||
let before = vec!["Type here".to_string()];
|
||||
let mut untouched = draft(&["Type here"]);
|
||||
refresh_draft(&mut untouched, &before, &[]);
|
||||
assert_eq!((untouched.text.as_str(), untouched.picked.len()), ("", 0));
|
||||
let mut typed = draft(&["Type here"]);
|
||||
typed.text = "Ann".into();
|
||||
refresh_draft(&mut typed, &before, &[]);
|
||||
assert_eq!(typed.text, "Ann", "the person's typing stays");
|
||||
// A list's choices count as typing too.
|
||||
let mut picked = draft(&["a"]);
|
||||
picked.picked.push("b".into());
|
||||
refresh_draft(&mut picked, &["a".to_string()], &["c".to_string()]);
|
||||
assert_eq!(picked.picked, ["a", "b"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,33 @@ fn xfa_enter_and_exit_scripts_run_whenever_the_focus_moves() {
|
||||
assert!(h.query_by_label_contains("left quiet").is_some(), "console: {:?}", h.state().js_console.log);
|
||||
}
|
||||
|
||||
/// An enter script that changes the field it enters (clearing a "Type here") shows in the editor,
|
||||
/// so leaving the field keeps the script's value rather than writing the old one back; typing
|
||||
/// after it is kept as usual.
|
||||
#[test]
|
||||
fn an_enter_script_that_changes_its_field_shows_in_the_editor() {
|
||||
use egui::{Key, Modifiers};
|
||||
let mut h = open(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::focus_template()));
|
||||
wait_for_pages(&mut h);
|
||||
let value = |h: &Harness<'static, PdfCraftApp>| {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[0].id).unwrap().form.iter().find(|f| f.name == "hint").and_then(|f| f.value.first().cloned()).unwrap_or_default()
|
||||
};
|
||||
assert_eq!(value(&h), "Type here");
|
||||
click_field(&mut h, "hint");
|
||||
assert_eq!(value(&h), "", "the enter script cleared it");
|
||||
assert_eq!(h.state().views[0].forms.focus.as_ref().map(|f| f.text.clone()).as_deref(), Some(""), "and the editor shows that");
|
||||
h.key_press_modifiers(Modifiers::NONE, Key::Tab);
|
||||
h.run_steps(4);
|
||||
assert_eq!(value(&h), "", "leaving it doesn't write \"Type here\" back");
|
||||
click_field(&mut h, "hint");
|
||||
h.event(egui::Event::Text("Ann".into()));
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(Modifiers::NONE, Key::Tab);
|
||||
h.run_steps(4);
|
||||
assert_eq!(value(&h), "Ann");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_from_scripts_run_on_open_show_right_away() {
|
||||
let tpl = pdfcraft_xfa::fixtures::scripted_template()
|
||||
|
||||
@@ -662,7 +662,7 @@ if (this.rawValue == "1") {
|
||||
/// A form whose text field `probe` counts its enters in `enters` and its exits in `exits`, whose
|
||||
/// text field `quiet` only shows a message when it is left, and whose radio button `size.large`
|
||||
/// counts its enters in `picks` and its exits in `drops` (each button of a radio group is a field
|
||||
/// with scripts of its own).
|
||||
/// with scripts of its own), and whose text field `hint` clears its "Type here" when it gains focus.
|
||||
pub fn focus_template() -> String {
|
||||
let js = "application/x-javascript";
|
||||
let count = |name: &str| format!(r#"<script contentType="{js}">{name}.rawValue = Number({name}.rawValue) + 1;</script>"#);
|
||||
@@ -681,6 +681,7 @@ pub fn focus_template() -> String {
|
||||
<field name="large" w="0.5in" h="0.25in"><ui><checkButton/></ui><items><text>L</text></items><event activity="enter">{}</event><event activity="exit">{}</event></field>
|
||||
</exclGroup>
|
||||
{}{}{}{}
|
||||
<field name="hint" w="2in" h="0.3in"><ui><textEdit/></ui><value><text>Type here</text></value><event activity="enter"><script contentType="{js}">if (this.rawValue == "Type here") this.rawValue = "";</script></event></field>
|
||||
</subform>
|
||||
</template>
|
||||
</xdp:xdp>
|
||||
|
||||
Reference in New Issue
Block a user