Actions & scripts
A PDF can do things by itself: a button that resets a form, a field that adds up others, a link that opens a website, a script that runs when the document opens. The actions plugin runs them, with rules you choose. Register it, and install the browser’s defaults for opening websites and printing once, near the top of your viewer:
import { actionsPlugin, useActionsUiAdapter } from '@embedpdf/react/actions';
export const plugins = [/* … */ actionsPlugin()];
export function ViewerShell() {
useActionsUiAdapter();
return <>{/* your viewer */}</>;
}Without it, the plugin still runs what stays inside the viewer, such as going to a page or resetting a form, but it won’t open a website or a print dialog.
What runs#
| Action | By default |
|---|---|
| Go to a page or a destination | Runs |
| A viewer action, such as next page | Runs |
| Show or hide a form field | Runs |
| Reset a form | Runs |
| Open a website | Goes to your UI adapter when the user clicked; reported otherwise |
| Goes to your UI adapter when the user clicked; never on its own | |
| Submit a form | Goes to your submit handler when the user clicked; never on its own |
| JavaScript | Off, until you turn it on |
Actions that start a program, open another file on the user’s computer, play media or import data never run.
Turn on JavaScript#
Many forms calculate totals, check values and format dates with JavaScript. Turn it on:
actionsPlugin({ javascript: { enabled: true } });Scripts run in a sealed sandbox: they can read and change the document’s form fields and move between pages, but they can’t reach your app, the page’s HTML or the network. Each script has a time budget, so a script that loops forever is stopped instead of freezing the viewer.
A script can ask who the user is (identity.name in Acrobat’s JavaScript). It gets the user the
document was opened for, and you can add the fields scripts often ask for:
actionsPlugin({
javascript: { enabled: true, identity: { corporation: 'Acme', email: 'dana@acme.com' } },
});Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import {
actionsPlugin,
useActions,
useActionsEvent,
useActionsUiAdapter,
type ActionContext,
type PdfActionTree,
} from '@embedpdf/react/actions';
import { localEngine } from '@embedpdf/engine';
import './javascript.css';
const engine = localEngine();
// JavaScript on, and two fields scripts often ask for about the user.
const plugins = [
actionsPlugin({
javascript: { enabled: true, identity: { name: 'Dana Smith', corporation: 'Acme' } },
}),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A JavaScript action, as a PDF carries it behind a button or a link.
const script = (code: string): PdfActionTree => ({
root: { type: 'javascript', subtype: 'JavaScript', script: code, next: [] },
incomplete: false,
warningFlags: 0,
warnings: [],
});
const click: ActionContext = {
origin: 'user',
source: { kind: 'api' },
event: { scope: 'activate' },
};
const GREETING = "app.alert('Hello, ' + identity.name + ' from ' + identity.corporation + '!');";
// Edit the script and run it. Its alerts and its errors show below it.
function ScriptConsole() {
const actions = useActions();
const [code, setCode] = useState(GREETING);
const [output, setOutput] = useState<Array<{ kind: 'alert' | 'error'; text: string }>>([]);
const show = (kind: 'alert' | 'error', text: string) =>
setOutput((lines) => [{ kind, text }, ...lines].slice(0, 5));
// A script's alert goes to your UI adapter: here, a line below the script.
useActionsUiAdapter({ alert: (message) => show('alert', message) });
useActionsEvent(
(capability) => capability.onScriptFailed,
({ error }) => show('error', error.message),
);
// The greeting runs once on load.
const greeted = useRef(false);
useEffect(() => {
if (greeted.current) return;
greeted.current = true;
void actions.execute(script(GREETING), click);
});
return (
<section className="panel">
<label className="name" htmlFor="script">
Script {actions.isScriptingEnabled() ? '' : '(JavaScript is off)'}
</label>
<textarea
id="script"
className="code"
spellCheck={false}
rows={3}
value={code}
onChange={(event) => setCode(event.target.value)}
/>
<div className="actions">
<button
type="button"
className="button primary"
onClick={() => actions.execute(script(code), click)}
>
Run
</button>
<button type="button" className="button" onClick={() => setCode('app.alrt("typo");')}>
A script with a mistake
</button>
</div>
<ol className="output" aria-live="polite">
{output.map((line, index) => (
<li key={output.length - index} className={line.kind}>
{line.text}
</li>
))}
</ol>
</section>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<ScriptConsole />
</DocumentGate>
</Viewer>
);
}How websites open, printing and alerts#
useActionsUiAdapter() installs the browser’s defaults: a website opens in a new tab after a safety
check, printing opens the print dialog, and a script’s alert shows the browser’s alert. Replace any
of them with your own UI:
import { useActionsUiAdapter } from '@embedpdf/react/actions';
import { toast } from './toast';
export function ViewerShell() {
useActionsUiAdapter({
openUri: (uri) => {
if (confirm(`Open ${uri}?`)) window.open(uri, '_blank', 'noopener');
},
alert: (message) => toast(message),
});
return <>{/* your viewer */}</>;
}Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import {
actionsPlugin,
useActions,
useActionsUiAdapter,
type ActionContext,
type PdfActionTree,
} from '@embedpdf/react/actions';
import { localEngine } from '@embedpdf/engine';
import './website-links.css';
const engine = localEngine();
const plugins = [actionsPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A link to a website, as a PDF carries it: what getActionTree() reads from a link.
const websiteLink: PdfActionTree = {
root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
incomplete: false,
warningFlags: 0,
warnings: [],
};
const click: ActionContext = {
origin: 'user',
source: { kind: 'api' },
event: { scope: 'activate' },
};
// Your UI decides how a website opens: here, it asks first.
function AskBeforeOpening() {
const actions = useActions();
const [asking, setAsking] = useState<string | null>(null);
useActionsUiAdapter({ openUri: (uri) => setAsking(uri) });
// The link is clicked once on load, so the question is there to see.
const clicked = useRef(false);
useEffect(() => {
if (clicked.current) return;
clicked.current = true;
void actions.execute(websiteLink, click);
});
return (
<section className="panel">
<p className="lead">
The document links to <code>https://www.embedpdf.com</code>.
</p>
<button type="button" className="button" onClick={() => actions.execute(websiteLink, click)}>
Click the link
</button>
{asking && (
<div className="prompt" role="alertdialog" aria-label="Open a website">
<p className="prompt-text">
This document wants to open <strong>{asking}</strong>.
</p>
<div className="prompt-actions">
<button type="button" className="button" onClick={() => setAsking(null)}>
Stay here
</button>
<button
type="button"
className="button primary"
onClick={() => {
window.open(asking, '_blank', 'noopener');
setAsking(null);
}}
>
Open in a new tab
</button>
</div>
</div>
)}
</section>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<AskBeforeOpening />
</DocumentGate>
</Viewer>
);
}| Handler | Called when |
|---|---|
openUri | An action opens a website or an email address |
print | An action or a script asks to print |
alert | A script shows a message |
gotoPage | A script moves to a page |
Change the rules#
Each kind of action has a rule for each way it can start: a click by the user (user), the pointer
moving over something (hover), or the document opening or closing (lifecycle):
actionsPlugin({
policy: {
uri: { user: 'adapter', hover: 'block', lifecycle: 'block' }, // websites only on click
javascript: { lifecycle: 'block' }, // no scripts when the document opens
},
});| Rule | Means |
|---|---|
'allow' | Run it |
'adapter' | Hand it to your UI adapter (a form submit to your submit handler), which decides |
'report' | Don’t run it; report it in onDiagnosticReported |
'block' | Don’t run it |
The rules can change while the app runs, for example when the user trusts a document:
actions.updateSettings({ policy: { javascript: { lifecycle: 'allow' } } });Loading live preview…
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import {
actionsPlugin,
useActions,
useActionsEvent,
useActionsSettings,
useActionsUiAdapter,
type ActionContext,
type ActionOrigin,
type ActionPolicyDecision,
type PdfActionTree,
} from '@embedpdf/react/actions';
import { localEngine } from '@embedpdf/engine';
import './rules.css';
const engine = localEngine();
// Websites open only on a click, as the page's example registers it.
const plugins = [
actionsPlugin({
policy: { uri: { user: 'adapter', hover: 'block', lifecycle: 'block' } },
}),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A link to a website, as a PDF carries it: what getActionTree() reads from a link.
const websiteLink: PdfActionTree = {
root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
incomplete: false,
warningFlags: 0,
warnings: [],
};
// The three ways an action can start, each as the context it runs with.
const STARTS: ReadonlyArray<{ label: string; context: ActionContext }> = [
{
label: 'A click',
context: { origin: 'user', source: { kind: 'api' }, event: { scope: 'activate' } },
},
{
label: 'The pointer over it',
context: {
origin: 'hover',
source: { kind: 'api' },
event: { scope: 'annotation', name: 'cursorEnter' },
},
},
{
label: 'The document opening',
context: {
origin: 'lifecycle',
source: { kind: 'api' },
event: { scope: 'document', name: 'open' },
},
},
];
const RULES: readonly ActionPolicyDecision[] = ['allow', 'adapter', 'report', 'block'];
// The rules for websites, changed while the app runs, and what happens when the link runs.
function WebsiteRules() {
const actions = useActions();
const rules = useActionsSettings((settings) => settings.policy.uri);
const [log, setLog] = useState<string[]>([]);
const note = (line: string) => setLog((lines) => [line, ...lines].slice(0, 6));
// Instead of opening a tab, the adapter notes the website it was given.
useActionsUiAdapter({ openUri: (uri) => note(`Your adapter got ${uri}`) });
useActionsEvent(
(capability) => capability.onDiagnosticReported,
({ code, action }) => note(`The ${action} action was not run: ${code}`),
);
const run = (context: ActionContext) => actions.execute(websiteLink, context);
const setRule = (origin: ActionOrigin, rule: ActionPolicyDecision) =>
actions.updateSettings({ policy: { uri: { [origin]: rule } } });
// One click on load, so the log shows what a click does.
const clicked = useRef(false);
useEffect(() => {
if (clicked.current) return;
clicked.current = true;
void run(STARTS[0].context);
});
return (
<section className="panel">
<ul className="starts">
{STARTS.map(({ label, context }) => (
<li className="start" key={context.origin}>
<span className="start-label">
{label} <code>{context.origin}</code>
</span>
<select
className="field"
aria-label={`The rule for ${context.origin}`}
value={rules[context.origin]}
onChange={(event) =>
setRule(context.origin, event.target.value as ActionPolicyDecision)
}
>
{RULES.map((rule) => (
<option key={rule} value={rule}>
{rule}
</option>
))}
</select>
<button type="button" className="button" onClick={() => run(context)}>
Run the link
</button>
</li>
))}
</ul>
<div className="footer">
<ol className="log" aria-live="polite">
{log.map((line, index) => (
<li key={log.length - index}>{line}</li>
))}
</ol>
<button type="button" className="button" onClick={() => actions.resetSettings()}>
Back to the registered rules
</button>
</div>
</section>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<WebsiteRules />
</DocumentGate>
</Viewer>
);
}Run an action from code#
await actions.executeNamed('NextPage');executeNamed runs a viewer action by its name: 'NextPage', 'PrevPage', 'FirstPage',
'LastPage' or 'Print'. It follows the same rules as a click.
Loading live preview…
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
actionsPlugin,
useActions,
useActionsUiAdapter,
type PdfNamedAction,
} from '@embedpdf/react/actions';
import { localEngine } from '@embedpdf/engine';
import './run-from-code.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), actionsPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
const VERBS: ReadonlyArray<[PdfNamedAction, string]> = [
['FirstPage', '⇤ First'],
['PrevPage', '‹ Previous'],
['NextPage', 'Next ›'],
['LastPage', 'Last ⇥'],
['Print', 'Print'],
];
// The viewer actions a PDF's buttons name, run from your own buttons.
function NamedActions() {
const actions = useActions();
const { currentPageIndex, pageCount } = useStageState();
const [ran, setRan] = useState<string | null>(null);
// Print goes to your UI adapter. This one says so, instead of opening the browser's print dialog.
useActionsUiAdapter({ print: () => setRan('Print: your print handler ran') });
// On load, as if a "Last page" button in the PDF was clicked. Running it twice changes nothing.
useEffect(() => {
void actions.executeNamed('LastPage').then(({ status }) => setRan(`LastPage: ${status}`));
}, [actions]);
return (
<div className="toolbar">
{VERBS.map(([name, label]) => (
<button
type="button"
className="button"
key={name}
disabled={!actions.canExecuteNamed(name)}
onClick={async () => {
const { status } = await actions.executeNamed(name);
if (name !== 'Print') setRan(`${name}: ${status}`);
}}
>
{label}
</button>
))}
<span className="spacer" />
<output className="readout">
Page {currentPageIndex + 1} of {pageCount}
{ran && <span className="ran"> · {ran}</span>}
</output>
</div>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<NamedActions />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}Methods#
Everything useActions() gives you.
| Method | What it does |
|---|---|
executeNamed(name, context?) | Runs a viewer action by its name, such as 'NextPage', as if the user clicked it. Run an action from code |
execute(tree, context) | Runs actions you already have, such as a tree from getActionTree(), under the rules. Resolves { status, nodes, diagnostics }: an action that fails is reported there, not thrown. |
dispatch(trigger) | Reports that something happened, such as a click on a link or a page opening, and runs the actions behind it. The viewer does this for its own clicks and pages. Never rejects: problems come back in the result. |
getActionTree(source) | Reads the actions behind a link or button, a form field, a page or the document, without running them. Resolves null when there are none. |
isScriptingEnabled() | Whether this document’s JavaScript runs: it’s turned on and the script engine started. Turn on JavaScript |
setUiAdapter(adapter) | Installs your handlers for websites, printing and alerts in place of the ones before, and returns a function that removes them. useActionsUiAdapter() does this for you. How websites open, printing and alerts |
setSubmitHandler(handler) | Sets the function a form’s Submit button hands its values to, with the address the PDF names. The viewer never sends them anywhere itself. Returns a function that removes it. |
registerExecutor(type, executor) | Runs one kind of action, such as 'goto', with your own code in place of the built-in one. Returns a function that removes it. |
runDocumentVerb(verb, operation) | Runs your own save ('save') or print ('print') between the document’s before and after actions, and resolves with what operation returns. download() and print() do this by themselves. |
prepareClose() | Runs the document’s closing actions. Call it before documents.close(), which skips them. |
getSettings(), updateSettings(changes), resetSettings() | Read the settings, change the rules while the app runs, and go back to what you registered. Settings |
canExecute(tree, context), canExecuteNamed(name) | Whether the rules would run these actions, or this viewer action. Change the rules |
canDispatch(trigger) | Whether that kind of trigger is turned on in the triggers setting. Settings |
context is { origin, source, event }: why the actions run ('user', 'hover' or 'lifecycle', as
in Change the rules), what started them, and which event it was. executeNamed()
fills it in as a click from your code. Every method that returns a promise also takes a signal in
its options, to cancel it.
Settings#
Set them when you register the plugin, and change them while the app runs with
actions.updateSettings({ … }); actions.resetSettings() goes back to what you registered, and
useActionsSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
javascript.enabled | Run the document’s JavaScript. A change applies to documents opened after it, since a document’s script engine starts when it opens | false |
javascript.identity | Who scripts think the user is, on top of the document’s user | the document’s user |
policy | The rules above, per kind of action and per origin | as in the table |
triggers | Run actions of the document, its pages, or annotations | all true |
openSequence | When the document’s opening actions run: 'auto', 'headless' (as the document opens, for an app without a Stage), 'off'. A change applies to documents whose opening actions haven’t run yet | 'auto' |
Events#
import { useActionsEvent } from '@embedpdf/react/actions';
export function ActionDiagnostics() {
useActionsEvent(
(actions) => actions.onDiagnosticReported,
({ code, action }) => console.warn(`Action ${action} was not run: ${code}`),
);
return null;
}| Event | Fires when | Carries |
|---|---|---|
onExecuted | Actions ran, whatever started them | tree, result, source |
onDiagnosticReported | An action was blocked, reported or couldn’t run | code, action, source |
onScriptFailed | A script threw an error | error, source |
onScriptDiagnosticReported | A script was stopped from doing something, such as reaching the network, or used something the viewer doesn’t support | code, message |
onOpenSequenceCompleted | The document’s opening actions ran | result |
onSettingsChanged | A setting changed | settings, changed |
Permissions#
Actions don’t need a permission of their own, but some things they do do: printing needs
doc.print, and changing a form field needs doc.forms.fill. An action the user may not perform
is reported, never run.
Good to know#
When opening actions run#
With openSequence: 'auto', the actions a document runs when it opens wait for the first sign of a
person: the UI adapter being installed, or a first click. So a document that plays a script on open
doesn’t do it in a background tab nobody has looked at.
Scripts and forms#
Form calculations, validation and formatting are scripts too: with JavaScript on, the form plugin runs them as people type. See Filling forms.
Your feedback goes directly to the documentation team.