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 withInteraction():
import { Component } from '@angular/core';
import { withInteraction } from '@embedpdf/angular/interaction';
import { withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
@Component({
selector: 'app-document-viewer',
providers: [provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction())],
template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}Then switch tools from your toolbar:
import { Component, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
@Component({
selector: 'app-tool-switch',
template: `
<button
[attr.aria-pressed]="interaction.activeToolId() === 'pointer'"
(click)="interaction.activateTool('pointer')"
>
Select
</button>
<button
[attr.aria-pressed]="interaction.activeToolId() === 'pan'"
(click)="interaction.activateTool('pan')"
>
Hand
</button>
`,
})
export class ToolSwitch {
protected readonly interaction = inject(EpdfInteraction);
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';
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()) };
};
const LABELS: Record<string, string> = { pointer: 'Select', pan: 'Hand' };
const HINTS: Record<string, string> = {
pointer: 'A drag selects text',
pan: 'A drag scrolls the pages',
};
// A button for every tool you can switch to, the active one pressed.
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfSelectionLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
// The document opens with the hand tool.
withInteraction({ defaultTool: 'pan' }),
withSelection(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './switch.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<div class="segmented" role="group" aria-label="Tool">
@for (tool of interaction.tools(); track tool.id) {
<button
type="button"
class="segment"
[attr.aria-pressed]="tool.id === interaction.activeToolId()"
(click)="interaction.activateTool(tool.id)"
>
{{ labels[tool.id] ?? tool.id }}
</button>
}
</div>
<output class="readout">{{ hint() }}</output>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-selection-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly interaction = inject(EpdfInteraction);
protected readonly labels = LABELS;
protected readonly hint = computed(() => {
const activeToolId = this.interaction.activeToolId();
return activeToolId ? HINTS[activeToolId] : '';
});
}The built-in tools#
| Tool | A drag on the page | Also |
|---|---|---|
pointer | Selects text, or selects and moves annotations | Fills forms, follows links. The default. |
pan | Scrolls, like Acrobat’s hand tool | Fills 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:
import { Directive, inject } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
/** On your viewer's root element: `<div appHoldSpaceToPan>`. */
@Directive({
selector: '[appHoldSpaceToPan]',
host: {
'(window:keydown.space)': 'press($event)',
'(window:keyup.space)': 'interaction.popTool()',
},
})
export class HoldSpaceToPan {
protected readonly interaction = inject(EpdfInteraction);
protected press(event: KeyboardEvent) {
if (!event.repeat) this.interaction.pushTool('pan');
}
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
Directive,
ElementRef,
inject,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';
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()) };
};
// While Space is held over the pages, the hand tool is active; on release, the tool from before.
@Directive({
selector: '[demoHoldSpaceToPan]',
host: {
'(window:keydown)': 'down($event)',
'(window:keyup)': 'up($event)',
},
})
export class HoldSpaceToPan {
private readonly interaction = inject(EpdfInteraction);
// Only over the pages, so Space still scrolls the rest of your page.
private readonly viewer = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
private held = false;
protected down(event: KeyboardEvent) {
if (event.code !== 'Space' || !this.viewer.matches(':hover')) return;
event.preventDefault();
if (event.repeat || this.held) return;
this.held = true;
this.interaction.pushTool('pan');
}
protected up(event: KeyboardEvent) {
if (event.code !== 'Space' || !this.held) return;
this.held = false;
this.interaction.popTool();
}
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfSelectionLayer,
HoldSpaceToPan,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
withSelection(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './hold-space.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<span class="badge" [attr.data-active]="panning()">{{ panning() ? 'Hand' : 'Select' }}</span>
<span class="readout">
{{
panning() ? 'Drag to scroll, then let go of Space' : 'Hold Space over the pages to scroll'
}}
</span>
</div>
<div class="viewer" demoHoldSpaceToPan>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-selection-layer />
</ng-template>
</epdf-stage>
</div>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly interaction = inject(EpdfInteraction);
protected readonly panning = computed(() => this.interaction.activeToolId() === 'pan');
}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…
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
input,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocumentGate,
injectPage,
pageRefsEqual,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, svgCursor, withInteraction } from '@embedpdf/angular/interaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
interface Pin {
readonly id: number;
readonly page: PageRef;
readonly point: { readonly x: number; readonly y: number };
}
// The cursor is the pin itself; its tip is the point that clicks.
const PIN_CURSOR = svgCursor({
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M12 23s7.5-7.4 7.5-12.5a7.5 7.5 0 0 0-15 0C4.5 15.6 12 23 12 23z" fill="#e5484d" stroke="#fff" stroke-width="1.5"/>
<circle cx="12" cy="10.5" r="2.8" fill="#fff"/>
</svg>`,
hotspot: { x: 12, y: 23 },
fallback: 'copy',
});
// The pins on one page, placed in its pixels at the last moment.
@Component({
selector: 'demo-pin-layer',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (pin of onThisPage(); track pin.id) {
<span class="pin" [style.left.px]="pixels(pin).x" [style.top.px]="pixels(pin).y">{{
pin.id
}}</span>
}
`,
})
export class PinLayer {
readonly pins = input.required<readonly Pin[]>();
private readonly page = injectPage('<demo-pin-layer>');
protected readonly onThisPage = computed(() =>
this.pins().filter((pin) => pageRefsEqual(pin.page, this.page.ref)),
);
protected pixels(pin: Pin) {
return this.page.transform().toPixels(pin.point);
}
}
@Component({
selector: 'demo-pin-board',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, PinLayer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="toolbar">
<div class="segmented" role="group" aria-label="Tool">
<button
type="button"
class="segment"
[attr.aria-pressed]="interaction.activeToolId() === 'pin'"
(click)="interaction.activateTool('pin')"
>
Pin
</button>
<button
type="button"
class="segment"
[attr.aria-pressed]="interaction.activeToolId() === 'pan'"
(click)="interaction.activateTool('pan')"
>
Hand
</button>
</div>
<button type="button" class="button" [disabled]="pins().length === 0" (click)="pins.set([])">
Clear
</button>
<output class="readout">
{{ pins().length === 0 ? 'Click a page to drop a pin' : pins().length + ' pins' }}
</output>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<demo-pin-layer [pins]="pins()" />
</ng-template>
</epdf-stage>
`,
})
export class PinBoard {
protected readonly interaction = inject(EpdfInteraction);
protected readonly pins = signal<readonly Pin[]>([]);
constructor() {
// A tool of your own: a click on a page drops a pin there, in page coordinates.
const remove = this.interaction.registerTool({
id: 'pin',
cursor: PIN_CURSOR,
onPointerDown: ({ page, point }) => {
this.pins.update((current) => [...current, { id: current.length + 1, page, point }]);
return true; // this tool handled the click
},
});
this.interaction.activateTool('pin');
inject(DestroyRef).onDestroy(remove);
}
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, PinBoard],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './pin.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-pin-board *epdfDocumentGate="let document; fallback: loading" />
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {}| Option | What it is |
|---|---|
id | The tool’s name, for activateTool(id) |
cursor | The cursor over the page, any CSS cursor |
onPointerDown | A press on a page: page, point, modifiers. Return true to take the gesture |
onPointerMove, onPointerUp | The rest of a gesture you took |
onHover | The 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…
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
input,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocumentGate,
injectPage,
pageRefsEqual,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
interface Point {
readonly x: number;
readonly y: number;
}
interface Measurement {
readonly page: PageRef;
readonly from: Point;
readonly to: Point;
}
// Page coordinates are points, 72 to the inch.
const lengthOf = ({ from, to }: Measurement) => {
const inches = Math.hypot(to.x - from.x, to.y - from.y) / 72;
return `${inches.toFixed(2)} in · ${(inches * 2.54).toFixed(1)} cm`;
};
// The measured line on its page, placed in the page's pixels.
@Component({
selector: 'demo-ruler-layer',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (line(); as line) {
<svg class="ruler-layer">
<line class="ruler-line" [attr.x1]="line.from.x" [attr.y1]="line.from.y" [attr.x2]="line.to.x" [attr.y2]="line.to.y" />
<circle class="ruler-end" [attr.cx]="line.from.x" [attr.cy]="line.from.y" r="4" />
<circle class="ruler-end" [attr.cx]="line.to.x" [attr.cy]="line.to.y" r="4" />
</svg>
<span
class="ruler-label"
[style.left.px]="(line.from.x + line.to.x) / 2"
[style.top.px]="(line.from.y + line.to.y) / 2"
>{{ line.length }}</span>
}
`,
})
export class RulerLayer {
readonly measurement = input.required<Measurement | null>();
private readonly page = injectPage('<demo-ruler-layer>');
protected readonly line = computed(() => {
const measurement = this.measurement();
if (!measurement || !pageRefsEqual(measurement.page, this.page.ref)) return null;
const transform = this.page.transform();
return {
from: transform.toPixels(measurement.from),
to: transform.toPixels(measurement.to),
length: lengthOf(measurement),
};
});
}
@Component({
selector: 'demo-ruler',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, RulerLayer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="toolbar">
<output class="readout">{{ readout() }}</output>
<output class="readout muted">
@if (pointer(); as pointer) {
x {{ round(pointer.x) }} · y {{ round(pointer.y) }} pt
}
</output>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<demo-ruler-layer [measurement]="measurement()" />
</ng-template>
</epdf-stage>
`,
})
export class Ruler {
protected readonly measurement = signal<Measurement | null>(null);
protected readonly pointer = signal<Point | null>(null);
protected readonly readout = computed(() => {
const measurement = this.measurement();
return measurement ? lengthOf(measurement) : 'Drag on a page to measure';
});
protected readonly round = Math.round;
constructor() {
const interaction = inject(EpdfInteraction);
// A tool you drag with: the press starts a line, the moves stretch it, the release ends it.
const remove = interaction.registerTool({
id: 'ruler',
cursor: 'crosshair',
touch: 'draw', // one finger measures, two fingers scroll and zoom
onPointerDown: ({ page, point }) => {
this.measurement.set({ page, from: point, to: point });
return true;
},
onPointerMove: ({ point }) => {
this.measurement.update((current) => current && { ...current, to: point });
},
onPointerUp: ({ point }) => {
this.measurement.update((current) => current && { ...current, to: point });
},
onHover: ({ point }) => this.pointer.set(point),
});
interaction.activateTool('ruler');
inject(DestroyRef).onDestroy(remove);
}
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, Ruler],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './ruler.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-ruler *epdfDocumentGate="let document; fallback: loading" />
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {}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 awidthand aheight, andhotspotis the point that clicks. - An image that changes, such as a pen in the current color:
interaction.overrideCursor()from a component, which follows the values it reads.
import { Directive, inject, input } from '@angular/core';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
import { penIcon } from './icons';
/** `<epdf-stage [appPenCursor]="color()">` */
@Directive({ selector: '[appPenCursor]' })
export class PenCursor {
readonly color = input.required<string>({ alias: 'appPenCursor' });
constructor() {
// Follows the color, and gives the tool its own cursor back when the directive goes
inject(EpdfInteraction).overrideCursor(() => ({
toolId: 'ink',
cursors: { crosshair: { svg: penIcon(this.color()), hotspot: { x: 2, y: 22 } } },
}));
}
}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
input,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocumentGate,
injectPage,
pageRefsEqual,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { localEngine } from '@embedpdf/engine';
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()) };
};
const COLORS = ['#e5484d', '#2f80ed', '#30a46c', '#1a2748'];
// A pen in the current color, with its tip at the bottom left.
const penIcon = (color: string) =>
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M3 21l1.6-5.6L16.2 3.8a2.2 2.2 0 0 1 3.1 3.1L7.7 18.5z" fill="${color}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>
</svg>`;
interface Dot {
readonly id: number;
readonly page: PageRef;
readonly point: { readonly x: number; readonly y: number };
readonly color: string;
}
// While this is there, the dot tool's crosshair is a pen in `color`.
@Component({
selector: 'demo-pen-cursor',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
})
export class PenCursor {
readonly color = input.required<string>();
constructor() {
inject(EpdfInteraction).overrideCursor(() => ({
toolId: 'dot',
cursors: { crosshair: { svg: penIcon(this.color()), hotspot: { x: 3, y: 21 } } },
}));
}
}
@Component({
selector: 'demo-dot-layer',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (dot of onThisPage(); track dot.id) {
<span
class="dot"
[style.left.px]="pixels(dot).x"
[style.top.px]="pixels(dot).y"
[style.background]="dot.color"
></span>
}
`,
})
export class DotLayer {
readonly dots = input.required<readonly Dot[]>();
private readonly page = injectPage('<demo-dot-layer>');
protected readonly onThisPage = computed(() =>
this.dots().filter((dot) => pageRefsEqual(dot.page, this.page.ref)),
);
protected pixels(dot: Dot) {
return this.page.transform().toPixels(dot.point);
}
}
@Component({
selector: 'demo-dots',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, PenCursor, DotLayer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (penCursor()) {
<demo-pen-cursor [color]="color()" />
}
<div class="toolbar">
<div class="swatches" role="radiogroup" aria-label="Color">
@for (swatch of colors; track swatch) {
<button
type="button"
role="radio"
[attr.aria-checked]="swatch === color()"
[attr.aria-label]="swatch"
class="swatch"
[style.background]="swatch"
(click)="color.set(swatch)"
></button>
}
</div>
<label class="check">
<input #check type="checkbox" [checked]="penCursor()" (change)="penCursor.set(check.checked)" />
Pen cursor
</label>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<demo-dot-layer [dots]="dots()" />
</ng-template>
</epdf-stage>
`,
})
export class Dots {
protected readonly colors = COLORS;
protected readonly color = signal(COLORS[0]);
protected readonly penCursor = signal(true);
protected readonly dots = signal<readonly Dot[]>([]);
constructor() {
const interaction = inject(EpdfInteraction);
const remove = interaction.registerTool({
id: 'dot',
cursor: 'crosshair',
// The tool reads the color at the moment of the click.
onPointerDown: ({ page, point }) => {
this.dots.update((current) => [
...current,
{ id: current.length + 1, page, point, color: this.color() },
]);
return true;
},
});
interaction.activateTool('dot');
inject(DestroyRef).onDestroy(remove);
}
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, Dots],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './cursor.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-dots *epdfDocumentGate="let document; fallback: loading" />
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {}overrideCursor() runs again when color() changes, and brings the tool’s own cursors back when
the directive is destroyed.
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
pointerandpan. - 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:
import { Component } from '@angular/core';
import { vibrationFeedback, withFeedback, withInteraction } from '@embedpdf/angular/interaction';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { engine } from './pdf';
@Component({
selector: 'app-document-viewer',
providers: [
provideEmbedPdf(
{ engine },
/* … */
withInteraction(),
withFeedback({ provider: vibrationFeedback }),
),
],
template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}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 inject(EpdfInteraction) gives you.
| Method | What 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 interaction.overrideCursor(), 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 inject(EpdfInteraction).settings(). Settings |
State#
Every value is a signal on inject(EpdfInteraction): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
activeToolId() | The active tool’s id | getActiveToolId(), or getActiveTool() for the whole tool |
tools() | Every tool you can switch to, in the order they were added, the built-in ones first | listTools(), 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
inject(EpdfInteraction).settings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
defaultTool | The tool that’s active when a document opens | 'pointer' |
tools | Your own tools, registered from the start | none |
Events#
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfInteraction } from '@embedpdf/angular/interaction';
import { analytics } from './analytics';
@Component({
selector: 'app-viewer-layout',
template: `<!-- your toolbar and pages -->`,
})
export class ViewerLayout {
private readonly interaction = inject(EpdfInteraction);
constructor() {
this.interaction.toolChanged$
.pipe(takeUntilDestroyed())
.subscribe(({ toolId }) => analytics.track('tool', { toolId }));
}
}| Event | Fires when | Carries |
|---|---|---|
toolChanged$ | Another tool became active | toolId, previousToolId |
gestureStarted$ | A drag with a tool began | toolId, page, pointerType |
gestureEnded$ | It ended | toolId, page, pointerType |
gestureCancelled$ | It was cancelled, for example by a second finger | toolId, page, pointerType |
settingsChanged$ | The settings changed | settings, 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().
Your feedback goes directly to the documentation team.