EmbedPDF

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:

commands-plugin.svelte
<script lang="ts">
  import { AnnotationToken } from '@embedpdf/svelte/annotation';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { DocumentsToken, Viewer } from '@embedpdf/svelte/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>

<Viewer {engine} {plugins}>
  <!-- your toolbar and pages -->
</Viewer>

Show a command#

useCommand(id) gives you everything a button needs, and updates when any of it changes:
command-button.svelte
<script lang="ts">
  import { useCommand } from '@embedpdf/svelte/commands';
  import Icon from './Icon.svelte';

  let { id }: { id: string } = $props();
  const command = useCommand(() => id);
</script>

{#if command.current.visible}
  {@const cmd = command.current}
  <button onclick={cmd.run} disabled={!cmd.enabled} aria-pressed={cmd.active}>
    <Icon name={cmd.icon} />
    {cmd.label}
    {#if cmd.shortcut}<kbd>{cmd.shortcut}</kbd>{/if}
  </button>
{/if}

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import CommandButton from './CommandButton.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <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">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>

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:

CommandDoesShortcut
zoom:in, zoom:outZoom in or outMod+=, Mod+-
zoom:fit-width, zoom:fit-pageFit the page’s width, or the whole page
page:next, page:previous, page:first, page:lastMove through pages→, ←, Home, End
view:rotate-clockwise, view:rotate-counterclockwiseTurn the view
tool:pointer, tool:panSwitch toolsV, H
tool:<id>Any registered tool, such as tool:highlight
selection:copyCopy the selected textMod+C
annotation:deleteDelete the selected annotationsDelete
document:download, document:printDownload or print the documentMod+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:

command-shortcuts.svelte
<script lang="ts">
  import { useCommandShortcuts } from '@embedpdf/svelte/commands';

  useCommandShortcuts();
</script>

<!-- your toolbar and pages -->

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 lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { renderPlugin } from '@embedpdf/svelte/render';
  import { stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import ShortcutArea from './ShortcutArea.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ShortcutArea />
  </DocumentGate>
</Viewer>

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 matches

A disabled command is still in the results, so show it greyed out.

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { commandsPlugin, standardCommands } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import CommandPalette from './CommandPalette.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="layout">
      <CommandPalette />
      <Stage class="stage">
        <RenderLayer />
      </Stage>
    </div>
  </DocumentGate>
</Viewer>

Define a command#

FieldWhat it is
idA unique name, by convention area:action
labelWhat the user sees, or labelKey for a translated label
iconYour icon’s name; your buttons turn it into an icon
shortcutA key such as 'Mod+K', or a list of them
shortcutInTextFieldstrue: its shortcut runs while the cursor is in a text field too
enabledWhen it can run, as a function of the viewer’s state
activeWhen it shows as pressed, such as the current tool
visibleWhen it shows at all
runWhat it does
requiresThe permissions it needs, such as 'doc.annotate.modify'. Without them, it’s hidden.
categoriesThe features it belongs to, so you can turn them off together, such as ['annotations.highlight']
panel, menu, modalA 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 lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, StageToken, stagePlugin } from '@embedpdf/svelte/stage';
  import { commandsPlugin, standardCommands, type CommandDef } from '@embedpdf/svelte/commands';
  import { localEngine } from '@embedpdf/engine';
  import Toolbar from './Toolbar.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Toolbar />
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Methods#

Everything useCommands() gives you.

MethodWhat 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.

SettingWhat it doesDefault
commandsThe commands your viewer hasnone
disabledCategoriesCategories that start turned off, with every category under themnone
disabledCommandsCommands that start turned off, by idnone
notAllowedWhat a command the scope doesn’t allow does: 'hide' leaves it out, 'disable' shows it disabled with disabledReason'hide'

Events#

command-events.svelte
<script lang="ts">
  import { useCommandsEvent } from '@embedpdf/svelte/commands';
  import { analytics } from './analytics';

  useCommandsEvent(
    (commands) => commands.onExecuted,
    ({ commandId }) => analytics.track('command', { commandId }),
  );
</script>

<!-- your toolbar and pages -->
EventFires whenCarries
onExecutedA command rancommandId, documentId, args
onExecutionFailedA command’s run threwcommandId, documentId, error
onSettingsChangedThe settings changedsettings, 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.

Was this page helpful?

Your feedback goes directly to the documentation team.