use crate::error::PdfError;
use crate::forms::calculations::FieldValue;
use crate::objects::{Dictionary, Object};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Default)]
pub struct FieldActionSystem {
actions: HashMap<String, FieldActions>,
event_history: Vec<ActionEvent>,
focused_field: Option<String>,
handlers: ActionHandlers,
settings: ActionSettings,
}
#[derive(Debug, Clone, Default)]
pub struct FieldActions {
pub on_focus: Option<FieldAction>,
pub on_blur: Option<FieldAction>,
pub on_format: Option<FieldAction>,
pub on_keystroke: Option<FieldAction>,
pub on_calculate: Option<FieldAction>,
pub on_validate: Option<FieldAction>,
pub on_mouse_enter: Option<FieldAction>,
pub on_mouse_exit: Option<FieldAction>,
pub on_mouse_down: Option<FieldAction>,
pub on_mouse_up: Option<FieldAction>,
}
#[derive(Debug, Clone)]
pub enum FieldAction {
JavaScript { script: String, async_exec: bool },
Format { format_type: FormatActionType },
Validate { validation_type: ValidateActionType },
Calculate { expression: String },
SubmitForm {
url: String,
fields: Vec<String>,
include_empty: bool,
},
ResetForm { fields: Vec<String>, exclude: bool },
ImportData { file_path: String },
SetField {
target_field: String,
value: FieldValue,
},
ShowHide { fields: Vec<String>, show: bool },
PlaySound { sound_name: String, volume: f32 },
Custom {
action_type: String,
parameters: HashMap<String, String>,
},
}
#[derive(Debug, Clone)]
pub enum FormatActionType {
Number {
decimals: usize,
currency: Option<String>,
},
Percent { decimals: usize },
Date { format: String },
Time { format: String },
Special { format: SpecialFormatType },
Custom { script: String },
}
#[derive(Debug, Clone, Copy)]
pub enum SpecialFormatType {
ZipCode,
ZipPlus4,
Phone,
SSN,
}
#[derive(Debug, Clone)]
pub enum ValidateActionType {
Range { min: Option<f64>, max: Option<f64> },
Custom { script: String },
}
#[derive(Debug, Clone)]
pub struct ActionEvent {
pub timestamp: DateTime<Utc>,
pub field_name: String,
pub event_type: ActionEventType,
pub action: Option<FieldAction>,
pub result: ActionResult,
pub data: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ActionEventType {
Focus,
Blur,
Format,
Keystroke,
Calculate,
Validate,
MouseEnter,
MouseExit,
MouseDown,
MouseUp,
}
#[derive(Debug, Clone)]
pub enum ActionResult {
Success,
Failed(String),
Cancelled,
Modified(FieldValue),
}
pub type JsExecutor = fn(&str) -> Result<String, String>;
pub type FormSubmitter = fn(&str, &[String]) -> Result<(), String>;
pub type SoundPlayer = fn(&str, f32) -> Result<(), String>;
pub type CustomHandler = fn(&str, &HashMap<String, String>) -> Result<(), String>;
#[derive(Clone, Default)]
pub struct ActionHandlers {
pub js_executor: Option<JsExecutor>,
pub form_submitter: Option<FormSubmitter>,
pub sound_player: Option<SoundPlayer>,
pub custom_handler: Option<CustomHandler>,
}
impl fmt::Debug for ActionHandlers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ActionHandlers")
.field("js_executor", &self.js_executor.is_some())
.field("form_submitter", &self.form_submitter.is_some())
.field("sound_player", &self.sound_player.is_some())
.field("custom_handler", &self.custom_handler.is_some())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ActionSettings {
pub enable_javascript: bool,
pub enable_form_submit: bool,
pub enable_sound: bool,
pub log_events: bool,
pub max_event_history: usize,
}
impl Default for ActionSettings {
fn default() -> Self {
Self {
enable_javascript: true,
enable_form_submit: false,
enable_sound: true,
log_events: true,
max_event_history: 1000,
}
}
}
impl FieldActionSystem {
pub fn new() -> Self {
Self::default()
}
pub fn with_settings(settings: ActionSettings) -> Self {
Self {
settings,
..Self::default()
}
}
pub fn register_field_actions(&mut self, field_name: impl Into<String>, actions: FieldActions) {
self.actions.insert(field_name.into(), actions);
}
pub fn handle_focus(&mut self, field_name: impl Into<String>) -> Result<(), PdfError> {
let field_name = field_name.into();
if let Some(prev_field) = self.focused_field.clone() {
if prev_field != field_name {
self.handle_blur(prev_field)?;
}
}
self.focused_field = Some(field_name.clone());
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_focus.clone())
{
self.execute_action(&field_name, ActionEventType::Focus, &action)?;
}
Ok(())
}
pub fn handle_blur(&mut self, field_name: impl Into<String>) -> Result<(), PdfError> {
let field_name = field_name.into();
if self.focused_field.as_ref() == Some(&field_name) {
self.focused_field = None;
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_blur.clone())
{
self.execute_action(&field_name, ActionEventType::Blur, &action)?;
}
}
Ok(())
}
pub fn handle_format(
&mut self,
field_name: impl Into<String>,
value: &mut FieldValue,
) -> Result<(), PdfError> {
let field_name = field_name.into();
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_format.clone())
{
let result = self.execute_action(&field_name, ActionEventType::Format, &action)?;
if let ActionResult::Modified(new_value) = result {
*value = new_value;
}
}
Ok(())
}
pub fn handle_keystroke(
&mut self,
field_name: impl Into<String>,
_key: char,
_current_value: &str,
) -> Result<bool, PdfError> {
let field_name = field_name.into();
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_keystroke.clone())
{
let result = self.execute_action(&field_name, ActionEventType::Keystroke, &action)?;
return match result {
ActionResult::Success => Ok(true),
ActionResult::Cancelled => Ok(false),
_ => Ok(true),
};
}
Ok(true)
}
pub fn handle_validate(
&mut self,
field_name: impl Into<String>,
_value: &FieldValue,
) -> Result<bool, PdfError> {
let field_name = field_name.into();
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_validate.clone())
{
let result = self.execute_action(&field_name, ActionEventType::Validate, &action)?;
return match result {
ActionResult::Success => Ok(true),
ActionResult::Failed(_) => Ok(false),
_ => Ok(true),
};
}
Ok(true)
}
pub fn handle_calculate(
&mut self,
field_name: impl Into<String>,
value: &mut FieldValue,
) -> Result<(), PdfError> {
let field_name = field_name.into();
if let Some(action) = self
.actions
.get(&field_name)
.and_then(|a| a.on_calculate.clone())
{
let result = self.execute_action(&field_name, ActionEventType::Calculate, &action)?;
if let ActionResult::Modified(new_value) = result {
*value = new_value;
}
}
Ok(())
}
fn execute_action(
&mut self,
field_name: &str,
event_type: ActionEventType,
action: &FieldAction,
) -> Result<ActionResult, PdfError> {
let result = match action {
FieldAction::JavaScript { script, async_exec } => {
self.execute_javascript(script, *async_exec)
}
FieldAction::Format { format_type } => self.execute_format(format_type),
FieldAction::Validate { validation_type } => self.execute_validate(validation_type),
FieldAction::Calculate { expression } => self.execute_calculate(expression),
FieldAction::ShowHide { fields, show } => self.execute_show_hide(fields, *show),
FieldAction::SetField {
target_field,
value,
} => self.execute_set_field(target_field, value),
_ => Ok(ActionResult::Success),
};
if self.settings.log_events {
let result_for_log = result
.as_ref()
.map(|r| r.clone())
.unwrap_or_else(|e| ActionResult::Failed(e.to_string()));
self.log_event(field_name, event_type, Some(action.clone()), result_for_log);
}
result
}
fn execute_javascript(
&self,
script: &str,
_async_exec: bool,
) -> Result<ActionResult, PdfError> {
if !self.settings.enable_javascript {
return Ok(ActionResult::Cancelled);
}
if let Some(executor) = self.handlers.js_executor {
match executor(script) {
Ok(_result) => Ok(ActionResult::Success),
Err(e) => Ok(ActionResult::Failed(e)),
}
} else {
Ok(ActionResult::Success)
}
}
fn execute_format(&self, _format_type: &FormatActionType) -> Result<ActionResult, PdfError> {
Ok(ActionResult::Success)
}
fn execute_validate(
&self,
validation_type: &ValidateActionType,
) -> Result<ActionResult, PdfError> {
match validation_type {
ValidateActionType::Range { min: _, max: _ } => {
Ok(ActionResult::Success)
}
ValidateActionType::Custom { script } => self.execute_javascript(script, false),
}
}
fn execute_calculate(&self, _expression: &str) -> Result<ActionResult, PdfError> {
Ok(ActionResult::Success)
}
fn execute_show_hide(&self, _fields: &[String], _show: bool) -> Result<ActionResult, PdfError> {
Ok(ActionResult::Success)
}
fn execute_set_field(
&self,
_target_field: &str,
value: &FieldValue,
) -> Result<ActionResult, PdfError> {
Ok(ActionResult::Modified(value.clone()))
}
fn log_event(
&mut self,
field_name: &str,
event_type: ActionEventType,
action: Option<FieldAction>,
result: ActionResult,
) {
let event = ActionEvent {
timestamp: Utc::now(),
field_name: field_name.to_string(),
event_type,
action,
result,
data: HashMap::new(),
};
self.event_history.push(event);
if self.event_history.len() > self.settings.max_event_history {
self.event_history.remove(0);
}
}
pub fn get_focused_field(&self) -> Option<&String> {
self.focused_field.as_ref()
}
pub fn get_event_history(&self) -> &[ActionEvent] {
&self.event_history
}
pub fn clear_event_history(&mut self) {
self.event_history.clear();
}
pub fn set_js_executor(&mut self, executor: fn(&str) -> Result<String, String>) {
self.handlers.js_executor = Some(executor);
}
pub fn to_pdf_dict(&self, field_name: &str) -> Dictionary {
let mut dict = Dictionary::new();
if let Some(actions) = self.actions.get(field_name) {
let mut aa_dict = Dictionary::new();
if let Some(action) = &actions.on_focus {
aa_dict.set("Fo", self.action_to_object(action));
}
if let Some(action) = &actions.on_blur {
aa_dict.set("Bl", self.action_to_object(action));
}
if let Some(action) = &actions.on_format {
aa_dict.set("F", self.action_to_object(action));
}
if let Some(action) = &actions.on_keystroke {
aa_dict.set("K", self.action_to_object(action));
}
if let Some(action) = &actions.on_calculate {
aa_dict.set("C", self.action_to_object(action));
}
if let Some(action) = &actions.on_validate {
aa_dict.set("V", self.action_to_object(action));
}
if !aa_dict.is_empty() {
dict.set("AA", Object::Dictionary(aa_dict));
}
}
dict
}
fn action_to_object(&self, action: &FieldAction) -> Object {
let mut dict = Dictionary::new();
match action {
FieldAction::JavaScript { script, .. } => {
dict.set("S", Object::Name("JavaScript".to_string()));
dict.set("JS", Object::String(script.clone()));
}
FieldAction::SubmitForm { url, .. } => {
dict.set("S", Object::Name("SubmitForm".to_string()));
dict.set("F", Object::String(url.clone()));
}
FieldAction::ResetForm { .. } => {
dict.set("S", Object::Name("ResetForm".to_string()));
}
_ => {
dict.set("S", Object::Name("Unknown".to_string()));
}
}
Object::Dictionary(dict)
}
}
impl fmt::Display for ActionEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] Field '{}': {:?} -> {:?}",
self.timestamp.format("%H:%M:%S"),
self.field_name,
self.event_type,
self.result
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_focus_blur_events() {
let mut system = FieldActionSystem::new();
let actions = FieldActions {
on_focus: Some(FieldAction::JavaScript {
script: "console.log('Field focused');".to_string(),
async_exec: false,
}),
on_blur: Some(FieldAction::JavaScript {
script: "console.log('Field blurred');".to_string(),
async_exec: false,
}),
..Default::default()
};
system.register_field_actions("test_field", actions);
system.handle_focus("test_field").unwrap();
assert_eq!(system.get_focused_field(), Some(&"test_field".to_string()));
system.handle_blur("test_field").unwrap();
assert_eq!(system.get_focused_field(), None);
}
#[test]
fn test_field_switching() {
let mut system = FieldActionSystem::new();
for field in ["field1", "field2"] {
let actions = FieldActions {
on_focus: Some(FieldAction::SetField {
target_field: "status".to_string(),
value: FieldValue::Text(format!("{} focused", field)),
}),
on_blur: Some(FieldAction::SetField {
target_field: "status".to_string(),
value: FieldValue::Text(format!("{} blurred", field)),
}),
..Default::default()
};
system.register_field_actions(field, actions);
}
system.handle_focus("field1").unwrap();
assert_eq!(system.get_focused_field(), Some(&"field1".to_string()));
system.handle_focus("field2").unwrap();
assert_eq!(system.get_focused_field(), Some(&"field2".to_string()));
}
#[test]
fn test_validate_action() {
let mut system = FieldActionSystem::new();
let actions = FieldActions {
on_validate: Some(FieldAction::Validate {
validation_type: ValidateActionType::Range {
min: Some(0.0),
max: Some(100.0),
},
}),
..Default::default()
};
system.register_field_actions("score", actions);
let valid = system
.handle_validate("score", &FieldValue::Number(50.0))
.unwrap();
assert!(valid);
}
#[test]
fn test_event_history() {
let mut system = FieldActionSystem::new();
let actions = FieldActions {
on_focus: Some(FieldAction::ShowHide {
fields: vec!["help_text".to_string()],
show: true,
}),
on_blur: Some(FieldAction::ShowHide {
fields: vec!["help_text".to_string()],
show: false,
}),
..Default::default()
};
system.register_field_actions("field1", actions);
system.handle_focus("field1").unwrap();
system.handle_blur("field1").unwrap();
assert_eq!(system.get_event_history().len(), 2);
assert_eq!(
system.get_event_history()[0].event_type,
ActionEventType::Focus
);
assert_eq!(
system.get_event_history()[1].event_type,
ActionEventType::Blur
);
}
#[test]
fn test_format_action_types() {
let number_format = FormatActionType::Number {
decimals: 2,
currency: Some("USD".to_string()),
};
match number_format {
FormatActionType::Number { decimals, currency } => {
assert_eq!(decimals, 2);
assert_eq!(currency, Some("USD".to_string()));
}
_ => panic!("Expected Number format"),
}
let percent_format = FormatActionType::Percent { decimals: 1 };
match percent_format {
FormatActionType::Percent { decimals } => assert_eq!(decimals, 1),
_ => panic!("Expected Percent format"),
}
let date_format = FormatActionType::Date {
format: "mm/dd/yyyy".to_string(),
};
match date_format {
FormatActionType::Date { format } => assert_eq!(format, "mm/dd/yyyy"),
_ => panic!("Expected Date format"),
}
let special_format = FormatActionType::Special {
format: SpecialFormatType::ZipCode,
};
match special_format {
FormatActionType::Special { format } => {
matches!(format, SpecialFormatType::ZipCode);
}
_ => panic!("Expected Special format"),
}
}
#[test]
fn test_validate_action_types() {
let range_validate = ValidateActionType::Range {
min: Some(0.0),
max: Some(100.0),
};
match range_validate {
ValidateActionType::Range { min, max } => {
assert_eq!(min, Some(0.0));
assert_eq!(max, Some(100.0));
}
_ => panic!("Expected Range validation"),
}
let custom_validate = ValidateActionType::Custom {
script: "return value > 0;".to_string(),
};
match custom_validate {
ValidateActionType::Custom { script } => {
assert!(script.contains("return"));
}
_ => panic!("Expected Custom validation"),
}
}
#[test]
fn test_action_result() {
let success_result = ActionResult::Success;
matches!(success_result, ActionResult::Success);
let failed_result = ActionResult::Failed("Test error".to_string());
match failed_result {
ActionResult::Failed(msg) => assert_eq!(msg, "Test error"),
_ => panic!("Expected Failed result"),
}
let cancelled_result = ActionResult::Cancelled;
matches!(cancelled_result, ActionResult::Cancelled);
let modified_result = ActionResult::Modified(FieldValue::Text("Modified".to_string()));
match modified_result {
ActionResult::Modified(value) => {
assert_eq!(value, FieldValue::Text("Modified".to_string()));
}
_ => panic!("Expected Modified result"),
}
}
#[test]
fn test_action_settings() {
let settings = ActionSettings::default();
assert!(settings.enable_javascript);
assert!(!settings.enable_form_submit); assert!(settings.enable_sound);
assert!(settings.log_events);
assert_eq!(settings.max_event_history, 1000);
let custom_settings = ActionSettings {
enable_javascript: false,
enable_form_submit: true,
enable_sound: false,
log_events: false,
max_event_history: 500,
};
assert!(!custom_settings.enable_javascript);
assert!(custom_settings.enable_form_submit);
assert!(!custom_settings.enable_sound);
assert!(!custom_settings.log_events);
assert_eq!(custom_settings.max_event_history, 500);
}
#[test]
fn test_field_action_system_settings() {
let system = FieldActionSystem::new();
assert!(system.settings.enable_javascript);
assert!(!system.settings.enable_form_submit);
assert!(system.settings.enable_sound);
assert!(system.settings.log_events);
assert_eq!(system.settings.max_event_history, 1000);
let custom_settings = ActionSettings {
enable_javascript: false,
enable_form_submit: true,
enable_sound: false,
log_events: false,
max_event_history: 100,
};
let system_with_settings = FieldActionSystem::with_settings(custom_settings);
assert!(!system_with_settings.settings.enable_javascript);
assert!(system_with_settings.settings.enable_form_submit);
}
#[test]
fn test_clear_event_history() {
let mut system = FieldActionSystem::new();
let actions = FieldActions {
on_focus: Some(FieldAction::Custom {
action_type: "test".to_string(),
parameters: HashMap::new(),
}),
..Default::default()
};
system.register_field_actions("field1", actions);
system.handle_focus("field1").unwrap();
assert!(system.get_event_history().len() > 0);
system.clear_event_history();
assert_eq!(system.get_event_history().len(), 0);
}
#[test]
fn test_mouse_actions() {
let mut system = FieldActionSystem::new();
let actions = FieldActions {
on_mouse_enter: Some(FieldAction::Custom {
action_type: "highlight".to_string(),
parameters: HashMap::from([("color".to_string(), "yellow".to_string())]),
}),
on_mouse_exit: Some(FieldAction::Custom {
action_type: "unhighlight".to_string(),
parameters: HashMap::new(),
}),
on_mouse_down: Some(FieldAction::Custom {
action_type: "pressed".to_string(),
parameters: HashMap::new(),
}),
on_mouse_up: Some(FieldAction::Custom {
action_type: "released".to_string(),
parameters: HashMap::new(),
}),
..Default::default()
};
system.register_field_actions("button1", actions);
assert!(system.actions.contains_key("button1"));
let registered_actions = &system.actions["button1"];
assert!(registered_actions.on_mouse_enter.is_some());
assert!(registered_actions.on_mouse_exit.is_some());
assert!(registered_actions.on_mouse_down.is_some());
assert!(registered_actions.on_mouse_up.is_some());
}
#[test]
fn test_submit_form_action() {
let action = FieldAction::SubmitForm {
url: "https://example.com/submit".to_string(),
fields: vec!["name".to_string(), "email".to_string()],
include_empty: false,
};
match action {
FieldAction::SubmitForm {
url,
fields,
include_empty,
} => {
assert_eq!(url, "https://example.com/submit");
assert_eq!(fields.len(), 2);
assert!(!include_empty);
}
_ => panic!("Expected SubmitForm action"),
}
}
#[test]
fn test_reset_form_action() {
let action = FieldAction::ResetForm {
fields: vec!["field1".to_string(), "field2".to_string()],
exclude: true,
};
match action {
FieldAction::ResetForm { fields, exclude } => {
assert_eq!(fields.len(), 2);
assert!(exclude);
}
_ => panic!("Expected ResetForm action"),
}
}
#[test]
fn test_field_value_action() {
let action = FieldAction::SetField {
target_field: "total".to_string(),
value: FieldValue::Number(100.0),
};
match action {
FieldAction::SetField {
target_field,
value,
} => {
assert_eq!(target_field, "total");
assert_eq!(value, FieldValue::Number(100.0));
}
_ => panic!("Expected SetField action"),
}
}
#[test]
fn test_action_event_types() {
let focus = ActionEventType::Focus;
let blur = ActionEventType::Blur;
let format = ActionEventType::Format;
let keystroke = ActionEventType::Keystroke;
let calculate = ActionEventType::Calculate;
let validate = ActionEventType::Validate;
assert_eq!(focus, ActionEventType::Focus);
assert_eq!(blur, ActionEventType::Blur);
assert_eq!(format, ActionEventType::Format);
assert_eq!(keystroke, ActionEventType::Keystroke);
assert_eq!(calculate, ActionEventType::Calculate);
assert_eq!(validate, ActionEventType::Validate);
assert_ne!(focus, blur);
}
#[test]
fn test_special_format_types() {
let zip = SpecialFormatType::ZipCode;
let zip_plus = SpecialFormatType::ZipPlus4;
let phone = SpecialFormatType::Phone;
let ssn = SpecialFormatType::SSN;
matches!(zip, SpecialFormatType::ZipCode);
matches!(zip_plus, SpecialFormatType::ZipPlus4);
matches!(phone, SpecialFormatType::Phone);
matches!(ssn, SpecialFormatType::SSN);
}
#[test]
fn test_play_sound_action() {
let action = FieldAction::PlaySound {
sound_name: "beep".to_string(),
volume: 0.5,
};
match action {
FieldAction::PlaySound { sound_name, volume } => {
assert_eq!(sound_name, "beep");
assert_eq!(volume, 0.5);
}
_ => panic!("Expected PlaySound action"),
}
}
#[test]
fn test_import_data_action() {
let action = FieldAction::ImportData {
file_path: "/path/to/data.fdf".to_string(),
};
match action {
FieldAction::ImportData { file_path } => {
assert_eq!(file_path, "/path/to/data.fdf");
}
_ => panic!("Expected ImportData action"),
}
}
#[test]
fn test_show_hide_action() {
let action = FieldAction::ShowHide {
fields: vec!["field1".to_string(), "field2".to_string()],
show: true,
};
match action {
FieldAction::ShowHide { fields, show } => {
assert_eq!(fields.len(), 2);
assert!(show);
}
_ => panic!("Expected ShowHide action"),
}
}
#[test]
fn test_custom_action() {
let mut params = HashMap::new();
params.insert("key1".to_string(), "value1".to_string());
params.insert("key2".to_string(), "value2".to_string());
let action = FieldAction::Custom {
action_type: "custom_type".to_string(),
parameters: params.clone(),
};
match action {
FieldAction::Custom {
action_type,
parameters,
} => {
assert_eq!(action_type, "custom_type");
assert_eq!(parameters.len(), 2);
assert_eq!(parameters.get("key1"), Some(&"value1".to_string()));
}
_ => panic!("Expected Custom action"),
}
}
}