Skip to content

feat: add an onClick option for a form field's mouse-up action - #1790

Merged
blikblum merged 1 commit into
foliojs:masterfrom
KaiPressmar:acroform-aa-action-option
Sep 11, 2026
Merged

blikblum merged 1 commit into
foliojs:masterfrom
KaiPressmar:acroform-aa-action-option

Conversation

@KaiPressmar

@KaiPressmar KaiPressmar commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature. Fixes #1792.

A small, purpose-built option, along the lines invited in docs/forms.md's Advanced Form Field Use section ("If an option is not supported, open an issue on Github and it will be considered for addition to the API").

Context

There's no supported way to attach a mouse-up JavaScript action to a form field — most usefully, a push button that runs custom logic when clicked. The only path that ever reaches AA is mapFormat(), and only together with a format option, which is meant for keystroke/format validation, not arbitrary actions.

That's deliberate, not an oversight: pdfkit's docs explicitly say the handful of raw dictionary escape hatches still recognized (Ff, MK.CA, AA-with-format) are discouraged and may be removed. So rather than widening that raw access further, this adds a dedicated option instead.

The change

Adds an onClick option, accepted by all form annotation methods, mapped by a new mapActions(options, pdfObject) step (mirroring the other unconditional mappers) that sets the field's AA.U mouse-up action. mapFormat() still runs afterward and extends the same AA dictionary with format-validation actions when a format option is also given, so that combination keeps working.

onClick accepts either a plain string or a function. A function is stringified and invoked with this bound to the Document (exactly as Acrobat itself binds it in any field action) and Acrobat's own app, getField, display and event passed in as arguments — the same pattern tools like Puppeteer use for page.evaluate(fn). It still runs inside the PDF viewer's own JavaScript engine, not wherever the PDF was generated, so it can't close over outside variables.

doc.formPushButton('btn1', 10, 200, 100, 30, {
  label: 'Test Button',
  onClick: function (app) {
    app.alert('clicked');
    this.getField('otherField').value = 'updated from btn1';
  },
});

Passing the globals as parameters, rather than the more obvious route of declaring them as ambient TypeScript globals, is deliberate: a global event collides with the DOM lib's own deprecated window.event in any project with "dom" in its lib array, which I only found by actually trying it. Parameters sidestep that entirely — nothing is declared globally, so there's nothing to collide with.

A new types/acrobat-js.d.ts (exported via package.json's exports map) gives TypeScript projects real types for this signature, kept separate from pdfkit's own types so nothing is pulled in just by installing pdfkit:

import type { AcrobatOnClick } from 'pdfkit/types/acrobat-js';

const onClick: AcrobatOnClick = function (app) {
  app.alert('clicked');
};

This is deliberately a minimal, best-effort common subset (app, getField, display, event, the Document this), not a full Acrobat SDK type surface — documented as such, with contributions welcome to extend it.

Demo

Attached: a minimal PDF pair (a push button with an onClick action, before/after this change) plus screenshots from Adobe Acrobat/Reader. Before: clicking the button does nothing. After: it shows an alert.

image

Testing

  • yarn test:unit — all existing tests pass unchanged, plus new ones: a push button with a string onClick, one with a function onClick, and the combined onClick + format case.
  • yarn lint / yarn format — clean.
  • The new type declarations were verified against both classic (node) and node16 TypeScript module resolution, and against a project with "dom" in lib, using a throwaway project outside this repo (not part of the diff).

Checklist:

  • Unit Tests
  • Documentation
  • Update CHANGELOG.md
  • Ready to be merged

cc @blikblum for review.

@KaiPressmar KaiPressmar changed the title fix: recognize AA unconditionally, not only with a format option fix: form fields silently drop a custom AA (action) option Sep 5, 2026
@KaiPressmar
KaiPressmar force-pushed the acroform-aa-action-option branch 3 times, most recently from 105406c to dde6b58 Compare September 5, 2026 12:10
@KaiPressmar KaiPressmar changed the title fix: form fields silently drop a custom AA (action) option feat: add an onClick option for a form field's mouse-up action Sep 5, 2026
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

Updated: on reflection this shouldn't widen the discouraged raw AA escape hatch further (the docs are explicit that even the existing ones are meant to shrink over time). Replaced with a purpose-built onClick option instead — description and diff updated above.

@KaiPressmar
KaiPressmar force-pushed the acroform-aa-action-option branch from dde6b58 to cea74ed Compare September 5, 2026 12:27
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

Updated again: onClick now calls the function with app/getField/display/event as parameters (this bound to the Document) instead of relying on ambient TypeScript globals — a plain global event turned out to collide with the DOM lib's deprecated window.event. Added a small, opt-in types/acrobat-js.d.ts for TypeScript users on top of that. Description and diff updated above.

@KaiPressmar
KaiPressmar force-pushed the acroform-aa-action-option branch 2 times, most recently from 8732f59 to 6e700f0 Compare September 5, 2026 12:33
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

Note: the aa-before.pdf/aa-after.pdf attached above were built against an earlier version of this PR (the raw AA escape hatch, before the switch to the onClick option). They still demonstrate the same before/after behavior visually, but I'll swap in ones built against the current onClick-based code shortly.

@blikblum blikblum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides onClick, is there other event that can be set?

Comment thread docs/forms.md Outdated
doc.formPushButton('btn1', 10, 200, 100, 30, opts);
```

`onClick` also accepts a plain function, called with Acrobat's own `app`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify the description. Be objective hiding internal details, just with enough information to user create correct functions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten. The push-button option is now a single line pointing at a short "Field Actions" section under Common Annotation Options, which lists the six options one line each and shows a string example and a function example. No stringification details, no this-binding rationale, no array.map analogy.

Comment thread docs/forms.md Outdated
Comment on lines +151 to +154
onClick: function (app, getField) {
app.alert('clicked');
this.getField('otherField').value = 'updated from btn1';
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

param getField is not used. Is really necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No — removed. The action is now written as (fn).call(this); with no arguments at all: the viewer's globals are already in scope inside the action, so there was never a need to pass them in. The only reason they were parameters was to avoid ambient global declarations for the types file, which is gone too.

Comment thread docs/forms.md Outdated
Comment on lines +158 to +161
TypeScript projects can import `AcrobatOnClick` and the other types this
signature uses from `pdfkit/types/acrobat-js` — a small, best-effort set of
types for the handful of Acrobat globals most `onClick` handlers need, kept
separate from pdfkit's own types so nothing is declared globally:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop. Types will be handled separately. Do not expose it for now. It can be documented internally

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. types/acrobat-js.d.ts is deleted, the package.json export is gone, and this documentation section with it. Nothing about types is exposed by the PR any more.

Comment thread docs/forms.md Outdated
### Advanced Form Field Use

Older implementations used to pass all unknown options to the internal PDF object structure. A small set of direct PDF dictionary escape hatches is still recognized: `Ff`, `MK.CA`, and `AA` when a `format` option is used but its use is discouraged and likely will be removed in future versions.
Older implementations used to pass all unknown options to the internal PDF object structure. A small set of direct PDF dictionary escape hatches is still recognized: `Ff` and `MK.CA`, but their use is discouraged and they may be removed in future versions. A previously-recognized `AA` escape hatch (only reachable together with a `format` option) has been replaced by the `onClick` option above, which needs no PDF dictionary knowledge and works on its own.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop. This is not a changelog and no need to justify

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, and the paragraph is back to master's wording, untouched. It only needed rewording because the original diff had dropped the AA escape hatch; I kept the escape hatch working instead, so the sentence is still accurate as written. mapFormat() runs first and still seeds pdfObject.AA from options.AA, and the action options merge into that dictionary afterwards.

Comment thread lib/mixins/acroform.js
// generated, so it can't close over outside variables, and only plain
// function syntax (not arrow functions or other syntax Acrobat's engine
// may not support) should be relied on.
const js =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep it simple, pass the base minimum arguments to get it working

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — it passes no arguments now, just (fn).call(this);. Only the this binding remains, which is the binding a viewer already gives any field action, and it is one token rather than a parameter list.

Comment thread package.json Outdated
"require": "./js/output.cjs",
"default": "./js/output.mjs"
},
"./types/acrobat-js": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted. package.json is unchanged from master in this PR now.

KaiPressmar added a commit to KaiPressmar/pdfkit that referenced this pull request Sep 11, 2026
…rk-only)

Not for upstream: this lets Yarn install this branch directly from
GitHub without a build step, since pdfkit has no committed build
output and Yarn Classic v1 does not reliably run "prepare"/"postinstall"
for a nested git dependency's own devDependencies. Consumed by the Plan
monorepo while foliojs#1789 and
foliojs#1790 are under review; rebuild
and recommit js/ if this branch is rebased onto a newer upstream master.
Add `onClick`, `onMouseDown`, `onMouseEnter`, `onMouseExit`, `onFocus` and
`onBlur` to the options every form annotation method accepts, mapping to the
widget annotation's /AA entries. Each takes the JavaScript to run, as a
string or as a plain function whose source text is written into the action.

The `AA` escape hatch keeps working exactly as documented: mapFormat() still
seeds the dictionary from it, and the action options are merged in
afterwards.
@KaiPressmar
KaiPressmar force-pushed the acroform-aa-action-option branch from 6e700f0 to 5ccc29d Compare September 11, 2026 11:39
KaiPressmar added a commit to KaiPressmar/pdfkit that referenced this pull request Sep 11, 2026
…rk-only)

Not for upstream: this lets Yarn install this branch directly from
GitHub without a build step, since pdfkit has no committed build
output and Yarn Classic v1 does not reliably run "prepare"/"postinstall"
for a nested git dependency's own devDependencies. Consumed by the Plan
monorepo while foliojs#1789 and
foliojs#1790 are under review; rebuild
and recommit js/ if this branch is rebased onto a newer upstream master.
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

Thanks — pushed an update addressing all of it.

"Besides onClick, is there other event that can be set?"

Yes. The widget annotation's /AA (table 197) has ten entries: U (mouse up), D (mouse down), E/X (cursor enter/exit), Fo/Bl (focus/blur), and four page-level ones (PO/PC/PV/PI). The field /AA additionally has K/F/V/C, which mapFormat() already owns.

Since the mapping is just a table, I exposed the six interaction events rather than special-casing one:

const ANNOTATION_ACTIONS = {
  onClick: 'U', onMouseDown: 'D', onMouseEnter: 'E',
  onMouseExit: 'X', onFocus: 'Fo', onBlur: 'Bl',
};

I left the four page-level ones out — they describe the page rather than the field, and I have no use case for them. Happy to cut this back to onClick alone if you would rather keep the surface minimal; it is a one-line change either way.

The inline comments

  • Dropped types/acrobat-js.d.ts and its package.json export entirely.
  • Removed the arguments. A function is now written as (fn).call(this); — nothing beyond the this binding a viewer already gives any field action. The viewer's own globals are in scope inside the function, so nothing needs passing in.
  • Rewrote the docs: one short section under Common Annotation Options, no internals and no justification.
  • Left the AA escape hatch exactly as documented. mapFormat() still seeds pdfObject.AA from options.AA and the action options merge in afterwards (mapFormat runs first now), so no documented behaviour changes and that paragraph in forms.md is untouched.

The diff is additions only now — no edits to existing docs text and no package.json change.

@blikblum

Copy link
Copy Markdown
Member

Many thanks

@blikblum
blikblum merged commit e6f498c into foliojs:master Sep 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support a mouse-up JavaScript action on form fields (onClick option)

2 participants