feat: closing the tab warns about a document that has no file

The unload warning followed the changes a file had not got yet; a document
that never was a file got none, on the grounds that the autosave keeps it.
It now warns as soon as such a document has content — the same emptiness
test the confirm before a new document uses.
This commit is contained in:
Steffen Becker committed 2026-09-08 00:19:36 +02:00
1 parent 52d9ab5720
commit 15608896a6
3 files changed
+19 -5

No files matched your search

+6 -3
View File
@@ -166,8 +166,8 @@
let dirty = $state(false);
let cleanSum: number | null = null;
let sumTimer: ReturnType<typeof setTimeout> | undefined;
// Whether there is a file to lose those changes from. A document that only ever
// lived in the browser is kept by the autosave, so leaving is not worth a warning.
// Whether there is a file to lose those changes from — a saved file makes the dot,
// and the unload warning, follow the changes rather than the document's existence.
let documentHasFile = $state(false);
// The page count rides in with the zones but is a layout result, not an edit: a
@@ -202,7 +202,10 @@
});
$effect(() => {
if (!dirty || !documentHasFile) return;
// A document with no file behind it lives in this browser alone, so closing the tab
// is worth the browser's warning as much as changes a file has not got yet. The
// same emptiness test the confirm before a new document uses.
if (documentHasFile ? !dirty : tick < 0 || !isDocNonEmpty()) return;
const warn = (e: BeforeUnloadEvent) => e.preventDefault();
addEventListener('beforeunload', warn);
return () => removeEventListener('beforeunload', warn);
+3 -2
View File
@@ -228,8 +228,9 @@ settle pass or the spell checker keeps the document moving), and the baseline is
**synchronously** on the effect's first run, not in that timer: under a repagination the
timer can be pushed out past the reader's first edit, which would make that edit the
baseline. A save, an open or a new document takes a fresh baseline (`markSaved`).
Leaving the page warns only when the document actually has a file — a browser-only document
is kept by the autosave, so there would be nothing to lose.
Leaving the page warns while a document with a file differs from it, and for a document
with no file at all as soon as it has content (the emptiness test the confirm before a new
document uses). The wording is the browser's own — `beforeunload` carries no message.
## Built-in templates (`TemplateGalleryDialog.svelte`, `lib/templates/`)
+10
View File
@@ -30,11 +30,20 @@ try {
await page.reload({ waitUntil: 'load' });
await page.waitForSelector('.tiptap', { timeout: 15_000 });
// What the browser asks before closing the tab: a beforeunload nobody cancels lets it go.
const holdsOn = () => page.evaluate(() => {
const e = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(e);
return e.defaultPrevented;
});
check(!(await holdsOn()), 'an empty document lets the tab close');
// Save on a document with no file behind it settles the format first: the download
// route cannot read one back from the browser's own dialog.
await page.evaluate(() => { window.showSaveFilePicker = undefined; });
await page.click('.tiptap');
await page.keyboard.type('Format first');
check(await holdsOn(), 'a document that was never saved warns before the tab closes');
await page.keyboard.press(`${MOD}+s`);
const [firstSave] = await Promise.all([
page.waitForEvent('download', { timeout: 30_000 }),
@@ -42,6 +51,7 @@ try {
]);
check(firstSave.suggestedFilename().endsWith('.docx'),
`Ctrl+S on an unsaved document asks for the format (${firstSave.suggestedFilename()})`);
check(!(await holdsOn()), 'the saved document lets the tab close again');
// A multi-page corpus document, through the file input (no picker in headless).
await page.setInputFiles('input.file-input[accept*=".odt"]', join(ROOT, 'tests/corpus/05-breaks.odt'));