# Unsaved changes done right

Let the viewer track whether the document differs from what it was opened from or last saved, warn before the tab closes only when it matters, and tell the viewer when a save of your own succeeded.

- Level: Common
- Uses: `documentChanged`, `viewer.hasUnsavedChanges`, `viewer.markSaved()`, `viewer.tools.editText.hasPendingEdits`, `exportCompleted`, `beforeunload`

A confirmation dialog that fires every time, changed or not, trains people to click through it. The viewer keeps the flag for you: `hasUnsavedChanges` is `true` from the first change on, whatever made it, until the document is exported whole or you call `markSaved()`.

## 1. One event, one flag

`documentChanged` fires once for every change to the document, after it is in the document, whatever made it (a tool, Manage Pages, a panel, a form field filled in or signed, the document API, undo or redo), and carries `source`, `tool`, `hasUnsavedChanges`, `canUndo` and `canRedo`. A history step that left the document as it was reports nothing. `viewer.hasUnsavedChanges` is the same flag read directly.

`dirty.ts`

```ts
viewer.on('documentChanged', ({ hasUnsavedChanges, canUndo, canRedo }) => {
  saveButton.disabled = !hasUnsavedChanges;
  undoButton.disabled = !canUndo;
  redoButton.disabled = !canRedo;
});
```

> A form field is outside the history and reports every commit of its value, each keystroke, as `formFieldChanged` does: debounce an autosave that hangs off this event.

## 2. Warn before leaving

The page itself is yours to guard. `viewer.tools.editText.hasPendingEdits` is whether Edit Text holds edits not yet in the document, so not yet in `hasUnsavedChanges`; every way of leaving the tool writes them, and so does an export, which is why it belongs in a `beforeunload` warning beside the flag. Whether an upload of the bytes succeeded is yours to track: the flag does not go back to `true` on a failed upload.

`dirty.ts`

```ts
let uploadFailed = false;

window.addEventListener('beforeunload', (e) => {
  if (viewer.hasUnsavedChanges || uploadFailed || viewer.tools.editText.hasPendingEdits) {
    e.preventDefault();
    e.returnValue = true; // older Chromium and WebViews
  }
});

backButton.addEventListener('click', () => {
  if (!viewer.hasUnsavedChanges || confirm('Leave without saving? Your changes will be lost.')) leave();
});
```

## 3. Clear on save

An export of the whole document by an exporter from `viewer.createExporter()` (the toolbar's Download; `download()`, `getBlob()`, `getArrayBuffer()`, `getBase64()`, `uploadTo()` without `pages`) clears the flag itself; `exportCompleted` with `partial: false` is that moment. After saving your own way (`doc.save()`, an upload of bytes you already had), call `viewer.markSaved()`. Nothing fires when the flag goes back to `false`.

`save.ts`

```ts
viewer.on('exportCompleted', ({ partial }) => {
  if (!partial) saveButton.disabled = !viewer.hasUnsavedChanges;
});

async function autosave(): Promise<void> {
  const blob = await viewer.createExporter().getBlob(); // clears hasUnsavedChanges itself
  try {
    await upload(blob);
    uploadFailed = false;
  } catch {
    uploadFailed = true; // the bytes left the viewer, not the building
  }
}

// Saving through doc.save() instead:
const bytes = await doc.save();
await upload(new Blob([bytes.slice()]));
viewer.markSaved();
```

## Gotchas

- "Saved" means the bytes left the viewer: a change made while an export ran is not in them and stays unsaved, and an undo back to the saved state still counts, since what you saved is not what is shown until it is saved again.
- A change whose undo step could not be recorded (an `error` with `EXPORT_FAILED`) still counts: `hasUnsavedChanges` turns true and `documentChanged` reports it.
- A page-range export (`pages` given) does not clear the flag; `exportCompleted` reports it with `partial: true`.
- `hasPendingEdits` is `false` while the Edit Text tool is not active.

## Related

- [Own the download](https://kernstra.com/docs/web-sdk/recipes/custom-download-s3)
- [documentChanged](https://kernstra.com/docs/web-sdk/api/events#ev-documentchanged)
- [PDFViewer.markSaved](https://kernstra.com/docs/web-sdk/api/pdfviewer/marksaved)
- [viewer.tools.editText](https://kernstra.com/docs/web-sdk/api/viewer-tools-edittext/haspendingedits)
- [What you can replace](https://kernstra.com/docs/web-sdk/guides/replace)

Web page: https://kernstra.com/docs/web-sdk/recipes/unsaved-changes
