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