From 71c1097d5ab8d9da1af2d7e51b07b6542b05ace3 Mon Sep 17 00:00:00 2001 From: TheRenegadeDev Date: Fri, 16 Oct 2026 21:20:34 -0500 Subject: [PATCH] #1054: clean up web print iframe and blob resources --- apps/gridcraft-web/Cargo.toml | 2 +- apps/gridcraft-web/src/web.rs | 54 ++++++++++++++++++++++++++--------- docs/ui-parity.md | 3 +- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/apps/gridcraft-web/Cargo.toml b/apps/gridcraft-web/Cargo.toml index eac6c1354..4856b565d 100644 --- a/apps/gridcraft-web/Cargo.toml +++ b/apps/gridcraft-web/Cargo.toml @@ -26,4 +26,4 @@ rfd = { workspace = true } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" -web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "HtmlIFrameElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration", "Clipboard", "ClipboardItem", "Navigator", "MessageEvent", "UrlSearchParams"] } +web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "HtmlIFrameElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration", "Clipboard", "ClipboardItem", "Navigator", "MessageEvent", "UrlSearchParams", "AddEventListenerOptions"] } diff --git a/apps/gridcraft-web/src/web.rs b/apps/gridcraft-web/src/web.rs index c0fcb22da..6efbbaedb 100644 --- a/apps/gridcraft-web/src/web.rs +++ b/apps/gridcraft-web/src/web.rs @@ -224,15 +224,20 @@ fn print_pdf(bytes: &[u8]) -> Result<(), String> { let window = web_sys::window().ok_or("no window")?; let document = window.document().ok_or("no document")?; + let iframe: web_sys::HtmlIFrameElement = document.create_element("iframe").map_err(js)?.dyn_into().map_err(|_| "not an iframe")?; + iframe.style().set_property("display", "none").map_err(js)?; + let body = document.body().ok_or("no body")?; + let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes)); let opts = web_sys::BlobPropertyBag::new(); opts.set_type("application/pdf"); let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &opts).map_err(js)?; let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(js)?; - let iframe: web_sys::HtmlIFrameElement = document.create_element("iframe").map_err(js)?.dyn_into().map_err(|_| "not an iframe")?; - iframe.style().set_property("display", "none").map_err(js)?; - document.body().ok_or("no body")?.append_child(&iframe).map_err(js)?; + if let Err(e) = body.append_child(&iframe) { + cleanup_print(&iframe, &url); + return Err(js(e)); + } // `onload` fires once the PDF has actually rendered inside the iframe; // printing before that would show a blank page. Set the handler BEFORE @@ -240,22 +245,44 @@ fn print_pdf(bytes: &[u8]) -> Result<(), String> { let iframe_for_load = iframe.clone(); let url_for_cleanup = url.clone(); let on_load = wasm_bindgen::closure::Closure::once_into_js(move || { - let Some(content_window) = iframe_for_load.content_window() else { return }; - let _ = content_window.print(); - // `afterprint` fires once the print dialog (or print preview) closes, whether the user - // printed or cancelled — the right moment to release the blob URL and drop the iframe. - let iframe_for_cleanup = iframe_for_load.clone(); - let on_after_print = wasm_bindgen::closure::Closure::once_into_js(move || { - web_sys::Url::revoke_object_url(&url_for_cleanup).ok(); - iframe_for_cleanup.remove(); - }); - let _ = content_window.add_event_listener_with_callback("afterprint", on_after_print.unchecked_ref()); + iframe_for_load.set_onload(None); + if let Err(e) = print_loaded_pdf(&iframe_for_load, &url_for_cleanup) { + cleanup_print(&iframe_for_load, &url_for_cleanup); + log::error!("print failed: {e:?}"); + } }); iframe.set_onload(Some(on_load.unchecked_ref())); iframe.set_src(&url); Ok(()) } +fn cleanup_print(iframe: &web_sys::HtmlIFrameElement, url: &str) { + // Both operations are idempotent: print can fail after dispatching `afterprint`. + web_sys::Url::revoke_object_url(url).ok(); + iframe.remove(); +} + +fn print_loaded_pdf(iframe: &web_sys::HtmlIFrameElement, url: &str) -> Result<(), JsValue> { + let content_window = iframe.content_window().ok_or_else(|| JsValue::from_str("no print content window"))?; + let iframe_for_cleanup = iframe.clone(); + let url_for_cleanup = url.to_owned(); + let on_after_print = wasm_bindgen::closure::Closure::once(move || { + cleanup_print(&iframe_for_cleanup, &url_for_cleanup); + }); + // Listen before `print()`, which can dispatch `afterprint` before it returns. + // A one-shot listener also releases the callback when the dialog closes or is cancelled. + let options = web_sys::AddEventListenerOptions::new(); + options.set_once(true); + let callback = on_after_print.as_ref().unchecked_ref(); + content_window.add_event_listener_with_callback_and_add_event_listener_options("afterprint", callback, &options)?; + if let Err(e) = content_window.print() { + let _ = content_window.remove_event_listener_with_callback("afterprint", callback); + return Err(e); + } + let _ = on_after_print.into_js_value(); + Ok(()) +} + fn mime_for(name: &str) -> &'static str { match name.rsplit('.').next().map(str::to_ascii_lowercase).as_deref() { Some("xlsx") => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @@ -274,7 +301,6 @@ fn mime_for(name: &str) -> &'static str { } /// Trigger a browser save dialog or download of `bytes` named after `name`. - fn download(name: &str, bytes: &[u8]) -> Result<(), String> { let js = |e: wasm_bindgen::JsValue| format!("{e:?}"); let window = web_sys::window().ok_or("no window")?; diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 22e221e69..19b026adc 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (web print resource cleanup, #1054) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -161,6 +161,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Web printing registers one-shot `afterprint` cleanup before opening the dialog; missing content windows and setup, listener or print failures also release the hidden iframe and PDF blob URL (#1054) | | 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread | | 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) | | 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |