mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 23:32:35 +00:00
* feat(js, forms): field setItems, setAction and a bounded toSource (#269) Ported from #344 by @DWAA1660 onto the per-document script session. - `field.setItems(items)` replaces a combo or list box's items (text, or [text, export] pairs). The value stays when an item still has it, and otherwise becomes the first item's. Written as the field's /Opt (pairs only where the export value differs), /I dropped, field redrawn. - `field.setAction(trigger, script)` installs a JavaScript action for one trigger (MouseUp/Down/Enter/Exit, OnFocus/OnBlur, Keystroke, Validate, Calculate, Format), replacing only that trigger's action. - `Object.prototype.toSource()` writes plain data (strings, numbers, booleans, null, undefined, arrays, objects) that eval reads back. It refuses functions, cycles, more than 64 levels, 10,000 values or 1 MiB with an error the script can catch. Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * feat(engine): run a form's open, Will Save and Will Print actions and its widgets' pointer and focus scripts (#269) Ported from #344 by @DWAA1660 onto the per-document script session: each runs in the document's retained realm, so state the document scripts and earlier events left is kept. - The catalog /OpenAction's JavaScript (and its /Next chain, bounded and cycle-safe; other action types skipped) runs once on opening, after the document scripts, for background opens and reloads too, and again on Revert in a fresh realm. Event Doc/Open. - `Session::run_document_actions` runs /AA /WS (Will Save) or /AA /WP (Will Print). A failing action is reported and never stops the save or print. doc_save and doc_print run them first and report what they said (`will_save`, `will_print`). - Scripts that run unasked (open, Will Save, Will Print, pointer over, focus) may only ask for highlight, zoom, focus or a page; a click's Mouse Down / Mouse Up keep their requests. - `Session::field_trigger` runs a widget's Mouse Enter, Mouse Exit, Mouse Down, Mouse Up, Focus or Blur JavaScript (one undoable edit), reading only that widget's dictionary. `field_focus` now runs an AcroForm widget's On Focus / On Blur as well as XFA enter/exit. - New tool `js_event`; form_focus's description says AcroForm focus scripts run too. Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * feat(ui): Will Save before saving, Will Print before printing, and widget pointer scripts in the app (#269, #247) Ported from #344 by @DWAA1660. - Save (both paths) runs the document's Will Save actions first, and Print its Will Print actions; what they change is saved or printed, and what they say is shown. - The pointer moving onto or off a form widget queues its Mouse Enter / Mouse Exit script, a press its Mouse Down. A click on a check box or radio button queues its Mouse Up after the toggle, so the script reads the value the click stored (#247). The queue runs after the frame's edit. Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * docs(parity): document actions and widget triggers are partly supported (#269) Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * test: check toSource's text (scripts can't eval it) and the trigger event names (#269) Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * test: a check box's Mouse Up reads its new value; keep the fixtures on screen and the realm across an in-place save (#247, #269) Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * fix(engine, forms, js, ui): review fixes for document actions and field triggers - widget trigger actions follow /Next, each action its own edit; errors don't stop the chain - setAction installs on the field the object stands for when names repeat - toSource refuses non-plain objects and a __proto__ key - doc_print checks the destination before Will Print; js_event returns only its own output - combo/list clicks run Mouse Up; a refused click's Mouse Up doesn't run - label crops are re-read when Will Print changed the document Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * forms: run a pointer trigger on the field it hit and refresh the edited field's draft A trigger now carries the field's object, so of fields sharing a name the one under the pointer runs its own script (Session::field_trigger_target); js_event passes the field it resolved. A trigger script changing the field being edited shows in its editor unless the user already typed there, as focus scripts do. Co-authored-by: DWAA1660 <dwatnip123@gmail.com> * forms: carry the field's identity through focus scripts and js_event Focus transitions now track the field object, so moving between fields sharing a name is a move and the field that gained or lost focus runs its own script (Session::field_focus_target). js_event takes an optional field_id from form_fields to pick one of fields sharing a name. Co-authored-by: DWAA1660 <dwatnip123@gmail.com> --------- Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
486 lines
25 KiB
Rust
486 lines
25 KiB
Rust
use super::host::{FieldKey, arg, colour, error, function, host, read, s, set_value, strings, text, text_line, value_of, write};
|
|
use super::model::{DISPLAY_HIDDEN, DISPLAY_VISIBLE, FieldType, Request};
|
|
use super::{Attribute, Context, JsArray, JsObject, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, js_string};
|
|
|
|
macro_rules! getter {
|
|
($ctx:expr, $name:expr, |$f:ident, $c:ident| $body:expr) => {{
|
|
fn get(_: &JsValue, _: &[JsValue], name: &FieldKey, $c: &mut Context) -> JsResult<JsValue> {
|
|
let $f = read($c, name)?;
|
|
Ok($body)
|
|
}
|
|
function($ctx, NativeFunction::from_copy_closure_with_captures(get, $name.clone()))
|
|
}};
|
|
}
|
|
|
|
/// Resolve public name lookups to their first matching object, preserving ordinary getField behavior.
|
|
pub(crate) fn field_object(ctx: &mut Context, name: &str) -> JsObject {
|
|
let id = host(ctx).ok().and_then(|h| h.borrow().fields.iter().find(|f| f.name == name).and_then(|f| f.object_id));
|
|
field_object_by_id(ctx, name, id)
|
|
}
|
|
/// Cache by object identity as well as name so duplicate-named targets remain distinct.
|
|
pub(crate) fn field_object_by_id(ctx: &mut Context, name: &str, id: Option<u64>) -> JsObject {
|
|
let token = host(ctx)
|
|
.ok()
|
|
.and_then(|h| h.borrow().fields.iter().find(|f| f.object_id == id && (id.is_some() || f.name == name)).map(|f| f.instance_id))
|
|
.unwrap_or(0);
|
|
let key = crate::host::field_cache_key(name, id, token);
|
|
if let Some(object) = host(ctx).ok().and_then(|h| h.borrow().objects.get(&key).cloned()) {
|
|
return object;
|
|
}
|
|
let object = field_object_new(ctx, name, id, token);
|
|
if let Ok(h) = host(ctx)
|
|
&& h.borrow().field_keys.contains(&key)
|
|
{
|
|
h.borrow_mut().objects.insert(key, object.clone());
|
|
}
|
|
object
|
|
}
|
|
/// Construct live accessors whose identity survives host snapshot refreshes.
|
|
fn field_object_new(ctx: &mut Context, name: &str, id: Option<u64>, token: u64) -> JsObject {
|
|
let n = (JsString::from(name), id, token);
|
|
let value_get = getter!(ctx, n, |f, c| value_of(&f, c));
|
|
let value_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let vals = strings(&arg(args, 0), c)?;
|
|
write(c, name, |f| set_value(f, vals))?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let as_string = getter!(ctx, n, |f, _c| s(&match f.kind {
|
|
FieldType::CheckBox | FieldType::RadioButton => f.value.first().cloned().unwrap_or_else(|| "Off".into()),
|
|
_ => f.value.join(","),
|
|
}));
|
|
let default = getter!(ctx, n, |f, _c| s(&f.default.first().cloned().unwrap_or_default()));
|
|
let kind = getter!(ctx, n, |f, _c| s(f.kind.name()));
|
|
let fname = getter!(ctx, n, |f, _c| s(&f.name));
|
|
let readonly_get = getter!(ctx, n, |f, _c| JsValue::from(f.readonly));
|
|
let readonly_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let on = arg(args, 0).to_boolean();
|
|
write(c, name, |f| f.readonly = on)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let required_get = getter!(ctx, n, |f, _c| JsValue::from(f.required));
|
|
let required_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let on = arg(args, 0).to_boolean();
|
|
write(c, name, |f| f.required = on)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let display_get = getter!(ctx, n, |f, _c| JsValue::from(f.display));
|
|
let display_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let d = arg(args, 0).to_number(c)? as i32;
|
|
write(c, name, |f| f.display = d.clamp(0, 3))?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let hidden_get = getter!(ctx, n, |f, _c| JsValue::from(f.display == DISPLAY_HIDDEN));
|
|
let hidden_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let on = arg(args, 0).to_boolean();
|
|
write(c, name, |f| f.display = if on { DISPLAY_HIDDEN } else { DISPLAY_VISIBLE })?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let num_items = getter!(ctx, n, |f, _c| JsValue::from(f.options.len() as f64));
|
|
let char_limit = getter!(ctx, n, |f, _c| JsValue::from(f.char_limit.unwrap_or(0) as f64));
|
|
let export_values = getter!(ctx, n, |f, c| {
|
|
let vals: Vec<JsValue> = f.options.iter().map(|o| s(&o.0)).collect();
|
|
JsArray::from_iter(vals, c).into()
|
|
});
|
|
let indices = getter!(ctx, n, |f, c| {
|
|
let idx: Vec<JsValue> = f.options.iter().enumerate().filter(|(_, o)| f.value.contains(&o.0)).map(|(i, _)| JsValue::from(i as f64)).collect();
|
|
if idx.len() == 1 { idx[0].clone() } else { JsArray::from_iter(idx, c).into() }
|
|
});
|
|
let text_color_get = getter!(ctx, n, |f, c| colour(f.text_color.as_deref(), c));
|
|
let text_color_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = strings(&arg(args, 0), c)?;
|
|
write(c, name, |f| f.text_color = Some(v.into()))?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let fill_color_get = getter!(ctx, n, |f, c| colour(f.fill_color.as_deref(), c));
|
|
let fill_color_set = {
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = strings(&arg(args, 0), c)?;
|
|
write(c, name, |f| f.fill_color = Some(v.into()))?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let text_size_get = getter!(ctx, n, |f, c| JsValue::from(f.text_size));
|
|
let text_size_set = {
|
|
/// Validate a finite text size in 0..=300 points before staging the field appearance change.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = arg(args, 0).to_number(c)?;
|
|
if !v.is_finite() || !(0.0..=300.0).contains(&v) {
|
|
return Err(error("textSize must be between 0 and 300"));
|
|
}
|
|
write(c, name, |f| f.text_size = v)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let line_width_get = getter!(ctx, n, |f, c| JsValue::from(f.line_width));
|
|
let line_width_set = {
|
|
/// Validate a finite border width in 0..=12 points before staging the appearance change.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = arg(args, 0).to_number(c)?;
|
|
if !v.is_finite() || !(0.0..=12.0).contains(&v) {
|
|
return Err(error("lineWidth must be between 0 and 12"));
|
|
}
|
|
write(c, name, |f| f.line_width = v)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let border_style_get = getter!(ctx, n, |f, c| s(&f.border_style));
|
|
let border_style_set = {
|
|
/// Accept only supported border styles and stage the validated value for PDF writeback.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = text(&arg(args, 0), c)?;
|
|
if !["solid", "dashed", "beveled", "inset", "underline"].contains(&v.as_str()) {
|
|
return Err(error("unsupported borderStyle"));
|
|
}
|
|
write(c, name, |f| f.border_style = v)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let user_name_get = getter!(ctx, n, |f, c| s(&f.user_name));
|
|
let user_name_set = {
|
|
/// Bound the tooltip string to 4,096 bytes before staging the field user name.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let v = text(&arg(args, 0), c)?;
|
|
if v.len() > 4096 {
|
|
return Err(error("userName is too long"));
|
|
}
|
|
write(c, name, |f| f.user_name = v)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let stroke_color_get = getter!(ctx, n, |f, c| colour(f.stroke_color.as_deref(), c));
|
|
let stroke_color_set = {
|
|
/// Read the bounded stroke-colour array for validation at the form writeback boundary.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let colour = strings(&arg(args, 0), c)?;
|
|
write(c, name, |f| f.stroke_color = Some(colour.into()))?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
let rect_get = getter!(ctx, n, |f, c| f.rect.map_or_else(JsValue::undefined, |r| JsArray::from_iter(r.map(JsValue::from), c).into()));
|
|
let rect_set = {
|
|
/// Validate finite ordered rectangle coordinates while permitting zero-sized script overlays.
|
|
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let a = arg(args, 0).as_object().ok_or_else(|| error("rect needs an array"))?;
|
|
let a = JsArray::from_object(a)?;
|
|
if a.length(c)? != 4 {
|
|
return Err(error("rect needs four coordinates"));
|
|
}
|
|
let mut coords = [0.0; 4];
|
|
for (i, v) in coords.iter_mut().enumerate() {
|
|
*v = a.get(i as u32, c)?.to_number(c)?;
|
|
if !v.is_finite() || v.abs() > 1e7 {
|
|
return Err(error("rect coordinates are out of range"));
|
|
}
|
|
}
|
|
if coords[2] < coords[0] || coords[1] < coords[3] {
|
|
return Err(error("rect must use left, top, right, bottom order"));
|
|
}
|
|
let v = Some(coords);
|
|
write(c, name, |f| f.rect = v)?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
|
|
};
|
|
|
|
/// Read the normal caption of the first button widget and refuse unsupported caption faces.
|
|
fn button_get_caption(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
if f.kind != FieldType::Button {
|
|
return Err(error(format!("{} is not a push button", f.name)));
|
|
}
|
|
let face = if args.is_empty() { 0.0 } else { arg(args, 0).to_number(c)? };
|
|
if !face.is_finite() || face != 0.0 {
|
|
return Err(error("only the normal button face (0) is supported"));
|
|
}
|
|
Ok(s(f.caption.as_deref().unwrap_or("")))
|
|
}
|
|
/// Record a normal-caption setter even when widget zero already matches, allowing all-widget writeback.
|
|
fn button_set_caption(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
if f.kind != FieldType::Button {
|
|
return Err(error("buttonSetCaption is only available on push buttons"));
|
|
}
|
|
let caption_arg = args.first().ok_or_else(|| error("buttonSetCaption requires a caption argument"))?;
|
|
let caption = text_line(caption_arg, c)?;
|
|
let face = if args.get(1).is_none() { 0.0 } else { arg(args, 1).to_number(c)? };
|
|
if !face.is_finite() || face != 0.0 {
|
|
return Err(error("only the normal button face (0) is supported"));
|
|
}
|
|
if host(c)?.borrow().event_name == "Format" {
|
|
return Err(error("buttonSetCaption is not available in a Format event"));
|
|
}
|
|
write(c, name, |f| {
|
|
f.caption = Some(caption);
|
|
f.caption_written = true;
|
|
})?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
fn get_item_at(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
let i = arg(args, 0).to_number(c)?;
|
|
// bExportValue defaults to true.
|
|
let export = args.get(1).is_none_or(JsValue::to_boolean);
|
|
let i = if i < 0.0 { f.options.len().saturating_sub(1) } else { i as usize };
|
|
let o = f.options.get(i).ok_or_else(|| error(format!("{} has no item {i}", f.name)))?;
|
|
Ok(s(if export { &o.0 } else { &o.1 }))
|
|
}
|
|
fn is_box_checked(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
let i = arg(args, 0).to_number(c)? as usize;
|
|
let on = f.options.get(i).is_some_and(|o| f.value.first() == Some(&o.0));
|
|
Ok(JsValue::from(on))
|
|
}
|
|
fn check_this_box(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
let i = arg(args, 0).to_number(c)? as usize;
|
|
let on = args.get(1).is_none_or(JsValue::to_boolean);
|
|
let export = f.options.get(i).map(|o| o.0.clone()).ok_or_else(|| error(format!("{} has no widget {i}", f.name)))?;
|
|
write(c, name, |f| {
|
|
if on {
|
|
f.value = vec![export];
|
|
} else if f.value.first() == Some(&export) {
|
|
f.value.clear();
|
|
}
|
|
})?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
fn set_focus(_: &JsValue, _: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let field = read(c, name)?;
|
|
let request = match field.object_id {
|
|
Some(object_id) => Request::FocusField { name: field.name, object_id },
|
|
None => Request::Focus(field.name),
|
|
};
|
|
host(c)?.borrow_mut().request(request);
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
/// `setAction(cTrigger, cScript)`: install `cScript` as the field's JavaScript for
|
|
/// `cTrigger`, replacing what it ran before; saved with the document (#269).
|
|
fn set_action(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let trigger = text(&arg(args, 0), c)?;
|
|
if !SET_ACTION_TRIGGERS.contains(&trigger.as_str()) {
|
|
return Err(error(format!("setAction: unknown trigger {trigger:?}; use one of {}", SET_ACTION_TRIGGERS.join(", "))));
|
|
}
|
|
let script = text(&arg(args, 1), c)?;
|
|
if let Some(why) = crate::run::refuse(&script) {
|
|
return Err(error(format!("setAction: {why}")));
|
|
}
|
|
write(c, name, |f| {
|
|
f.actions.retain(|(t, _)| *t != trigger);
|
|
f.actions.push((trigger, script));
|
|
})?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
/// `setItems(oArray)`: replace a combo box's or list box's items. Each item is its text, or
|
|
/// a `[text, exportValue]` pair. The value stays when an item still has it, and otherwise
|
|
/// becomes the first item's (#269).
|
|
fn set_items(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
|
|
let f = read(c, name)?;
|
|
if !matches!(f.kind, FieldType::ComboBox | FieldType::ListBox) {
|
|
return Err(error(format!("setItems: {} is not a combo box or list box", f.name)));
|
|
}
|
|
let list = arg(args, 0).as_object().filter(JsObject::is_array).ok_or_else(|| error("setItems needs an array of items"))?;
|
|
let list = JsArray::from_object(list)?;
|
|
let len = list.length(c)?;
|
|
if len > MAX_ITEMS as u64 {
|
|
return Err(error(format!("setItems: at most {MAX_ITEMS} items")));
|
|
}
|
|
let mut items = Vec::new();
|
|
let mut bytes = 0usize;
|
|
for i in 0..len {
|
|
let item = list.get(i, c)?;
|
|
let (label, export) = match item.as_object().filter(JsObject::is_array) {
|
|
Some(pair) => {
|
|
let pair = JsArray::from_object(pair.clone())?;
|
|
let label = text(&pair.get(0, c)?, c)?;
|
|
let export = pair.get(1, c)?;
|
|
let export = if export.is_undefined() { label.clone() } else { text(&export, c)? };
|
|
(label, export)
|
|
}
|
|
None => {
|
|
let label = text(&item, c)?;
|
|
(label.clone(), label)
|
|
}
|
|
};
|
|
bytes = bytes.saturating_add(label.len()).saturating_add(export.len());
|
|
if bytes > MAX_ITEM_BYTES {
|
|
return Err(error("setItems: the items are too long"));
|
|
}
|
|
items.push((export, label));
|
|
}
|
|
write(c, name, |f| {
|
|
let kept: Vec<String> = f.value.iter().filter(|v| items.iter().any(|(export, _)| export == *v)).cloned().collect();
|
|
f.value = if kept.is_empty() { items.first().map(|(export, _)| vec![export.clone()]).unwrap_or_default() } else { kept };
|
|
f.options = items;
|
|
})?;
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
let rw = Attribute::CONFIGURABLE | Attribute::ENUMERABLE;
|
|
let set_action = NativeFunction::from_copy_closure_with_captures(set_action, n.clone());
|
|
let set_items = NativeFunction::from_copy_closure_with_captures(set_items, n.clone());
|
|
let get_item_at = NativeFunction::from_copy_closure_with_captures(get_item_at, n.clone());
|
|
let is_box_checked = NativeFunction::from_copy_closure_with_captures(is_box_checked, n.clone());
|
|
let check_this_box = NativeFunction::from_copy_closure_with_captures(check_this_box, n.clone());
|
|
let set_focus = NativeFunction::from_copy_closure_with_captures(set_focus, n.clone());
|
|
let button_get_caption = NativeFunction::from_copy_closure_with_captures(button_get_caption, n.clone());
|
|
let button_set_caption = NativeFunction::from_copy_closure_with_captures(button_set_caption, n.clone());
|
|
let button_get_icon = NativeFunction::from_copy_closure_with_captures(crate::icons::get, n.clone());
|
|
let button_set_icon = NativeFunction::from_copy_closure_with_captures(crate::icons::set, n.clone());
|
|
ObjectInitializer::new(ctx)
|
|
.accessor(js_string!("value"), Some(value_get), Some(value_set), rw)
|
|
.accessor(js_string!("valueAsString"), Some(as_string), None, rw)
|
|
.accessor(js_string!("defaultValue"), Some(default), None, rw)
|
|
.accessor(js_string!("type"), Some(kind), None, rw)
|
|
.accessor(js_string!("name"), Some(fname), None, rw)
|
|
.accessor(js_string!("readonly"), Some(readonly_get), Some(readonly_set), rw)
|
|
.accessor(js_string!("required"), Some(required_get), Some(required_set), rw)
|
|
.accessor(js_string!("display"), Some(display_get), Some(display_set), rw)
|
|
.accessor(js_string!("hidden"), Some(hidden_get), Some(hidden_set), rw)
|
|
.accessor(js_string!("numItems"), Some(num_items), None, rw)
|
|
.accessor(js_string!("charLimit"), Some(char_limit), None, rw)
|
|
.accessor(js_string!("exportValues"), Some(export_values), None, rw)
|
|
.accessor(js_string!("currentValueIndices"), Some(indices), None, rw)
|
|
.accessor(js_string!("textColor"), Some(text_color_get), Some(text_color_set), rw)
|
|
.accessor(js_string!("fillColor"), Some(fill_color_get), Some(fill_color_set), rw)
|
|
.accessor(js_string!("textSize"), Some(text_size_get), Some(text_size_set), rw)
|
|
.accessor(js_string!("lineWidth"), Some(line_width_get), Some(line_width_set), rw)
|
|
.accessor(js_string!("borderStyle"), Some(border_style_get), Some(border_style_set), rw)
|
|
.accessor(js_string!("userName"), Some(user_name_get), Some(user_name_set), rw)
|
|
.accessor(js_string!("strokeColor"), Some(stroke_color_get), Some(stroke_color_set), rw)
|
|
.accessor(js_string!("rect"), Some(rect_get), Some(rect_set), rw)
|
|
.function(get_item_at, js_string!("getItemAt"), 2)
|
|
.function(is_box_checked, js_string!("isBoxChecked"), 1)
|
|
.function(check_this_box, js_string!("checkThisBox"), 2)
|
|
.function(set_focus, js_string!("setFocus"), 0)
|
|
.function(set_action, js_string!("setAction"), 2)
|
|
.function(set_items, js_string!("setItems"), 1)
|
|
.function(button_get_icon, js_string!("buttonGetIcon"), 1)
|
|
.function(button_set_icon, js_string!("buttonSetIcon"), 2)
|
|
.function(button_get_caption, js_string!("buttonGetCaption"), 1)
|
|
.function(button_set_caption, js_string!("buttonSetCaption"), 2)
|
|
.build()
|
|
}
|
|
|
|
/// The triggers `field.setAction` takes (Acrobat JavaScript reference, Field.setAction).
|
|
const SET_ACTION_TRIGGERS: [&str; 10] =
|
|
["MouseUp", "MouseDown", "MouseEnter", "MouseExit", "OnFocus", "OnBlur", "Keystroke", "Validate", "Calculate", "Format"];
|
|
/// Most items one `setItems` call takes, and most bytes of their texts and export values: a
|
|
/// script must not grow a field without bound.
|
|
const MAX_ITEMS: usize = 10_000;
|
|
const MAX_ITEM_BYTES: usize = 1 << 20;
|
|
|
|
// ── the document (global object) ────────────────────────────────────────────────────────────
|
|
|
|
pub(crate) fn get_field(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
let name = text(&arg(args, 0), ctx)?;
|
|
let exists = host(ctx)?.borrow().fields.iter().any(|f| f.name == name);
|
|
if exists {
|
|
return Ok(field_object(ctx, &name).into());
|
|
}
|
|
// A parent name (e.g. "total" for "total.0"): its first kid stands in, as for groups.
|
|
let prefix = format!("{name}.");
|
|
let kid = host(ctx)?.borrow().fields.iter().find(|f| f.name.starts_with(&prefix)).map(|f| f.name.clone());
|
|
Ok(kid.map_or(JsValue::null(), |k| field_object(ctx, &k).into()))
|
|
}
|
|
|
|
pub(crate) fn get_nth_field_name(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
let i = arg(args, 0).to_number(ctx)?;
|
|
let name = host(ctx)?.borrow().fields.get(i.max(0.0) as usize).map(|f| f.name.clone());
|
|
Ok(name.map_or(JsValue::null(), |n| s(&n)))
|
|
}
|
|
|
|
pub(crate) fn reset_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
{
|
|
let mut h = host(ctx)?.borrow_mut();
|
|
if h.reset_calls >= crate::MAX_RESET_CALLS {
|
|
h.reset_limit_exceeded = true;
|
|
return Err(error("reset call limit exceeded"));
|
|
}
|
|
h.reset_calls = h.reset_calls.saturating_add(1);
|
|
}
|
|
let names = strings(&arg(args, 0), ctx)?;
|
|
let mut h = host(ctx)?.borrow_mut();
|
|
let queued = h.requests.len();
|
|
h.request(Request::Reset(names));
|
|
// Keep the live shadow and the bounded host request in agreement, including clipped names.
|
|
let Some(Request::Reset(names)) = h.requests.get(queued).cloned() else { return Ok(JsValue::undefined()) };
|
|
let super::host::Host { fields, changed, .. } = &mut *h;
|
|
for (i, field) in std::sync::Arc::make_mut(fields).iter_mut().enumerate() {
|
|
if field.default_known
|
|
&& !matches!(field.kind, FieldType::Button | FieldType::Signature)
|
|
&& (names.is_empty() || names.iter().any(|n| field.name == *n || field.name.strip_prefix(n).is_some_and(|rest| rest.starts_with('.'))))
|
|
{
|
|
field.value.clone_from(&field.default);
|
|
// Retain the final snapshot even when a later assignment restores the pre-reset value.
|
|
changed.insert(i);
|
|
}
|
|
}
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
pub(crate) fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
host(ctx)?.borrow_mut().request(Request::Print);
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
pub(crate) fn submit_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
let a = arg(args, 0);
|
|
let url = match a.as_object() {
|
|
Some(o) if !o.is_array() => {
|
|
let u = o.get(js_string!("cURL"), ctx)?;
|
|
text(&u, ctx)?
|
|
}
|
|
_ => text(&a, ctx)?,
|
|
};
|
|
host(ctx)?.borrow_mut().request(Request::Submit(url));
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
pub(crate) fn noop(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
|
Ok(JsValue::undefined())
|
|
}
|
|
|
|
pub(crate) fn num_fields(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
Ok(JsValue::from(host(ctx)?.borrow().fields.len() as f64))
|
|
}
|
|
|
|
pub(crate) fn page_num_get(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
Ok(JsValue::from(host(ctx)?.borrow().doc.page as f64))
|
|
}
|
|
|
|
pub(crate) fn page_num_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
|
let n = arg(args, 0).to_number(ctx)?;
|
|
let mut h = host(ctx)?.borrow_mut();
|
|
let last = h.doc.num_pages.saturating_sub(1);
|
|
let p = (n.max(0.0) as usize).min(last);
|
|
h.doc.page = p;
|
|
h.request(Request::GoToPage(p));
|
|
Ok(JsValue::undefined())
|
|
}
|