Commands & shortcuts
A command is one action of your viewer, like “zoom in” or “download”, defined once: its label, its icon, when it’s enabled or active, what it does, and its keyboard shortcut. Toolbars, menus, a command palette and the keyboard all run the same command, so they always agree. Register the plugin with the commands you need:
<script setup lang="ts">
import { AnnotationToken } from '@embedpdf/vue/annotation';
import { commandsPlugin, standardCommands } from '@embedpdf/vue/commands';
import { DocumentsToken, Viewer } from '@embedpdf/vue/runtime';
import { engine } from './pdf';
import { approveDocument } from './review';
const plugins = [
/* … */
commandsPlugin({
commands: [
...standardCommands,
{
id: 'review:approve',
label: 'Approve',
icon: 'check',
shortcut: 'Mod+Enter',
enabled: ({ get }) => get(AnnotationToken).canCreate(),
run: ({ get }) => approveDocument(get(DocumentsToken)),
},
],
}),
];
</script>
<template>
<Viewer :engine :plugins>
<!-- your toolbar and pages -->
</Viewer>
</template>Show a command#
useCommand(id) gives you everything a button needs, and updates when any of it changes:
<script setup lang="ts">
import { useCommand } from '@embedpdf/vue/commands';
import Icon from './Icon.vue';
const props = defineProps<{ id: string }>();
const command = useCommand(() => props.id);
</script>
<template>
<button
v-if="command.visible"
:disabled="!command.enabled"
:aria-pressed="command.active"
@click="command.run()"
>
<Icon :name="command.icon" />
{{ command.label }}
<kbd v-if="command.shortcut">{{ command.shortcut }}</kbd>
</button>
</template>Loading live preview…
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { commandsPlugin, standardCommands } from '@embedpdf/vue/commands';
import { localEngine } from '@embedpdf/engine';
import CommandButton from './CommandButton.vue';
import '../buttons.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), commandsPlugin({ commands: standardCommands })];
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
<DocumentGate>
<template #fallback><p class="loading">Loading…</p></template>
<div class="toolbar">
<CommandButton id="page:previous" />
<CommandButton id="page:next" />
<CommandButton id="zoom:out" />
<CommandButton id="zoom:in" />
<CommandButton id="zoom:fit-width" />
<CommandButton id="view:rotate-clockwise" />
<CommandButton id="document:download" />
</div>
<Stage class="stage">
<template #page><RenderLayer /></template>
</Stage>
</DocumentGate>
</Viewer>
</template>Pass the id as a function, and the button follows id when it changes.
shortcut is formatted for the user’s platform: ⌘K on a Mac, Ctrl+K elsewhere. The
toolbar draws commands the same way.
There’s always a command, so a button never checks for a missing one: an id no plugin registered
comes back labelled with its id, neither visible nor enabled, the way a turned-off command does,
and in development it warns once, with the closest id there is. Checking visible covers a
command that’s turned off, not allowed or missing. resolveCommand(id) is the lookup for code,
and says null for an id it doesn’t have.
The standard commands#
standardCommands covers what every viewer has, with labels in every language you load:
| Command | Does | Shortcut |
|---|---|---|
zoom:in, zoom:out | Zoom in or out | Mod+=, Mod+- |
zoom:fit-width, zoom:fit-page | Fit the page’s width, or the whole page | |
page:next, page:previous, page:first, page:last | Move through pages | →, ←, Home, End |
view:rotate-clockwise, view:rotate-counterclockwise | Turn the view | |
tool:pointer, tool:pan, tool:zoom | Switch tools | V, H, Z |
tool:<id> | Any registered tool, such as tool:highlight | |
tool:snapshot | Switch to the snapshot tool | |
selection:copy | Copy the selected text | Mod+C |
snapshot:copy, snapshot:save | Copy the snapshot, or save it as a PNG file | Mod+C |
snapshot:clear | Let the snapshot go | Escape |
annotation:delete | Delete the selected annotations | Delete |
document:download, document:print | Download or print the document | Mod+S, Mod+P |
Each one is enabled only when it can run: selection:copy needs a selection and permission to copy,
annotation:delete a selection the user may delete, the snapshot: commands a snapshot. Without
doc.content.copy, tool:snapshot is not allowed.
Keyboard shortcuts#
useCommandShortcuts() turns every command’s shortcut into a working key. Call it once, near the top
of your viewer:
<script setup lang="ts">
import { useCommandShortcuts } from '@embedpdf/vue/commands';
useCommandShortcuts();
</script>
<template>
<!-- your toolbar and pages -->
</template>It ignores keys typed into text fields, except a command’s whose shortcutInTextFields is true
(a Find that should work from the search box too), runs commands for the active document, and leaves a key alone
when its command can’t run right now. A command that’s turned off or not
allowed still takes its key, so the browser doesn’t do something else with
it (Mod+P printing the page), and onRefused(commandId, refusal) tells you why: refusal.message
is a sentence to show. With more than one viewer on a page, pass target, a ref to the element
around one: its keys then work only while focus is inside it. Mod means Cmd on a Mac and Ctrl elsewhere. A help sheet lists
listShortcuts():
Loading live preview…
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { commandsPlugin, standardCommands } from '@embedpdf/vue/commands';
import { localEngine } from '@embedpdf/engine';
import ShortcutArea from './ShortcutArea.vue';
import '../shortcuts.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
commandsPlugin({ commands: standardCommands }),
];
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
<DocumentGate>
<template #fallback><p class="loading">Loading…</p></template>
<ShortcutArea />
</DocumentGate>
</Viewer>
</template>Run a command from code#
await commands.execute('zoom:in');
await commands.execute('review:approve', { args: { note: 'Looks good' } });execute() resolves with { status: 'executed' }, or { status: 'rejected', reason } when the command
is disabled, hidden or unknown.
A command palette#
searchCommands() finds commands by their label, for a palette like the one in code editors:
const results = commands.searchCommands(query); // visible commands whose label or id matchesA disabled command is still in the results, so show it greyed out.
Loading live preview…
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { commandsPlugin, standardCommands } from '@embedpdf/vue/commands';
import { localEngine } from '@embedpdf/engine';
import CommandPalette from './CommandPalette.vue';
import '../palette.css';
const engine = localEngine();
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
commandsPlugin({ commands: standardCommands }),
];
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
<DocumentGate>
<template #fallback><p class="loading">Loading…</p></template>
<div class="layout">
<CommandPalette />
<Stage class="stage">
<template #page><RenderLayer /></template>
</Stage>
</div>
</DocumentGate>
</Viewer>
</template>Define a command#
| Field | What it is |
|---|---|
id | A unique name, by convention area:action |
label | What the user sees, or labelKey for a translated label |
icon | Your icon’s name; your buttons turn it into an icon |
shortcut | A key such as 'Mod+K', or a list of them |
shortcutInTextFields | true: its shortcut runs while the cursor is in a text field too |
enabled | When it can run, as a function of the viewer’s state and your app’s (appState) |
active | When it shows as pressed, such as the current tool |
visible | When it shows at all |
run | What it does |
requires | The permissions it needs, such as 'doc.annotate.modify'. Without them, it’s hidden. |
locked | The lock on the setting it changes, as a function of the viewer’s state: (context) => context.get(StageToken).getLock('spread'). While there is one, it shows disabled. |
categories | The features it belongs to, so you can turn them off together, such as ['annotations.highlight'] |
panel, menu, modal | A command that opens a panel, menu or dialog needs no run |
enabled, active and visible get the viewer’s state through get(Token) and your app’s through
appState. They’re asked again whenever either changes, and after the command runs, so a button is
always right without events or effects of your own. One that throws, because what it reads isn’t
there yet, counts as false: the button is disabled, not pressed, or hidden until it answers.
appState is a setting: values, and functions that change them. Give it a new value whenever your
state changes, and every button asks its command again:
// Your app's state, and what changes it.
let flagged: string[] = [];
function toggleFlag(id: string) {
flagged = flagged.includes(id) ? flagged.filter((f) => f !== id) : [...flagged, id];
// A new value: every button asks its command again.
commands.updateSettings({ appState: { flagged, toggleFlag } });
}
commandsPlugin({
appState: { flagged, toggleFlag },
commands: [
{
id: 'review:flag',
label: 'Flag for review',
active: ({ appState, documentId }) => appState.flagged.includes(documentId),
run: ({ appState, documentId }) => appState.toggleFlag(documentId),
},
],
});A new value replaces the old one whole, never merged, and the commands see a new object, not a
change inside the one they have ([...flagged, id], not flagged.push(id)).
Turn features off#
Put commands in categories, and turn a whole category off: its commands are hidden and refused,
wherever they appear. A category turns off every category under it, on whole dotted parts:
annotations also turns off annotations.highlight. The disabledCategories setting
turns categories off from the start, and while the app runs:
commands.disableCategory('annotations');
commands.enableCategory('annotations');Turn off one command the same way, with disableCommand(id) and the disabledCommands setting. A
command that’s turned off is refused by execute() too, and its shortcut does nothing.
A command of your own, a category switch, and a command run from code when the document opens:
Loading live preview…
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, StageToken, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { commandsPlugin, standardCommands } from '@embedpdf/vue/commands';
import type { CommandDef } from '@embedpdf/vue/commands';
import { localEngine } from '@embedpdf/engine';
import Toolbar from './Toolbar.vue';
import '../custom.css';
const engine = localEngine();
// A command of your own: pressed while the pages show two at a time.
const twoPages: CommandDef = {
id: 'layout:two-pages',
label: 'Two pages',
categories: ['layout'],
active: ({ get }) => get(StageToken).getSettings().spread === 'odd',
run: ({ get }) => {
const stage = get(StageToken);
stage.updateSettings({ spread: stage.getSettings().spread === 'odd' ? 'none' : 'odd' });
},
};
const plugins = [
stagePlugin(),
renderPlugin(),
commandsPlugin({ commands: [...standardCommands, twoPages] }),
];
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
<DocumentGate>
<template #fallback><p class="loading">Loading…</p></template>
<Toolbar />
<Stage class="stage">
<template #page><RenderLayer /></template>
</Stage>
</DocumentGate>
</Viewer>
</template>Methods#
Everything useCommands() gives you.
| Method | What it does |
|---|---|
execute(id, { args?, documentId? }) | Runs a command. Resolves { status, reason? }: 'executed', or 'rejected' with why. A run that throws rejects. Run a command from code |
canExecute(id, documentId?) | Whether a command can run now: it’s registered, enabled and visible |
resolveCommand(id, documentId?) | A command as a button shows it: label, icon, shortcuts, and whether it’s enabled, active and visible. null for an unknown id. In a component, use useCommand(id). Show a command |
getRefusal(id, documentId?) | Why a command won’t run whatever the moment, as { reason, message }: 'turned-off', 'not-allowed' or 'locked', with a sentence for the user. null when it may run, or can’t only right now, and for an unknown id |
listCommands(documentId?) | Every command, resolved the same way, in the order they were added |
searchCommands(query, documentId?) | The visible commands whose label or id contains the query. A command palette |
listShortcuts() | Every shortcut with its command, as { commandId, shortcut }, for a keyboard help sheet. Keyboard shortcuts |
registerCommand(command, { replace? }), registerCommands(commands, { replace? }) | Adds one command, or several, while the app runs, and returns a function that removes them. An id that’s taken is refused unless you pass replace: true. Define a command |
getCommand(id), hasCommand(id) | A command as you defined it, or null; whether it’s registered |
listCommandIds() | Every command’s id, in the order they were added |
disableCategory(category), enableCategory(category) | Turns a category off, with every category under it, or on again. Turn features off |
setDisabledCategories(categories) | Replaces the list of categories that are off |
getDisabledCategories(), isCategoryDisabled(category) | The categories that are off; whether one is, itself or through a category it’s under |
disableCommand(id), enableCommand(id) | Turns one command off, or on again. Turn features off |
setDisabledCommands(ids) | Replaces the list of commands that are off one by one |
getDisabledCommands(), isCommandDisabled(id) | The commands that are off one by one; whether a command is off, by its id or through one of its categories |
getSettings(), updateSettings(changes), resetSettings() | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useCommandsSettings(). Settings |
Leave out documentId, and a command runs and resolves for the document in scope: the one a
<DocumentScope> around your component names, or
else the active one. 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
commands.updateSettings({ … }); commands.resetSettings() goes back to what you registered, and
useCommandsSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
commands | The commands your viewer has | none |
appState | Your app’s state, which commands read as appState; replaced whole | none |
disabledCategories | Categories that start turned off, with every category under them | none |
disabledCommands | Commands that start turned off, by id | none |
notAllowed | What a command the scope doesn’t allow does: 'hide' leaves it out, 'disable' shows it disabled with disabledReason | 'hide' |
Events#
<script setup lang="ts">
import { useCommandsEvent } from '@embedpdf/vue/commands';
import { analytics } from './analytics';
useCommandsEvent(
(commands) => commands.onExecuted,
({ commandId }) => analytics.track('command', { commandId }),
);
</script>
<template>
<!-- your toolbar and pages -->
</template>| Event | Fires when | Carries |
|---|---|---|
onExecuted | A command ran | commandId, documentId, args |
onExecutionFailed | A command’s run threw | commandId, documentId, error |
onSettingsChanged | The settings changed | settings, changed |
Good to know#
Commands and permissions#
A command that needs a permission says so with requires: ['doc.annotate.modify']. When the user
doesn’t have it, the command isn’t visible, so its buttons and menu rows are gone, and its shortcut
does nothing. With the notAllowed: 'disable' setting it shows disabled instead, and the resolved
command’s disabledReason says why, for its tooltip: the i18n string for
commands.notAllowed.<permission> (dots and colons as dashes, such as
commands.notAllowed.doc-print), else commands.notAllowed.default. Keep enabled for what can’t
run right now, such as Copy with nothing selected. The standard commands already say what they
need.
Commands and locked settings#
A command that changes a setting says which with locked: a function that returns the setting’s
lock, or null. While a lock holds the setting, the command
shows disabled, whatever the notAllowed setting says, since the setting has a value the user can
see and the lock is about this document now, not about the user. It stays active when the locked
value is its own, so a menu shows Single page checked and greyed. Its disabledReason, and
getRefusal()‘s 'locked' message, is the i18n string under the lock’s reasonKey, else
commands.locked.default. The standard commands for spreads, scroll direction and view rotation
already say which setting they change.
Your feedback goes directly to the documentation team.