---
title: "Selecting & copying — Angular"
description: "Let people select text with the pointer or a finger, select text from code, copy it, and show a menu next to the selection."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/text/selection"
---

# Selecting & copying

The selection plugin lets people select text with the mouse or a finger, and lets your code select the
same text. Register `withInteraction()` and `withSelection()`, and put `<epdf-selection-layer>`
above the page:

**`basic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelection, 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()) };
};

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfSelectionLayer],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './basic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <epdf-stage *epdfDocumentGate="let document; fallback: loading" class="stage">
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
      </ng-template>
    </epdf-stage>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // Something to see on load: the title on the cover, selected from code.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 10, count: 52 });
    });
  }
}
```

**`basic.css`**

```css
/* Colors */
:root {
  --muted: #5a6b92;
  --line: #e6eaf2;
  --canvas: #f7f9fc;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The pages */
.stage {
  height: 460px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** the cover's title starts out selected from code. Drag over text, double-click a word,
> or triple-click a line. A selection can run from one page onto the next. On a touch screen,
> long-press a word and drag the handles.

`withInteraction()` is the input system every tool shares: the Stage passes the pointer to it,
the selection plugin handles it, and `<epdf-selection-layer>` draws the result. So you can restyle or
replace the highlight without touching how selecting works.

Selecting uses where the characters are on the page, not the text itself. Nothing reads the page's
text until your code asks for it, and a document can allow selecting while it forbids copying (see
[Permissions](#permissions)).

## Select text from code

`inject(EpdfSelection)` does from code what the pointer does, and the highlight follows:

**`programmatic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelection, EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';

// On the cover: the characters of its title, and a point on its word "Viewers", in page coordinates.
const TITLE = { start: 10, count: 52 };
const POINT = { x: 260, y: 242 };

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()) };
};

// Every button selects on the cover, the first page: by its ref, or by its index, 0.
@Component({
  selector: 'demo-selection-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button
        type="button"
        class="button"
        [disabled]="!selection.canSelect() || !cover()"
        (click)="selectTitle()"
      >
        Title
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!selection.canSelect()"
        (click)="selection.selectWordAt(0, point)"
      >
        Word
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!selection.canSelect()"
        (click)="selection.selectLineAt(0, point)"
      >
        Line
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!selection.canSelect()"
        (click)="selection.selectPage(0)"
      >
        Page
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!selection.canSelect()"
        (click)="selection.selectAll()"
      >
        Everything
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!selection.hasSelection()"
        (click)="selection.clear()"
      >
        Clear
      </button>
      <output class="badge">{{ summary() }}</output>
    </div>
  `,
})
export class SelectionToolbar {
  protected readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);
  protected readonly cover = computed(() => this.document.pages()[0]?.ref);
  protected readonly point = POINT;

  protected readonly summary = computed(() => {
    const pages = this.selection.pages();
    const range = this.selection.range();
    if (pages.length > 1) return `On ${pages.length} pages`;
    if (range) return `${range.end.index - range.start.index} characters`;
    return 'Nothing selected';
  });

  constructor() {
    // The title is selected on load.
    effect(() => {
      if (this.cover()) this.selectTitle();
    });
  }

  protected selectTitle() {
    const cover = this.cover();
    if (cover) this.selection.select({ page: cover, ...TITLE });
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    SelectionToolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './programmatic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-selection-toolbar />
      <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 {}
```

**`programmatic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The buttons, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* What is selected now, pushed to the right while there's room */
.badge {
  margin-left: auto;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 11px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

```ts
selection.select({ page: page.ref, start: 20, count: 40 }); // 40 characters on one page
selection.selectWordAt(page.ref, { x: 120, y: 80 }); // the word at a point
selection.selectLineAt(page.ref, { x: 120, y: 80 }); // the line at a point
selection.selectPage(page.ref);
selection.selectAll();
selection.clear();
```

A search result is already a range, so selecting it takes no conversion:

```ts
selection.select(hit); // { page, start, count }
```

For a selection across pages, give where it starts and where it ends:

```ts
selection.select({
  start: { page: firstPage.ref, index: 120 },
  end: { page: lastPage.ref, index: 35 },
});
```

Ranges include `start` and stop before `end`, and they count characters as the page stores them,
not JavaScript string positions. `selection.extendTo(page.ref, point)` grows the current selection
to a point on a page, as dragging a handle does. Every method that takes a page takes its ref or its
index: `selection.selectPage(0)` selects the first page.

## Show the selection in your UI

`inject(EpdfSelection)` gives you what to show, and your component updates when it
changes:

**`highlight-button.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfSelection } from '@embedpdf/angular/selection';

@Component({
  selector: 'app-highlight-button',
  template: `<button [disabled]="!selection.hasSelection()">Highlight</button>`,
})
export class HighlightButton {
  // selection.hasSelection(), selection.isSelecting(), selection.pages()
  protected readonly selection = inject(EpdfSelection);
}
```

**`state.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfSelection, 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()) };
};

// Every field of the selection's state, as it changes.
@Component({
  selector: 'demo-selection-status',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <dl class="status">
      <div class="field">
        <dt>hasSelection</dt>
        <dd>{{ selection.hasSelection() }}</dd>
      </div>
      <div class="field" [attr.data-live]="selection.isSelecting()">
        <dt>isSelecting</dt>
        <dd>{{ selection.isSelecting() }}</dd>
      </div>
      <div class="field">
        <dt>pages</dt>
        <dd>
          {{ selection.pages().length === 1 ? '1 page' : selection.pages().length + ' pages' }}
        </dd>
      </div>
      <div class="field">
        <dt>range</dt>
        <dd>
          @if (selection.range(); as range) {
            {{ range.start.index }} → {{ range.end.index }}
          } @else {
            null
          }
        </dd>
      </div>
    </dl>
  `,
})
export class SelectionStatus {
  protected readonly selection = inject(EpdfSelection);
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    SelectionStatus,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './state.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-selection-status />
      <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 {
  private readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // Something selected on load: the title on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 10, count: 52 });
    });
  }
}
```

**`state.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --live: #0876fd;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The state, one field per tile: four in a row, two by two on a phone */
.status {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 8px;
  margin: 0 0 14px;
}

.field {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 11px;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    border-color 0.15s ease,
    background-color 0.15s ease;
}

/* isSelecting lights up while the pointer is down */
.field[data-live='true'] {
  background: var(--tint);
  border-color: var(--live);
}

.field dt {
  font-family: ui-monospace, monospace;
  font-size: 11.5px;
  color: var(--muted);
}

.field dd {
  margin: 3px 0 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** drag over text. `isSelecting` is `true` until you let go, and `pages` counts the pages
> the selection reaches.

Every field is listed under [State](#state). For drawing your own highlight, `selection.listSegments(page.ref)` gives you each selected line as
four corners, turned with the text, and `selection.listRects(page.ref)` a plain box per line.

## Copy the selected text

Reading the text and using the clipboard are two steps:

```ts
const text = await selection.readText();
```

`readText()` reads only the selected characters and gives them to you; it doesn't touch the
clipboard. Lines on different pages are joined with a new line, and with nothing selected it's an
empty string. `readTextInRange(range)` reads any range without selecting it.

For the clipboard, the Angular package has two helpers. `copySelection()` copies from your own
button:

**`copy-button.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfSelection, copySelection } from '@embedpdf/angular/selection';

@Component({
  selector: 'app-copy-button',
  template: `
    <button [disabled]="!selection.hasSelection() || !selection.canCopy()" (click)="copy()">
      Copy
    </button>
  `,
})
export class CopyButton {
  protected readonly selection = inject(EpdfSelection);

  protected copy() {
    void copySelection(this.selection);
  }
}
```

`<epdf-selection-clipboard>` makes Ctrl+C and the browser's Copy menu work. Mount it once per document view:

**`clipboard.ts`**

```typescript
import { Component } from '@angular/core';
import { EpdfSelectionClipboard } from '@embedpdf/angular/selection';

@Component({
  selector: 'app-clipboard',
  imports: [EpdfSelectionClipboard],
  // Once per document view: Ctrl+C / Cmd+C and the browser's Copy menu
  template: `<epdf-selection-clipboard />`,
})
export class Clipboard {}
```

It reads the text as soon as a selection settles, so the browser's copy can answer at once. Set
`prefetch` to `false` to read text only when someone copies. A selection the user [may not
copy](../concepts/permissions) isn't copied by the keys or by the browser's Copy menu, and
`(refused)` fires,
so you can say so.

With more than one viewer on a page, put the `epdfSelectionClipboard`
directive on the element around one (`<div tabindex="0" epdfSelectionClipboard>`) in place of
`<epdf-selection-clipboard />`: Ctrl+C and the Copy menu then copy its selection only while focus is
inside it, and the page keeps its own copying everywhere else.

**`copy.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  copySelection,
  EpdfSelection,
  EpdfSelectionClipboard,
  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()) };
};

@Component({
  selector: 'demo-copy-button',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      <button
        type="button"
        class="button"
        [disabled]="!selection.hasSelection() || !selection.canCopy()"
        (click)="copy()"
      >
        Copy
      </button>
      <output class="readout">{{ status() || 'Or press Ctrl+C' }}</output>
    </div>
  `,
})
export class CopyButton {
  protected readonly selection = inject(EpdfSelection);
  protected readonly status = signal('');

  protected copy() {
    copySelection(this.selection).then(
      (text) => this.status.set(`Copied ${text.length} characters`),
      // The browser can refuse the clipboard, for example in a frame that doesn't allow it.
      () => this.status.set("The browser didn't allow copying"),
    );
  }
}

// The selected text, read with readText() each time the selection settles.
@Component({
  selector: 'demo-selected-text',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p class="preview">{{ text() || 'Select some text to read it here.' }}</p>`,
})
export class SelectedText {
  private readonly selection = inject(EpdfSelection);
  protected readonly text = signal('');

  constructor() {
    effect((onCleanup) => {
      this.selection.range();
      if (this.selection.isSelecting() || !this.selection.canCopy()) return;
      // A newer selection cancels a read that hasn't finished.
      const controller = new AbortController();
      this.selection.readText({ signal: controller.signal }).then(
        (text) => this.text.set(text),
        () => {},
      );
      onCleanup(() => controller.abort());
    });
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfSelectionClipboard,
    CopyButton,
    SelectedText,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './copy.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <epdf-selection-clipboard />
      <demo-copy-button />
      <demo-selected-text />
      <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 {
  private readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // Something selected on load: the title on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 10, count: 52 });
    });
  }
}
```

**`copy.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The copy button, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 14px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The text readText() gave, kept to two lines */
.preview {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin: 0 0 14px;
  min-height: 2.9em;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
  background: var(--tint);
  border-left: 3px solid var(--line-strong);
  border-radius: 6px;
  padding: 8px 12px;
}

/* The pages */
.stage {
  height: 400px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** select other text: the line above the pages shows what `readText()` reads once the
> selection settles. Then press **Copy**, or Ctrl+C.

## Add a selection menu

`<epdf-selection-menu>` places your own UI next to the selection. It hides while the user drags and
appears when the selection settles:

**`menu.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  copySelection,
  EpdfSelection,
  EpdfSelectionClipboard,
  EpdfSelectionLayer,
  EpdfSelectionMenu,
  withSelection,
} from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';

const PLACEMENTS = ['top', 'bottom', 'left', 'right'] as const;
type Placement = (typeof PLACEMENTS)[number];

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()) };
};

// What's in the menu is yours: here, copy and clear.
@Component({
  selector: 'demo-selection-actions',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="menu">
      @if (selection.canCopy()) {
        <button type="button" class="button" (click)="copy()">Copy</button>
      }
      <button type="button" class="button" (click)="selection.clear()">Clear</button>
    </div>
  `,
})
export class SelectionActions {
  protected readonly selection = inject(EpdfSelection);

  protected copy() {
    void copySelection(this.selection).catch(() => {
      // Show your product's clipboard error message here.
    });
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfSelectionMenu,
    EpdfSelectionClipboard,
    SelectionActions,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './menu.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <epdf-selection-clipboard />
      <div class="toolbar" role="group" aria-label="Where the menu goes">
        @for (name of placements; track name) {
          <button
            type="button"
            class="segment"
            [attr.aria-pressed]="placement() === name"
            (click)="placement.set(name)"
          >
            {{ name }}
          </button>
        }
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
        </ng-template>
        <epdf-selection-menu [placement]="placement()" [gap]="8">
          <demo-selection-actions />
        </epdf-selection-menu>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly placements = PLACEMENTS;
  protected readonly placement = signal<Placement>('top');
  private readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // Something selected on load, so the menu shows: the title on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 10, count: 52 });
    });
  }
}
```

**`menu.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* Where the menu goes, above the pages */
.toolbar {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  margin-bottom: 14px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.segment {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  text-transform: capitalize;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 7px 13px;
  cursor: pointer;
}

.segment:hover {
  color: var(--ink);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 3px rgb(7 32 76 / 0.12);
}

.segment:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* The menu that floats next to a selection */
.menu {
  display: flex;
  gap: 6px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgb(7 32 76 / 0.18);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The pages */
.stage {
  height: 460px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** select other text, and the menu follows it. Pick where it goes above the pages.

Mount it inside
`<epdf-stage>`, next to the page template, and it stays attached to the text while the user
scrolls and zooms. The menu only places things; what's in it is yours: copy, comment, highlight, redact. `placement`
is `'top'`, `'bottom'`, `'left'` or `'right'`, and `gap` the space in pixels.

## On touch screens

A finger has no text cursor, so selection works as it does elsewhere on a phone: **long-press a word,
then drag the handles.** Add `<epdf-selection-handles>` next to your menu:

**`selection-handles.ts`**

```typescript
import { Component } from '@angular/core';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import {
  EpdfSelectionHandles,
  EpdfSelectionLayer,
  EpdfSelectionMenu,
} from '@embedpdf/angular/selection';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';

import { CopyButton } from './copy-button';

@Component({
  selector: 'app-pages',
  imports: [
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfSelectionMenu,
    EpdfSelectionHandles,
    CopyButton,
  ],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
      </ng-template>

      <epdf-selection-menu>
        <app-copy-button />
      </epdf-selection-menu>
      <epdf-selection-handles />
    </epdf-stage>
  `,
})
export class Pages {}
```

**`handles.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  copySelection,
  EpdfSelection,
  EpdfSelectionHandles,
  EpdfSelectionLayer,
  EpdfSelectionMenu,
  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()) };
};

@Component({
  selector: 'demo-copy-button',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button type="button" class="button" [disabled]="!selection.canCopy()" (click)="copy()">
      Copy
    </button>
  `,
})
export class CopyButton {
  protected readonly selection = inject(EpdfSelection);

  protected copy() {
    void copySelection(this.selection).catch(() => {});
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfSelectionMenu,
    EpdfSelectionHandles,
    CopyButton,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './handles.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <epdf-stage *epdfDocumentGate="let document; fallback: loading" class="stage">
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
      </ng-template>
      <!-- Clear of the start handle's grip, which sits above the first line. -->
      <epdf-selection-menu [gap]="20">
        <demo-copy-button />
      </epdf-selection-menu>
      <epdf-selection-handles />
    </epdf-stage>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // A word selected on load, as a long-press selects one: "Viewers", on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 14, count: 7 });
    });
  }
}
```

**`handles.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The menu's one button, floating above the selection */
.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 9px 15px;
  cursor: pointer;
  box-shadow: 0 8px 24px rgb(7 32 76 / 0.18);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The pages */
.stage {
  height: 460px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** drag a handle's round grip, with a finger or the mouse: the selection grows or shrinks
> from the other end.

Each handle is the selection's own edge, with a round grip. Dragging one grows the selection from the
other end, across pages too. Grabbing a handle never scrolls the page, and everywhere else a finger
still scrolls and pinch-zooms. For the small tick a native app gives on long-press, see
[haptic feedback](../ui/tools#haptic-feedback).

## Methods

Everything `inject(EpdfSelection)` gives you.

| Method                                                        | What it does                                                                                                                                                            |
| :------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `select(range)`                                               | Selects `{ page, start, count }` on one page, or `{ start, end }` across pages. An empty range clears. [Select text from code](#select-text-from-code)                  |
| `selectWordAt(page, point)`                                   | Selects the word at a point, like a double-click. Returns `false` when there's no text there.                                                                           |
| `selectLineAt(page, point)`                                   | Selects the line at a point, like a triple-click. Returns `false` when there's no text there.                                                                           |
| `selectPage(page)`                                            | Selects every character on a page                                                                                                                                       |
| `selectAll()`                                                 | Selects the whole document                                                                                                                                              |
| `extendTo(page, point)`                                       | Grows the selection to a point, as dragging a handle does                                                                                                               |
| `clear()`                                                     | Clears the selection. Always allowed.                                                                                                                                   |
| `readText()`                                                  | The selected text, or `''` with nothing selected. Needs permission to copy. [Copy the selected text](#copy-the-selected-text)                                           |
| `readTextInRange(range)`                                      | The text of any range, without selecting it                                                                                                                             |
| `listSegments(page)`                                          | Each selected line on a page, as four corners turned with the text                                                                                                      |
| `listRects(page)`                                             | A plain box around each selected line on a page                                                                                                                         |
| `getAnchor()`                                                 | Where a menu attaches: `{ page, bounds }` on the page where the selection ends, or `null`. `<epdf-selection-menu>` uses it.                                             |
| `getSnapshot()`                                               | Everything at once: each page's lines, both ends with their direction, and the range. For drawing your own highlight and handles.                                       |
| `canSelect()`, `canCopy()`                                    | Whether selecting, and reading the selected text, are allowed. [Permissions](#permissions)                                                                              |
| `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(EpdfSelection).settings()`. [Settings](#settings) |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Every value is a signal on `inject(EpdfSelection)`: 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             |
| :--------------- | :--------------------------------------------------------------------------------- | :-------------------- |
| `hasSelection()` | Whether anything is selected                                                       | `hasSelection()`      |
| `isSelecting()`  | Whether the user is still dragging                                                 | `isSelecting()`       |
| `range()`        | The selection as `{ start, end }`, to store and pass back to `select()`, or `null` | `getRange()`          |
| `pages()`        | The pages the selection is on, in document order                                   | `listSelectedPages()` |

## Settings

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

| Setting          | What it does                                                | Default                         | From CSS                              |
| :--------------- | :---------------------------------------------------------- | :------------------------------ | :------------------------------------ |
| `dragThreshold`  | How far, in pixels, the pointer moves before a drag selects | `4`                             | —                                     |
| `color`          | The color of selected text                                  | the viewer's accent at 35%      | `--epdf-text-selection`               |
| `handles.color`  | The grips at its ends, on touch screens                     | the viewer's accent             | `--epdf-text-selection-handle`        |
| `handles.shadow` | Their shadow                                                | `'0 1px 4px rgb(0 0 0 / 0.35)'` | `--epdf-text-selection-handle-shadow` |

A CSS variable wins over the setting, for dark mode or a different look per viewer: see
[Colors & theming](../ui/theming).

**`colors.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, 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 { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfSelection,
  EpdfSelectionHandles,
  EpdfSelectionLayer,
  withSelection,
} from '@embedpdf/angular/selection';
import { localEngine } from '@embedpdf/engine';

// Each: the selected text and its handles. `null` is the viewer's accent: at 35% for the text.
const COLORS = [
  { name: 'Accent', color: null, handles: null, swatch: '#3858e9' },
  { name: 'Yellow', color: 'rgb(250 204 21 / 0.45)', handles: '#ca8a04', swatch: '#facc15' },
  { name: 'Green', color: 'rgb(34 197 94 / 0.35)', handles: '#16a34a', swatch: '#22c55e' },
  { name: 'Pink', color: 'rgb(236 72 153 / 0.3)', handles: '#db2777', swatch: '#ec4899' },
];

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()) };
};

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfSelectionHandles,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection({ handles: { shadow: '0 1px 3px rgb(0 0 0 / 0.3)' } }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './colors.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        @for (choice of colors; track choice.name) {
          <button
            type="button"
            class="swatch"
            [attr.aria-label]="choice.name"
            [attr.aria-pressed]="selection.settings().color === choice.color"
            [style.background]="choice.swatch"
            (click)="
              selection.updateSettings({ color: choice.color, handles: { color: choice.handles } })
            "
          ></button>
        }
        <button type="button" class="button" (click)="selection.resetSettings()">Reset</button>
        <label class="option">
          <input
            #override
            type="checkbox"
            [checked]="fromCss()"
            (change)="fromCss.set(override.checked)"
          />
          Override with CSS
        </label>
      </div>
      <epdf-stage class="stage" [class.from-css]="fromCss()">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
        </ng-template>
        <epdf-selection-handles />
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly colors = COLORS;
  protected readonly fromCss = signal(false);
  protected readonly selection = inject(EpdfSelection);
  private readonly document = inject(EpdfDocument);

  constructor() {
    // Something selected on load: the title on the cover.
    effect(() => {
      const cover = this.document.pages()[0]?.ref;
      if (cover) this.selection.select({ page: cover, start: 10, count: 52 });
    });
  }
}
```

**`colors.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The colors to pick from, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.swatch {
  width: 32px;
  height: 32px;
  border: 2px solid var(--surface);
  border-radius: 50%;
  padding: 0;
  cursor: pointer;
  box-shadow: 0 0 0 1px var(--line);
  transition: box-shadow 0.15s ease;
}

.swatch:hover {
  box-shadow: 0 0 0 1px var(--focus);
}

.swatch[aria-pressed='true'] {
  box-shadow: 0 0 0 2px var(--accent);
}

.swatch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 9px 13px;
  cursor: pointer;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A checkbox in a pill that lights up while it's on */
.option {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-left: auto;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 11px;
  cursor: pointer;
  user-select: none;
  box-shadow: 0 1px 2px var(--shadow);
}

.option:has(input:checked) {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--focus);
}

.option input {
  margin: 0;
  accent-color: #0876fd;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

/* CSS wins over the settings: an orange selection, whichever color is picked */
.stage.from-css {
  --epdf-text-selection: rgb(249 115 22 / 0.35);
  --epdf-text-selection-handle: #ea580c;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** pick a color, and `selection.updateSettings({ color, handles })` changes the selection
> and its handles at once. Then turn on **Override with CSS**: the CSS variables win over the color
> you picked.

## Events

**`selection-committed.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfSelection } from '@embedpdf/angular/selection';

import { prefetchTranslation } from './translation';

@Component({
  selector: 'app-translation-prefetch',
  template: '',
})
export class TranslationPrefetch {
  constructor() {
    inject(EpdfSelection)
      .committed$.pipe(takeUntilDestroyed())
      .subscribe(() => prefetchTranslation());
  }
}
```

| Event              | Fires when                                | Carries               |
| :----------------- | :---------------------------------------- | :-------------------- |
| `changed$`         | The selection changed, while dragging too | `range`, `pages`      |
| `committed$`       | A drag or a tap finished selecting        | `range`               |
| `cleared$`         | The selection was cleared                 | —                     |
| `settingsChanged$` | The settings changed                      | `settings`, `changed` |

Use the change event for things that follow the selection as it grows, and the commit event for
things that should happen once, such as creating a highlight.

## Permissions

Selecting and copying are two permissions:

| Permission        | What it allows                              | Check                   |
| :---------------- | :------------------------------------------ | :---------------------- |
| `doc.text.select` | Selecting text, and highlighting it         | `selection.canSelect()` |
| `doc.text.copy`   | Reading the selected text with `readText()` | `selection.canCopy()`   |

Without `doc.text.select`, pointer selection does nothing and `select()` is refused; `clear()` always
works. Without `doc.text.copy`, people can still select and highlight, but `readText()` is refused
and no page text is sent. So a document can allow markup without copy.

## Good to know

### Characters, not string positions

A range counts characters as the page stores them. Search results, annotations and selections all use
the same counting, so a range from one goes straight into another.

### No HTML text layer

The selection works from where each character is, so there's no hidden HTML text over the page. That
keeps large documents fast, and it's why a document can allow selecting without giving away its text.

### Outside the browser

The selection plugin itself has no clipboard or HTML in it, so `readText()` also works in tests and in
Node. The clipboard is the Angular package's job.
