---
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.cloudpdf.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 { cloudEngine } from '@cloudpdf/engine';
import PageOrganizer from './PageOrganizer.vue';

import '../organize.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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 } 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, usePageEditState } from '@embedpdf/vue/page-edit';

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

const pageEdit = usePageEdit();
const pages = usePageList();
const { selected, canEdit } = usePageEditState();
// The second and third pages start selected. The selection holds refs: it names the same
// pages after a reorder, and a deleted page leaves it.
pageEdit.select([1, 2]);
const count = computed(() => selected.value.length);

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

const remove = () => pageEdit.delete(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. Each edit is one step to undo: undo puts the pages
back as they were, and brings a deleted page back with its annotations.

Calls that act on pages take a list of them, so one call works for a single page and for a
multi-selection: the pages in [`selected`](#select-pages).

## 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 { cloudEngine } from '@cloudpdf/engine';
import RotateToolbar from './RotateToolbar.vue';

import '../rotate.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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 { cloudEngine } from '@cloudpdf/engine';
import PageOrder from './PageOrder.vue';

import '../reorder.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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 position. -->
<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({ position: { after: page.ref }, size: { width: 595, height: 842 } });

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

// Pages from another document open in the viewer
await pageEdit.insertFromDocument(otherDocumentId, [pageRef], { position: '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 goes 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 { cloudEngine } from '@cloudpdf/engine';
import InsertToolbar from './InsertToolbar.vue';

import '../insert.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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('/pdfs/welcome.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({ position: { 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({ position: { 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],
      position: { 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" @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 { cloudEngine } from '@cloudpdf/engine';
import ExtractPages from './ExtractPages.vue';

import '../extract.css';

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

const welcome: OpenInput = { kind: 'grant', grant: 'shr_gvpJcG1ULUMon19C0D1wODSY' };
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: welcome }]">
    <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>
```

`duplicate()` and `extract()` copy the pages as they show, with the edits made before them. A
duplicate's copies bring the pages' annotations, and their form fields as fields of their own: a name
the document already uses gets the first free `name_2`, so filling in a copy leaves the original
alone.

## Select pages

`select(pages)` makes pages the selection, and `selected` lists them, in the order they were
selected. The selection is the reader's: show it, and act on it with your own buttons. It holds
refs, so it names the same pages after a reorder, and a page that's deleted leaves it.

```ts
pageEdit.select([page.ref]);
await pageEdit.rotateBy(selected, 90);
```

## Where a page goes

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

A position names a neighbour by its ref, not a number, so it stays right when pages move before
the edit applies.

## Methods

Everything `usePageEdit()` gives you.

| Method                                                 | What it does                                                                                                                    |
| :----------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| `select(pages)`                                        | Makes these pages the selection; `[]` clears it. [Select pages](#select-pages)                                                  |
| `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, position)`                             | 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({ position?, size?, count? })`            | Inserts blank pages, one unless you pass a `count`. Resolves `{ pages }`, the new pages. [Insert pages](#insert-pages)          |
| `insertFromBytes(bytes, { pageIndexes?, position? })`  | Inserts the pages of another PDF, or only the ones at `pageIndexes`. Resolves `{ pages }`.                                      |
| `insertFromDocument(documentId, pages, { position? })` | Inserts copies of pages from another document open in the viewer. Resolves `{ pages }`.                                         |
| `duplicate(pages, { position? })`                      | Copies pages, right after the last of them unless you pass a position. 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) |

`position` 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.

## State

Everything `usePageEditState()` gives you, as refs: your template updates when a value changes. The method in the last column reads the same value once.

| Field        | What it is                                                            | Read once       |
| :----------- | :-------------------------------------------------------------------- | :-------------- |
| `selected`   | The selected pages, in the order they were selected                   | `getSelected()` |
| `canEdit`    | Whether rotating, reordering, deleting and inserting pages is allowed | `canEdit()`     |
| `canExtract` | Whether copying pages out is allowed                                  | `canExtract()`  |

## 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, duplicate and insert pages          | `pageEdit.canEdit()`    |
| `doc.download`       | `extract()` 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`.

### Undo, and when an edit shows

Each edit is one step to undo, so an undo button undoes the last of them, page edits and annotation
edits alike. Undo puts the pages back as they were: a moved page goes back to its place, a deleted
page comes back under the same ref with its annotations, and inserted pages go. A page someone
changed since is left alone.

A turn, a move, a delete, a blank page or a duplicate shows at once, before the engine has it, and
so does undoing one: on a server there's no round trip to wait for. If the engine refuses an edit,
the pages go back as they were. Pages of another PDF or document show once the engine has them.
Quick edits add up: two quarter turns in a row are a half turn.

### Page labels

A page's `label` (`'iii'`, `'A-1'`) follows the page through edits, the way the engine numbers it:
a page takes the numbering of the section it lands in, so a page inserted after the front matter
is numbered as front matter, and deleting a section's first page makes the next page its first.
The labels show at once with the edit, and undo puts them back. The sections themselves are
`pageLabels` on the document; see [Page labels](https://www.cloudpdf.com/docs/engine/documents/pages#page-labels) in the
engine docs for the rules.

### 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).
