Web SDKv1.0.0

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.
CommondocumentChangedviewer.hasUnsavedChangesviewer.markSaved()viewer.tools.editText.hasPendingEditsexportCompletedbeforeunload
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.tsTypeScript
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.tsTypeScript
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.tsTypeScript
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.