EmbedPDF

Panels & dialogs

A viewer has sidebars (thumbnails, comments, search), dialogs and menus. The shell plugin keeps track of which are open, so a toolbar button, a keyboard shortcut and your own code can all open the same panel, and you can remember the layout for next time. You draw the panels; the plugin only holds their state. Register shellPlugin():

shell-plugin.vue
<script setup lang="ts">
import { Viewer } from '@embedpdf/vue/runtime';
import { shellPlugin } from '@embedpdf/vue/shell';
import { engine } from './pdf';

const plugins = [/* … */ shellPlugin()];
</script>

<template>
  <Viewer :engine :plugins>
    <!-- your toolbar, panels and pages -->
  </Viewer>
</template>

Then ask for a panel by name:

comments-panel.vue
<script setup lang="ts">
import { useSurface } from '@embedpdf/vue/shell';
import CommentList from './CommentList.vue';

const { isOpen, close, toggle } = useSurface('comments');
</script>

<template>
  <button @click="toggle({ exclusive: 'right' })">Comments</button>

  <aside v-if="isOpen">
    <button @click="close()">Close</button>
    <CommentList />
  </aside>
</template>

The names are yours: 'comments', 'thumbnails', 'settings'. Each document keeps its own, so every tab remembers which panels were open.

One panel per side#

Most layouts show one panel per side. Give panels a group with exclusive, and opening one closes the others in that group:

shell.open('thumbnails', { exclusive: 'left' });
shell.open('outline', { exclusive: 'left' }); // closes thumbnails
shell.open('comments', { exclusive: 'right' }); // the left panel stays

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 { searchPlugin } from '@embedpdf/vue/search';
import { shellPlugin } from '@embedpdf/vue/shell';
import { localEngine } from '@embedpdf/engine';
import Workspace from './Workspace.vue';

import '../sidebars.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), searchPlugin(), shellPlugin()];

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>
      <Workspace />
    </DocumentGate>
  </Viewer>
</template>

Open a panel with something in it#

A panel can carry data, such as which comment to scroll to:

shell.open('comments', { exclusive: 'right', props: { focus: thread.root.ref } });

The panel reads them:

panel-props.vue
<script setup lang="ts">
import { useSurface } from '@embedpdf/vue/shell';
import CommentList from './CommentList.vue';

const { props } = useSurface('comments'); // { focus: … }
</script>

<template>
  <CommentList :focus="props.focus" />
</template>

shell.updateSurfaceProps('comments', { focus: nextThread.root.ref }) changes them without reopening the panel.

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 { searchPlugin } from '@embedpdf/vue/search';
import { shellPlugin } from '@embedpdf/vue/shell';
import { localEngine } from '@embedpdf/engine';
import Workspace from './Workspace.vue';

import '../props.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), searchPlugin(), shellPlugin()];

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>
      <Workspace />
    </DocumentGate>
  </Viewer>
</template>

Put the cursor in a panel#

A panel you type in, such as search, can take the cursor as it opens. Ask for it when you open the panel:

shell.open('search', { focus: true });

and say which element takes it, with useSurfaceFocus(id, ref):

panel-focus.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { useSurfaceFocus } from '@embedpdf/vue/shell';

const field = useTemplateRef<HTMLInputElement>('field');
// The cursor goes in the field when the panel asks for it, its text selected.
useSurfaceFocus('search', field, { select: true });
</script>

<template>
  <input ref="field" placeholder="Search" />
</template>

The element takes focus on every open that asks, also when the panel is open already (pressing a Find key again), and when it appears while the request is up, as a panel whose code loads on its first open does. A plain open(id) leaves the cursor where it is. select: true selects the field’s text, so typing replaces it.

The request is the panel’s focusRequest in State: how many opens asked since the panel opened, back to 0 when it closes. Snapshots leave it out.

From commands#

A command can open a panel with no code: give it panel, and its button toggles the panel and shows as pressed while it’s open:

{ id: 'panel:comments', label: 'Comments', icon: 'comment', panel: { id: 'comments', exclusive: 'right' } }

With focus: true in panel, opening it asks for the cursor, as above. modal does the same for a dialog, and menu for a menu.

Menus are tracked separately, since several can be open at once, such as a menu and its submenu:

shell.openMenu('more');
shell.closeAllMenus(); // for a click outside, or Escape

openMenus in State lists the open menus, the last one opened on top.

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 { selectionPlugin } from '@embedpdf/vue/selection';
import { shellPlugin } from '@embedpdf/vue/shell';
import { localEngine } from '@embedpdf/engine';
import Workspace from './Workspace.vue';

import '../menus.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  shellPlugin(),
];

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>
      <Workspace />
    </DocumentGate>
  </Viewer>
</template>

Remember the layout#

localStorage.setItem('panels', JSON.stringify(shell.getSnapshot()));
 
// next time
shell.applySnapshot(JSON.parse(localStorage.getItem('panels')!));

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 { shellPlugin } from '@embedpdf/vue/shell';
import { localEngine } from '@embedpdf/engine';
import Workspace from './Workspace.vue';

import '../remember.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), shellPlugin()];

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>
      <Workspace />
    </DocumentGate>
  </Viewer>
</template>

Methods#

Everything useShell() gives you.

MethodWhat it does
open(id, { exclusive?, props?, focus? })Opens a panel or dialog. exclusive closes the others in that group, props travels with it, and focus asks for the cursor, also when it’s open. One panel per side, Put the cursor in a panel
toggle(id, { exclusive?, props?, focus? })Opens a closed panel, with the options open takes, or closes an open one
close(id), closeAll()Closes one panel, or every panel
updateSurfaceProps(id, props)Changes a panel’s props without reopening it. Open a panel with something in it
openMenu(id), closeMenu(id), toggleMenu(id)Opens, closes or toggles one menu. Opening a menu leaves the others open, so a submenu stays above its menu. Menus
closeAllMenus()Closes every menu, for a click outside or Escape
getSnapshot(), applySnapshot(snapshot)Everything the plugin holds, as plain data you can store; and back. Remember the layout
In a component, useSurface(id) gives you one panel: { isOpen, props, open, close, toggle }.

State#

Everything useShellState() gives you, as refs: your template updates when a value changes. The method in the last column reads the same value once.

FieldWhat it isRead once
openSurfacesThe open panels and dialogs, each with its id, exclusive group, props and focusRequestlistOpenSurfaces(), or isOpen(id) and getSurface(id) for one
openMenusThe open menus’ ids, the last one opened on toplistOpenMenus(), or isMenuOpen(id) for one

Events#

panel-events.vue
<script setup lang="ts">
import { useShellEvent } from '@embedpdf/vue/shell';
import { analytics } from './analytics';

useShellEvent(
  (shell) => shell.onSurfaceOpened,
  ({ id }) => analytics.track('panel', { id }),
);
</script>

<template>
  <!-- your toolbar, panels and pages -->
</template>
EventFires whenCarries
onSurfaceOpenedA panel or dialog openedid, props
onSurfaceClosedA panel or dialog closedid
onMenuOpenedA menu openedid
onMenuClosedA menu closedid

Good to know#

No document yet#

The panel composables work before a document is open: every panel reads as closed, and opening one does nothing until there is a document. So a toolbar outside <DocumentGate> can use them.

Was this page helpful?

Your feedback goes directly to the documentation team.