---
title: "Organizing pages — Vue"
description: "Rotate, reorder, delete, duplicate and insert pages, and extract pages into a new PDF. Edits are written into the document."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/documents/pages"
---

# Organizing pages

The page edit plugin changes the document's pages: rotate, reorder, delete, duplicate, and insert
new ones, blank or from another PDF. These are edits to the file, kept when it's downloaded. Register
`pageEditPlugin()` and use `usePageEdit()`:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { renderPlugin } from '@embedpdf/vue/render';
import { pageEditPlugin } from '@embedpdf/vue/page-edit';
import { localEngine } from '@embedpdf/engine';
import PageOrganizer from './PageOrganizer.vue';

import '../organize.css';

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

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

**`organize.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.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 12px;
  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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

.button.danger {
  color: var(--danger);
}

/* The pages in one row, scrolling sideways when they don't fit */
.pages {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  overflow-x: auto;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

/* A card is as wide as its page: a page turned a quarter makes it wider */
.pages > li {
  flex: none;
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 140px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

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

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.turn {
  font-weight: 500;
  color: var(--muted);
}

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

**`PageOrganizer.vue`**

```vue
<!-- Every page as a card. Click cards to select them; the toolbar edits every selected page at once. -->
<script setup lang="ts">
import { computed, shallowRef } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import type { PageRef } from '@embedpdf/vue/runtime';
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
import { usePageEdit } from '@embedpdf/vue/page-edit';

const sameRef = (left: PageRef, right: PageRef) => left.objectNumber === right.objectNumber;

const pageEdit = usePageEdit();
const pages = usePageList();
const canEdit = computed(() => pageEdit.canEdit());
// The second and third pages start selected. Refs, not indexes: they still name the
// same pages after a reorder.
const selected = shallowRef<readonly PageRef[]>(pages.value.slice(1, 3).map((page) => page.ref));
const count = computed(() => selected.value.length);

const isSelected = (page: PageRef) => selected.value.some((each) => sameRef(each, page));
function toggle(page: PageRef) {
  selected.value = isSelected(page)
    ? selected.value.filter((each) => !sameRef(each, page))
    : [...selected.value, page];
}

async function remove() {
  await pageEdit.delete(selected.value);
  selected.value = [];
}
</script>

<template>
  <div class="toolbar">
    <output class="readout">{{ count }} of {{ pages.length }} selected</output>
    <span class="spacer" />
    <button
      type="button"
      class="button"
      :disabled="!canEdit || count === 0"
      @click="pageEdit.rotateBy(selected, -90)"
    >
      ⟲ Rotate left
    </button>
    <button
      type="button"
      class="button"
      :disabled="!canEdit || count === 0"
      @click="pageEdit.rotateBy(selected, 90)"
    >
      ⟳ Rotate right
    </button>
    <button
      type="button"
      class="button"
      :disabled="!canEdit || count === 0"
      @click="pageEdit.reorder(selected, 'start')"
    >
      Move to front
    </button>
    <!-- A document keeps at least one page. -->
    <button
      type="button"
      class="button danger"
      :disabled="!canEdit || count === 0 || count === pages.length"
      @click="remove"
    >
      Delete
    </button>
  </div>
  <ol class="pages">
    <li v-for="page in pages" :key="page.ref.objectNumber">
      <button
        type="button"
        class="card"
        :aria-pressed="isSelected(page.ref)"
        @click="toggle(page.ref)"
      >
        <PageView :page="page.ref" :width="120" class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">
          Page {{ page.index + 1 }}
          <span v-if="page.rotation !== 0" class="turn">· {{ page.rotation }}°</span>
        </span>
      </button>
    </li>
  </ol>
</template>
```

```ts
await pageEdit.rotateBy([page.ref], 90);
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.delete([page.ref]);
```

After an edit, everything that shows pages updates by itself: the Stage, thumbnails, page numbers. On
a shared document, the others see the change too.

Calls that act on pages take a list of them, so one call works for a single page and for a
multi-selection.

## Rotate pages

```ts
await pageEdit.rotateBy([page.ref], 90); // a quarter turn clockwise from where it is
await pageEdit.rotateBy([page.ref], -90); // and back
await pageEdit.setRotation([pageA.ref, pageB.ref], 180); // the same rotation for several
```

**`App.vue`**

```vue
<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 { pageEditPlugin } from '@embedpdf/vue/page-edit';
import { localEngine } from '@embedpdf/engine';
import RotateToolbar from './RotateToolbar.vue';

import '../rotate.css';

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

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

**`rotate.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.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 12px;
  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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* 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);
}
```

**`RotateToolbar.vue`**

```vue
<!-- Rotates the page you're on, in the file: the turn is kept when the document is downloaded. -->
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import { useStageState } from '@embedpdf/vue/stage';
import { usePageEdit } from '@embedpdf/vue/page-edit';

const pageEdit = usePageEdit();
const pages = usePageList();
const currentPageIndex = useStageState((state) => state.currentPageIndex);
const page = computed(() => pages.value[currentPageIndex.value]);
const canEdit = computed(() => pageEdit.canEdit());

// The first page starts a quarter turn clockwise. setRotation() sets the same rotation
// however often it runs; rotateBy() would turn it again.
onMounted(() => {
  void pageEdit.setRotation([0], 90);
});

function everyPageUpright() {
  void pageEdit.setRotation(
    pages.value.map((each) => each.ref),
    0,
  );
}
</script>

<template>
  <div v-if="page" class="toolbar">
    <output class="readout">Page {{ currentPageIndex + 1 }} · {{ page.rotation }}°</output>
    <span class="spacer" />
    <button
      type="button"
      class="button"
      :disabled="!canEdit"
      @click="pageEdit.rotateBy([page.ref], -90)"
    >
      ⟲ Rotate left
    </button>
    <button
      type="button"
      class="button"
      :disabled="!canEdit"
      @click="pageEdit.rotateBy([page.ref], 90)"
    >
      ⟳ Rotate right
    </button>
    <button type="button" class="button" :disabled="!canEdit" @click="everyPageUpright">
      Every page upright
    </button>
  </div>
</template>
```

This rotates the page in the file. To only turn how pages are shown, use the Stage's
[view rotation](../viewing/stage#rotate-the-view). Keep the two on different buttons.

## Reorder pages

`reorder()` takes the pages and where they go:

```ts
await pageEdit.reorder([page.ref], 'start'); // to the front
await pageEdit.reorder([page.ref], 'end'); // to the back
await pageEdit.reorder([pageA.ref, pageB.ref], { after: pageC.ref }); // after another page
await pageEdit.reorder([page.ref], { before: pageC.ref });
```

Several pages keep their order and land together.

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { renderPlugin } from '@embedpdf/vue/render';
import { pageEditPlugin } from '@embedpdf/vue/page-edit';
import { localEngine } from '@embedpdf/engine';
import PageOrder from './PageOrder.vue';

import '../reorder.css';

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

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

**`reorder.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.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 12px;
  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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* The pages in their order: a row that scrolls sideways where it doesn't fit */
.strip {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  margin: 0;
  padding: 16px;
  overflow-x: auto;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font: inherit;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[aria-pressed='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

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

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

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

**`PageOrder.vue`**

```vue
<!-- Pick a page, then move it. Each button uses another placement. -->
<script setup lang="ts">
import { computed, shallowRef } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import type { PageRef } from '@embedpdf/vue/runtime';
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
import { usePageEdit } from '@embedpdf/vue/page-edit';

const pageEdit = usePageEdit();
const pages = usePageList();
const canEdit = computed(() => pageEdit.canEdit());
// The last page starts selected. Its ref names it wherever it moves.
const selected = shallowRef<PageRef | null>(pages.value[pages.value.length - 1]?.ref ?? null);

const page = computed(() =>
  pages.value.find((each) => each.ref.objectNumber === selected.value?.objectNumber),
);
const before = computed(() => page.value && pages.value[page.value.index - 1]);
const after = computed(() => page.value && pages.value[page.value.index + 1]);

function toFront() {
  if (page.value) void pageEdit.reorder([page.value.ref], 'start');
}
function earlier() {
  if (page.value && before.value)
    void pageEdit.reorder([page.value.ref], { before: before.value.ref });
}
function later() {
  if (page.value && after.value)
    void pageEdit.reorder([page.value.ref], { after: after.value.ref });
}
function toBack() {
  if (page.value) void pageEdit.reorder([page.value.ref], 'end');
}
</script>

<template>
  <div class="toolbar">
    <output class="readout">{{ page ? `Page ${page.index + 1} selected` : 'Pick a page' }}</output>
    <span class="spacer" />
    <button type="button" class="button" :disabled="!canEdit || !before" @click="toFront">
      ⇤ To the front
    </button>
    <button type="button" class="button" :disabled="!canEdit || !before" @click="earlier">
      ← Earlier
    </button>
    <button type="button" class="button" :disabled="!canEdit || !after" @click="later">
      Later →
    </button>
    <button type="button" class="button" :disabled="!canEdit || !after" @click="toBack">
      To the back ⇥
    </button>
  </div>
  <ol class="strip">
    <li v-for="each in pages" :key="each.ref.objectNumber">
      <button
        type="button"
        class="card"
        :aria-pressed="each.ref.objectNumber === selected?.objectNumber"
        @click="selected = each.ref"
      >
        <PageView :page="each.ref" :width="110" class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">{{ each.index + 1 }}</span>
      </button>
    </li>
  </ol>
</template>
```

## Delete pages

```ts
await pageEdit.delete([pageA.ref, pageB.ref]);
```

A document always keeps at least one page: deleting every page is refused.

## Insert pages

```ts
// A blank page after this one, the size of an A4 sheet
await pageEdit.insertBlank({ placement: { after: page.ref }, size: { width: 595, height: 842 } });

// Pages from another PDF: all of them, or the ones you name
await pageEdit.insertFromBytes(otherPdf, { placement: 'end' });
await pageEdit.insertFromBytes(otherPdf, { pageIndexes: [0, 2], placement: 'start' });

// Pages from another document open in the viewer
await pageEdit.insertFromDocument(otherDocumentId, [pageRef], { placement: 'end' });

// Copies of pages, right after them
await pageEdit.duplicate([page.ref]);
```

Each resolves with `{ pages }`: the new pages, so you can scroll to them or select them. Without a
`size`, a blank page takes the size of the page it's placed next to.

**`App.vue`**

```vue
<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 { pageEditPlugin } from '@embedpdf/vue/page-edit';
import { localEngine } from '@embedpdf/engine';
import InsertToolbar from './InsertToolbar.vue';

import '../insert.css';

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

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

**`insert.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.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 12px;
  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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

/* 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);
}
```

**`InsertToolbar.vue`**

```vue
<!-- Inserts next to the page you're on, then goes to the first new page. -->
<script setup lang="ts">
import { computed, shallowRef, watch } from 'vue';
import { usePageList } from '@embedpdf/vue/runtime';
import type { PageRef } from '@embedpdf/vue/runtime';
import { useStage, useStageState } from '@embedpdf/vue/stage';
import { usePageEdit } from '@embedpdf/vue/page-edit';

/** Another PDF's bytes, to insert pages from. */
const otherPdf = async () => (await fetch('https://snippet.embedpdf.com/ebook.pdf')).arrayBuffer();

const pageEdit = usePageEdit();
const stage = useStage();
const pages = usePageList();
const currentPageIndex = useStageState((state) => state.currentPageIndex);
const page = computed(() => pages.value[currentPageIndex.value]);
const added = shallowRef<readonly PageRef[]>([]);
const canEdit = computed(() => pageEdit.canEdit());

// Every insert resolves { pages }: the new pages, to go to or select.
function show(result: { pages: readonly PageRef[] }) {
  added.value = result.pages;
  stage.goToPage(result.pages[0]);
}

// A blank page after the cover, once, on load.
let inserted = false;
watch(
  () => pages.value[0],
  (cover) => {
    if (inserted || !cover) return;
    inserted = true;
    void pageEdit.insertBlank({ placement: { after: cover.ref } }).then(show);
  },
  { immediate: true },
);

const positions = computed(() =>
  added.value
    .map((each) => pages.value.findIndex((entry) => entry.ref.objectNumber === each.objectNumber) + 1)
    .filter((position) => position > 0),
);

function insertBlankAfter(after: PageRef) {
  void pageEdit.insertBlank({ placement: { after } }).then(show);
}

function duplicate(target: PageRef) {
  void pageEdit.duplicate([target]).then(show);
}

async function insertFromOtherPdf(after: PageRef) {
  show(
    await pageEdit.insertFromBytes(await otherPdf(), {
      pageIndexes: [0, 2],
      placement: { after },
    }),
  );
}
</script>

<template>
  <div v-if="page" class="toolbar">
    <button
      type="button"
      class="button"
      :disabled="!canEdit"
      @click="insertBlankAfter(page.ref)"
    >
      + Blank page after
    </button>
    <button
      type="button"
      class="button"
      :disabled="!canEdit || !pageEdit.canExtract()"
      @click="duplicate(page.ref)"
    >
      Duplicate page
    </button>
    <button
      type="button"
      class="button"
      :disabled="!canEdit"
      @click="insertFromOtherPdf(page.ref)"
    >
      + Pages 1 and 3 of another PDF
    </button>
    <span class="spacer" />
    <output class="readout">
      {{
        positions.length > 0
          ? `New: page ${positions.join(' and ')} of ${pages.length}`
          : `${pages.length} pages`
      }}
    </output>
  </div>
</template>
```

## Extract pages

`extract()` gives you some pages as a new PDF, without changing this document:

```ts
const bytes = await pageEdit.extract([pageA.ref, pageB.ref]);
saveFile(bytes, 'chapter-2.pdf');
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { renderPlugin } from '@embedpdf/vue/render';
import { pageEditPlugin } from '@embedpdf/vue/page-edit';
import { localEngine } from '@embedpdf/engine';
import ExtractPages from './ExtractPages.vue';

import '../extract.css';

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

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

**`extract.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.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 12px;
  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:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* The pages: as many in a row as fit */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  align-items: end;
  gap: 12px;
  margin: 0;
  padding: 16px;
  list-style: none;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 10px 8px 8px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.card:hover {
  background: var(--tint);
}

.card[data-ticked='true'] {
  background: var(--tint);
  border-color: var(--accent);
}

.thumbnail {
  box-shadow:
    0 0 0 1px var(--line),
    0 2px 6px rgb(7 32 76 / 0.12);
}

.label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.label input {
  margin: 0;
  accent-color: var(--accent);
}

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

**`ExtractPages.vue`**

```vue
<!-- Tick pages, then download them as a PDF of their own. This document doesn't change. -->
<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import { saveFile, usePageList } from '@embedpdf/vue/runtime';
import type { PageRef } from '@embedpdf/vue/runtime';
import { PageView } from '@embedpdf/vue/page-view';
import { RenderLayer } from '@embedpdf/vue/render';
import { usePageEdit } from '@embedpdf/vue/page-edit';

const pageEdit = usePageEdit();
const pages = usePageList();
// The middle two pages start ticked.
const ticked = shallowRef<readonly PageRef[]>(pages.value.slice(1, 3).map((page) => page.ref));
const saved = ref<string | null>(null);

const isTicked = (page: PageRef) => ticked.value.some((each) => each.objectNumber === page.objectNumber);
function toggle(page: PageRef) {
  ticked.value = isTicked(page)
    ? ticked.value.filter((each) => each.objectNumber !== page.objectNumber)
    : [...ticked.value, page];
}

// In document order, whatever order they were ticked in.
const chosen = computed(() =>
  pages.value.filter((page) => isTicked(page.ref)).map((page) => page.ref),
);

async function download() {
  const bytes = await pageEdit.extract(chosen.value);
  saveFile(bytes, 'pages.pdf');
  saved.value = `pages.pdf · ${chosen.value.length} pages · ${Math.round(bytes.byteLength / 1024)} KB`;
}
</script>

<template>
  <div class="toolbar">
    <button
      type="button"
      class="button primary"
      :disabled="chosen.length === 0 || !pageEdit.canExtract()"
      @click="download"
    >
      Download {{ chosen.length === 1 ? '1 page' : `${chosen.length} pages` }} as a PDF
    </button>
    <output class="readout">{{ saved }}</output>
  </div>
  <ol class="grid">
    <li v-for="page in pages" :key="page.ref.objectNumber">
      <label class="card" :data-ticked="isTicked(page.ref)">
        <PageView :page="page.ref" :width="110" class="thumbnail">
          <RenderLayer />
        </PageView>
        <span class="label">
          <input type="checkbox" :checked="isTicked(page.ref)" @change="toggle(page.ref)" />
          Page {{ page.index + 1 }}
        </span>
      </label>
    </li>
  </ol>
</template>
```

## Where a page goes

| Placement             | Means                  |
| :-------------------- | :--------------------- |
| `{ after: pageRef }`  | Right after that page  |
| `{ before: pageRef }` | Right before that page |
| `'start'`             | Before the first page  |
| `'end'`               | After the last page    |

A placement names a page, not a position, so it stays right when pages move while the edit waits
its turn. The page can also be an index, as everywhere in this plugin: the page at that index
when you call.

## Methods

Everything `usePageEdit()` gives you.

| Method                                                  | What it does                                                                                                                    |
| :------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------ |
| `rotateBy(pages, delta)`                                | Turns each page by `90`, `-90` or `180` degrees from where it is. [Rotate pages](#rotate-pages)                                 |
| `setRotation(pages, rotation)`                          | Gives every page the same rotation: `0`, `90`, `180` or `270`                                                                   |
| `reorder(pages, placement)`                             | Moves pages to a new place. Several pages keep their order and land together. [Reorder pages](#reorder-pages)                   |
| `delete(pages)`                                         | Deletes pages. Deleting every page is refused. [Delete pages](#delete-pages)                                                    |
| `insertBlank({ placement?, size?, count? })`            | Inserts blank pages, one unless you pass a `count`. Resolves `{ pages }`, the new pages. [Insert pages](#insert-pages)          |
| `insertFromBytes(bytes, { pageIndexes?, placement? })`  | Inserts the pages of another PDF, or only the ones at `pageIndexes`. Resolves `{ pages }`.                                      |
| `insertFromDocument(documentId, pages, { placement? })` | Inserts copies of pages from another document open in the viewer. Resolves `{ pages }`.                                         |
| `duplicate(pages, { placement? })`                      | Copies pages, right after the last of them unless you pass a placement. Resolves `{ pages }`.                                   |
| `extract(pages)`                                        | Copies pages into a new PDF and resolves its bytes. This document doesn't change. [Extract pages](#extract-pages)               |
| `canEdit()`, `canExtract()`                             | Whether rotating, reordering, deleting and inserting are allowed, and whether copying pages out is. [Permissions](#permissions) |

`placement` is one of the values in [Where a page goes](#where-a-page-goes); without one, new pages go
at the end. Every method that returns a promise also takes a `signal` in its options, to cancel it.

## Events

Page changes are document events, so every part of your app hears them, whichever plugin made the
change:

**`page-count.vue`**

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useDocumentsEvent } from '@embedpdf/vue/runtime';

const pageCount = ref(0);

useDocumentsEvent(
  (documents) => documents.onPagesChanged,
  ({ pages }) => (pageCount.value = pages.length),
);
</script>

<template>
  <span>{{ pageCount }} pages</span>
</template>
```

The page edit plugin has no events of its own. `onPagesChanged` and what it carries are listed under
[Opening documents](opening#events).

## Permissions

| Permission           | What it allows                                                              | Check                   |
| :------------------- | :-------------------------------------------------------------------------- | :---------------------- |
| `doc.pages.assemble` | Rotate, reorder, delete and insert pages                                    | `pageEdit.canEdit()`    |
| `doc.download`       | `extract()`, `duplicate()` and `insertFromDocument()`, which copy pages out | `pageEdit.canExtract()` |

## Good to know

### Pages have a ref, not just a number

Each page has a `ref`: its identity in the file. A page's index changes the moment a page before it
moves or goes; its ref never does. So a call still hits the page the user clicked, even if the
document changed in between. You get refs from `usePageList()` (`page.ref`), and anything that
points at a page carries one: a search result's `page`, an annotation's `page`.

### What moves with a page

Annotations, links and form fields belong to their page, so they move, rotate and go with it.
Rotation leaves annotations where they are on the page; they turn with it.

> Page edits are saved like every other change; see [Saving & downloading](saving).
