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 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()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
<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 | Switch tools | V, H |
tool:<id> | Any registered tool, such as tool:highlight | |
selection:copy | Copy the selected text | Mod+C |
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.
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 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()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
<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 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()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
<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 |
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. |
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 are asked again
whenever anything changes, so a button is always right without events or effects of your own.
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 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()) };
};
</script>
<template>
<Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
<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' or 'not-allowed', 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 |
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.
Your feedback goes directly to the documentation team.