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():
<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:
<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 staysLoading 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:
<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):
<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#
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 EscapeopenMenus 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.
| Method | What 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 |
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.
| Field | What it is | Read once |
|---|---|---|
openSurfaces | The open panels and dialogs, each with its id, exclusive group, props and focusRequest | listOpenSurfaces(), or isOpen(id) and getSurface(id) for one |
openMenus | The open menus’ ids, the last one opened on top | listOpenMenus(), or isMenuOpen(id) for one |
Events#
<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>| Event | Fires when | Carries |
|---|---|---|
onSurfaceOpened | A panel or dialog opened | id, props |
onSurfaceClosed | A panel or dialog closed | id |
onMenuOpened | A menu opened | id |
onMenuClosed | A menu closed | id |
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.
Your feedback goes directly to the documentation team.