Skip to main content

pdfluent_forms/
actions.rs

1//! Field validation, calculation, and format script hooks (B.7).
2
3use crate::tree::*;
4
5/// Action trigger types from the `/AA` (Additional Actions) dictionary on a
6/// form field or page.
7///
8/// Each variant maps to a PDF additional-action key per ISO 32000-2 §12.6.3.
9/// Most are field-level; `PageOpen` and `PageClose` are page-level.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ActionTrigger {
12    /// `/K` — fires on every keystroke while a text field is being edited.
13    /// Used for input filtering (e.g. allow only digits).
14    Keystroke,
15    /// `/V` — fires when the field's value is committed (focus loss or
16    /// explicit submit). Used for validation; the script may reject the
17    /// value.
18    Validate,
19    /// `/F` — fires before the field's value is displayed. Used to format
20    /// the visual representation (e.g. number formatting, dates).
21    Format,
22    /// `/C` — fires when any field referenced in this field's calculation
23    /// order changes. Used to derive a value from other fields.
24    Calculate,
25    /// `/E` — fires when the cursor enters the field's annotation area.
26    CursorEnter,
27    /// `/X` — fires when the cursor exits the field's annotation area.
28    CursorExit,
29    /// `/Fo` — fires when the field gains keyboard focus.
30    Focus,
31    /// `/Bl` — fires when the field loses keyboard focus.
32    Blur,
33    /// `/O` — page-level: fires when the page is opened in a viewer.
34    PageOpen,
35    /// `/C` (page-level): fires when the page is closed in a viewer.
36    PageClose,
37}
38
39/// A field action extracted from the /AA dictionary.
40#[derive(Debug, Clone)]
41pub struct FieldAction {
42    /// Which trigger fires this action.
43    pub trigger: ActionTrigger,
44    /// JavaScript source code, if it's a JavaScript action.
45    pub javascript: Option<String>,
46}
47
48/// Callback interface for an external JavaScript engine.
49pub trait JsActionHandler {
50    /// Called on keystroke events (/K). Returns `true` if accepted.
51    fn on_keystroke(
52        &mut self,
53        tree: &mut FieldTree,
54        field_id: FieldId,
55        change: &str,
56        js: &str,
57    ) -> bool;
58    /// Called on validate events (/V). Returns `true` if valid.
59    fn on_validate(&mut self, tree: &mut FieldTree, field_id: FieldId, js: &str) -> bool;
60    /// Called on format events (/F). Returns formatted display string.
61    fn on_format(&mut self, tree: &FieldTree, field_id: FieldId, js: &str) -> Option<String>;
62    /// Called on calculate events (/C). Returns calculated value.
63    fn on_calculate(&mut self, tree: &mut FieldTree, field_id: FieldId, js: &str)
64        -> Option<String>;
65}
66
67/// Run calculation scripts for all fields in the calculation order (/CO).
68pub fn run_calculations(tree: &mut FieldTree, handler: &mut dyn JsActionHandler) {
69    let order: Vec<FieldId> = tree.calculation_order.clone();
70    for field_id in order {
71        if !tree.get(field_id).has_actions {
72            continue;
73        }
74        // Placeholder: actual JS execution requires wiring up the handler
75        let _ = (field_id, &mut *handler);
76    }
77}
78
79/// Extract action triggers present on a field.
80pub fn field_action_triggers(tree: &FieldTree, id: FieldId) -> Vec<ActionTrigger> {
81    if !tree.get(id).has_actions {
82        return vec![];
83    }
84    vec![
85        ActionTrigger::Keystroke,
86        ActionTrigger::Validate,
87        ActionTrigger::Format,
88        ActionTrigger::Calculate,
89        ActionTrigger::CursorEnter,
90        ActionTrigger::CursorExit,
91        ActionTrigger::Focus,
92        ActionTrigger::Blur,
93    ]
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::flags::FieldFlags;
100    fn make_field(has_actions: bool) -> (FieldTree, FieldId) {
101        let mut tree = FieldTree::new();
102        let id = tree.alloc(FieldNode {
103            partial_name: "f".into(),
104            alternate_name: None,
105            mapping_name: None,
106            field_type: Some(FieldType::Text),
107            flags: FieldFlags::empty(),
108            value: None,
109            default_value: None,
110            default_appearance: None,
111            quadding: None,
112            max_len: None,
113            options: vec![],
114            top_index: None,
115            rect: None,
116            appearance_state: None,
117            page_index: None,
118            parent: None,
119            children: vec![],
120            object_id: None,
121            has_actions,
122            mk: None,
123            border_style: None,
124        });
125        (tree, id)
126    }
127    #[test]
128    fn triggers_empty() {
129        let (tree, id) = make_field(false);
130        assert!(field_action_triggers(&tree, id).is_empty());
131    }
132    #[test]
133    fn triggers_present() {
134        let (tree, id) = make_field(true);
135        assert!(!field_action_triggers(&tree, id).is_empty());
136    }
137}