Web: warn before reloading a document with unsaved changes (#812) (#856)

This commit is contained in:
MarekSpodBiedry 2026-10-10 21:38:42 +02:00 committed by GitHub
parent f6caba43b2
commit f620750934
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 40 additions and 1 deletions

View File

@ -16,5 +16,5 @@ egui = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "HtmlElement", "Location", "Url", "UrlSearchParams", "Response"] }
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "HtmlElement", "Location", "Url", "UrlSearchParams", "Response", "Event"] }
js-sys = "0.3"

View File

@ -51,6 +51,9 @@ fn main() {
ctx.request_repaint();
});
}
// eframe never raises a close request on a browser reload or tab close, so `guard_quit` never
// prompts; ask the browser to confirm instead while a document has unsaved work (#812).
warn_before_unload(app.unsaved_flag.clone());
Ok(Box::new(app))
}),
)
@ -61,6 +64,20 @@ fn main() {
});
}
#[cfg(target_arch = "wasm32")]
fn warn_before_unload(unsaved: std::sync::Arc<std::sync::atomic::AtomicBool>) {
use eframe::wasm_bindgen::{JsCast, closure::Closure};
let Some(window) = web_sys::window() else { return };
let handler = Closure::<dyn FnMut(web_sys::Event)>::new(move |event: web_sys::Event| {
if unsaved.load(std::sync::atomic::Ordering::Relaxed) {
event.prevent_default();
}
});
if window.add_event_listener_with_callback("beforeunload", handler.as_ref().unchecked_ref()).is_ok() {
handler.forget();
}
}
#[cfg(target_arch = "wasm32")]
fn query_param(key: &str) -> Option<String> {
let search = web_sys::window()?.location().search().ok()?;

View File

@ -590,8 +590,14 @@ impl PdfCraftApp {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
/// Publish whether any document has unsaved work, for the browser's reload warning (#812).
pub(crate) fn sync_unsaved_flag(&self) {
self.unsaved_flag.store(self.first_dirty().is_some(), std::sync::atomic::Ordering::Relaxed);
}
/// Intercept window close while documents have unsaved changes.
pub(crate) fn guard_quit(&mut self, ctx: &egui::Context) {
self.sync_unsaved_flag();
if !ctx.input(|i| i.viewport().close_requested()) {
return;
}
@ -926,6 +932,19 @@ mod revert_draft_tests {
assert!(app.views[index].forms.committed.is_some() && app.views[index].pending_edit.is_some());
}
#[test]
fn unsaved_flag_follows_dirty_state() {
use std::sync::atomic::Ordering;
// `guard_quit` runs every frame, also on the web, where no close request ever arrives.
let ctx = egui::Context::default();
let mut app = app();
app.guard_quit(&ctx);
assert!(!app.unsaved_flag.load(Ordering::Relaxed));
assert!(app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Changed".into()) }));
app.guard_quit(&ctx);
assert!(app.unsaved_flag.load(Ordering::Relaxed));
}
#[test]
fn successful_revert_clears_focused_or_queued_drafts_only_on_its_document() {
for queued in [false, true] {

View File

@ -424,6 +424,8 @@ pub struct PdfCraftApp {
startup_superseded: bool,
/// Asynchronous opens that failed (web `?file=` fetches), shown as a notice.
pub failed_inbox: FailedInbox,
/// Mirrors "some document has unsaved work" each frame, so the web page's `beforeunload` handler can read it (#812).
pub unsaved_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Requests from the operating system, polled every frame (macOS Apple events).
pub os_events: Option<OsEventsFn>,
/// A pending "save changes?" question (closing a dirty tab or quitting).
@ -689,6 +691,7 @@ impl PdfCraftApp {
startup_inbox: Default::default(),
startup_superseded: false,
failed_inbox: Default::default(),
unsaved_flag: Default::default(),
os_events: None,
close_request: None,
save_override: None,