EmbedPDF

Tools, cursors & touch

At any moment one tool is active, and it decides what a click or a drag on the page does: select text, scroll, draw a rectangle, place a stamp. The interaction plugin keeps track of it, and every feature that uses the pointer works through it. Register interactionPlugin():

interaction-plugin.vue
<script setup lang="ts">
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { renderPlugin } from '@embedpdf/vue/render';
import { Viewer } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { engine } from './pdf';

const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];
</script>

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

Then switch tools from your toolbar:

tool-switch.vue
<script setup lang="ts">
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const interaction = useInteraction();
const { activeToolId } = useInteractionState();
</script>

<template>
  <button :aria-pressed="activeToolId === 'pointer'" @click="interaction.activateTool('pointer')">
    Select
  </button>
  <button :aria-pressed="activeToolId === 'pan'" @click="interaction.activateTool('pan')">
    Hand
  </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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { localEngine } from '@embedpdf/engine';
import ToolSwitch from './ToolSwitch.vue';

import '../switch.css';

const engine = localEngine();
// The document opens with the hand tool.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin({ defaultTool: 'pan' }),
  selectionPlugin(),
];

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>
      <ToolSwitch />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

The built-in tools#

ToolA drag on the pageAlso
pointerSelects text, or selects and moves annotationsFills forms, follows links. The default.
panScrolls, like Acrobat’s hand toolFills forms, follows links

Plugins add their own: the annotation plugin a tool per kind (square, highlight, note…), the form plugin a tool per field, redaction redact, measurements distance. They’re all switched the same way, with activateTool(id), and tools in State lists them.

Hold a tool for a moment#

pushTool() switches to a tool and remembers the one before; popTool() goes back. That’s how “hold Space to pan” works:

hold-space.vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useInteraction } from '@embedpdf/vue/interaction';

const interaction = useInteraction();

function down(event: KeyboardEvent) {
  if (event.code === 'Space' && !event.repeat) interaction.pushTool('pan');
}
function up(event: KeyboardEvent) {
  if (event.code === 'Space') interaction.popTool();
}

onMounted(() => {
  window.addEventListener('keydown', down);
  window.addEventListener('keyup', up);
});
onUnmounted(() => {
  window.removeEventListener('keydown', down);
  window.removeEventListener('keyup', up);
});
</script>

<template>
  <!-- your toolbar and pages -->
</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 { interactionPlugin } from '@embedpdf/vue/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { localEngine } from '@embedpdf/engine';
import ActiveTool from './ActiveTool.vue';
import HoldSpaceToPan from './HoldSpaceToPan.vue';

import '../hold-space.css';

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

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>
      <ActiveTool />
      <HoldSpaceToPan>
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SelectionLayer />
          </template>
        </Stage>
      </HoldSpaceToPan>
    </DocumentGate>
  </Viewer>
</template>

activateDefaultTool() goes back to the default tool, pointer unless you set another.

Your own tool#

A tool of your own gets the pointer on the page, in page coordinates:

const remove = interaction.registerTool({
  id: 'pin',
  cursor: 'copy',
  onPointerDown: ({ page, point }) => {
    addPin(page, point);
    return true; // this tool handled the click
  },
});
 
interaction.activateTool('pin');

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

import '../pin.css';

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

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>
      <PinBoard />
    </DocumentGate>
  </Viewer>
</template>
OptionWhat it is
idThe tool’s name, for activateTool(id)
cursorThe cursor over the page, any CSS cursor
onPointerDownA press on a page: page, point, modifiers. Return true to take the gesture
onPointerMove, onPointerUpThe rest of a gesture you took
onHoverThe pointer moving over a page, with no button pressed
touch'draw': one finger drives the tool; 'tap' (default): a tap is a click, a drag scrolls

registerTool() returns a function that removes the tool again. A tool you drag with takes the press and follows the gesture:

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

import '../ruler.css';

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

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

Cursors#

Each tool has a cursor. Over things the tool can act on, the viewer switches to the right one by itself: a move or resize cursor over an annotation, a text cursor over text, a hand over a link. You choose the tool’s own cursor, in three steps:

  • A CSS cursor: cursor: 'crosshair'.
  • An image: cursor: svgCursor({ svg: penIcon, hotspot: { x: 2, y: 22 } }). The SVG needs a width and a height, and hotspot is the point that clicks.
  • An image that changes, such as a pen in the current color: useToolCursor() from a component, which follows the values it reads.
pen-cursor.vue
<script setup lang="ts">
import { useToolCursor } from '@embedpdf/vue/interaction';
import { penIcon } from './icons';

const props = defineProps<{ color: string }>();

useToolCursor(() => ({
  toolId: 'ink',
  cursors: { crosshair: { svg: penIcon(props.color), hotspot: { x: 2, y: 22 } } },
}));
</script>

<template>
  <slot />
</template>

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

import '../cursor.css';

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

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

cursors replaces the tool’s cursors by name. Pass a function, and the cursor follows color; the tool’s own comes back when the component unmounts.

See The cursor for annotation tools.

Touch#

On a touch screen, the viewer behaves like a native app:

  • One finger scrolls, and two fingers pinch-zoom, with pointer and pan.
  • Drawing tools take one finger: it draws, and two fingers still scroll and zoom.
  • Click tools, such as placing a note, place with a tap, and a drag still scrolls.
  • Long-press selects a word, and handles grow the selection.

A tool of your own chooses with touch: 'draw' for tools you drag with, 'tap' for tools you click with.

Haptic feedback#

Register a feedback provider, and a long-press that lands on a word gives the small tick people know from native apps:

haptics.vue
<script setup lang="ts">
import { feedbackPlugin, interactionPlugin, vibrationFeedback } from '@embedpdf/vue/interaction';
import { Viewer } from '@embedpdf/vue/runtime';
import { engine } from './pdf';

const plugins = [/* … */ interactionPlugin(), feedbackPlugin({ provider: vibrationFeedback })];
</script>

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

It’s safe to leave in: where the browser can’t vibrate, iOS Safari included, nothing happens. In a native app shell, pass wkFeedback('yourHandlerName') to use the app’s own haptics.

Methods#

Everything useInteraction() gives you.

MethodWhat it does
activateTool(id)Makes a tool the active one. An id that isn’t registered is refused. The built-in tools
pushTool(id), popTool()Switches to a tool and remembers the one before; goes back to it. Hold a tool for a moment
activateDefaultTool()Goes back to the default tool
getDefaultToolId()The tool a document opens with, pointer unless you set another
registerTool(tool, { replace? })Adds a tool of your own, and returns a function that removes it. An id that’s taken is refused unless you pass replace: true. Your own tool
setToolCursor(id, cursors)Replaces a tool’s cursors by name while the app runs; null brings its own back. In a component, use useToolCursor(), which undoes it when the component goes. Cursors
getSettings(), updateSettings(changes), resetSettings()Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useInteractionSettings(). Settings

State#

Everything useInteractionState() 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
activeToolIdThe active tool’s idgetActiveToolId(), or getActiveTool() for the whole tool
toolsEvery tool you can switch to, in the order they were added, the built-in ones firstlistTools(), or getTool(id) and hasTool(id) for one

Settings#

Set them when you register the plugin, and change them while the app runs with interaction.updateSettings({ … }); interaction.resetSettings() goes back to what you registered, and useInteractionSettings() gives you the current settings.

SettingWhat it doesDefault
defaultToolThe tool that’s active when a document opens'pointer'
toolsYour own tools, registered from the startnone

Events#

tool-events.vue
<script setup lang="ts">
import { useInteractionEvent } from '@embedpdf/vue/interaction';
import { analytics } from './analytics';

useInteractionEvent(
  (interaction) => interaction.onToolChanged,
  ({ toolId }) => analytics.track('tool', { toolId }),
);
</script>

<template>
  <!-- your toolbar and pages -->
</template>
EventFires whenCarries
onToolChangedAnother tool became activetoolId, previousToolId
onGestureStartedA drag with a tool begantoolId, page, pointerType
onGestureEndedIt endedtoolId, page, pointerType
onGestureCancelledIt was cancelled, for example by a second fingertoolId, page, pointerType
onSettingsChangedThe settings changedsettings, changed

Good to know#

Each document has its own tool#

The active tool belongs to the document: switch tabs, and each document is in the tool you left it in.

Permissions#

Tools don’t check permissions themselves; the features behind them do. A drawing tool without permission to create draws nothing. Hide the buttons with each plugin’s checks, such as annotation.canCreate().

Was this page helpful?

Your feedback goes directly to the documentation team.