From 81cff0468ed79668ae427e0ddd85c336459f96fe Mon Sep 17 00:00:00 2001 From: Kristian Myram Date: Tue, 27 Oct 2026 22:34:28 +0000 Subject: [PATCH] #2495: keep Duplicate from opening browser bookmarks --- apps/effectcraft-web/js/host.js | 14 ++++++++++++++ apps/effectcraft-web/src/app.rs | 3 +++ apps/effectcraft-web/tests/page.mjs | 22 ++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/apps/effectcraft-web/js/host.js b/apps/effectcraft-web/js/host.js index 9346acc29..41a4c7200 100644 --- a/apps/effectcraft-web/js/host.js +++ b/apps/effectcraft-web/js/host.js @@ -7,6 +7,20 @@ // // Plain functions only: the Rust side owns the logic. +// ------------------------------------------------------------------ shortcuts + +/// Keep Duplicate on the app canvas instead of also opening the browser's bookmarks. +/// eframe still receives the key; other chords and HTML inputs keep their browser behavior. +export function installCanvasShortcuts(canvas) { + canvas.addEventListener("keydown", (event) => { + if (event.target === canvas && (event.ctrlKey || event.metaKey) + && !event.shiftKey && !event.altKey && !event.isComposing + && event.key.toLowerCase() === "d") { + event.preventDefault(); + } + }, { capture: true }); +} + // ------------------------------------------------------------------ storage const DIR = "effectcraft"; diff --git a/apps/effectcraft-web/src/app.rs b/apps/effectcraft-web/src/app.rs index 169529e5d..1dcaf1334 100644 --- a/apps/effectcraft-web/src/app.rs +++ b/apps/effectcraft-web/src/app.rs @@ -15,6 +15,8 @@ use wasm_bindgen::prelude::*; extern "C" { #[wasm_bindgen(js_name = showDeviceLost)] fn show_device_lost(message: &str, kept: bool); + #[wasm_bindgen(js_name = installCanvasShortcuts)] + fn install_canvas_shortcuts(canvas: &web_sys::HtmlCanvasElement); } /// Work for the UI thread with access to the app (posted by async tasks: picked files…). @@ -204,6 +206,7 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> { effectcraft_ui_egui::i18n::set_system_locales(query_value("lang"), browser.collect()); let doc = web_sys::window().and_then(|w| w.document()).ok_or("no document")?; let canvas: web_sys::HtmlCanvasElement = doc.get_element_by_id(&canvas_id).ok_or("no canvas")?.dyn_into()?; + install_canvas_shortcuts(&canvas); let empty = query_flag("empty"); let home = query_flag("home"); // Browser storage: settings, recent projects, imported media, saved projects, auto-saves and diff --git a/apps/effectcraft-web/tests/page.mjs b/apps/effectcraft-web/tests/page.mjs index 063015236..606dbeceb 100644 --- a/apps/effectcraft-web/tests/page.mjs +++ b/apps/effectcraft-web/tests/page.mjs @@ -8,6 +8,8 @@ // - A lost WebGPU device: the page says so over the canvas and offers Save Project and Reload; // the reload writes pending changes to browser storage first, and a failed write is reported // instead of reloading (#225). +// - Duplicate cancels the browser's bookmark action on the app canvas, while still delivering +// the key to the app; other chords and HTML inputs keep their browser behavior (#2495). // Exits non-zero on failure. import assert from "node:assert/strict"; import { dirname, join } from "node:path"; @@ -58,6 +60,26 @@ globalThis.location = { reload: () => reloads++ }; const host = await import(pathToFileURL(join(here, "../js/host.js"))); +// #2495: prevent the browser default without swallowing the app's Duplicate key. +const canvas = new EventTarget(); +const input = new EventTarget(); +host.installCanvasShortcuts(canvas); +let delivered = 0; +canvas.addEventListener("keydown", () => delivered++); +const key = (target, properties) => { + const event = new Event("keydown", { cancelable: true }); + Object.assign(event, { key: "d", ...properties }); + target.dispatchEvent(event); + return event.defaultPrevented; +}; +assert.equal(key(canvas, { ctrlKey: true }), true); +assert.equal(key(canvas, { metaKey: true, key: "D" }), true); +for (const properties of [{}, { ctrlKey: true, shiftKey: true }, { ctrlKey: true, altKey: true }, { ctrlKey: true, isComposing: true }, { ctrlKey: true, key: "r" }]) { + assert.equal(key(canvas, properties), false); +} +assert.equal(delivered, 7, "all keys still reach the app"); +assert.equal(key(input, { ctrlKey: true }), false, "an external HTML input keeps its shortcut"); + // #226: one readable and one unreadable file. const file = (name, read) => ({ name, webkitRelativePath: "", arrayBuffer: read }); const picked = host.browsePickFiles(false);