From 21edbe7d8116494f6bf62ed14afb1c443539fbb5 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 21 May 2026 21:26:38 -0400 Subject: [PATCH] translate: add open-in-tab button, move controls above preview, fix blob lifetime --- public/src/translate-modal.js | 60 +++++++++++++++++--------- public/src/uncloseai-modal-builtin.css | 14 ++++++ public/src/uncloseai-modal-pico.css | 14 ++++++ 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/public/src/translate-modal.js b/public/src/translate-modal.js index fc8d9a1..e7e9708 100644 --- a/public/src/translate-modal.js +++ b/public/src/translate-modal.js @@ -518,12 +518,20 @@ ${samplePreserved}`; htmlWithBase = `\n${scriptTag}${translatedText}`; } + // Revoke any blob URL kept alive from a previous translation + if (resultArea._translateBlobUrl) { + URL.revokeObjectURL(resultArea._translateBlobUrl); + } + // Render inline via iframe with blob URL (no popup) const blob = new Blob([htmlWithBase], { type: "text/html" }); const blobUrl = URL.createObjectURL(blob); resultArea.innerHTML = ""; resultArea.className = "translate-result-area"; + // Keep the blob URL alive so the iframe and the + // open-in-tab link both stay valid until the next render. + resultArea._translateBlobUrl = blobUrl; const iframe = document.createElement("iframe"); iframe.src = blobUrl; @@ -542,36 +550,46 @@ ${samplePreserved}`; } iframe.title = iframeTitle; - // Clean up blob URL when iframe loads - iframe.addEventListener("load", () => URL.revokeObjectURL(blobUrl)); + // Controls bar above the preview + const controls = document.createElement("div"); + controls.className = "translate-result-controls"; - const toggleContainer = document.createElement("div"); - toggleContainer.style.marginBottom = "12px"; - - const toggleBtn = document.createElement("button"); - toggleBtn.className = "translate-raw-toggle-btn"; - toggleBtn.textContent = "Show Raw HTML"; - toggleBtn.onclick = () => { - const raw = toggleContainer.querySelector(".translate-raw-html"); - if (raw.style.display === "none") { - raw.style.display = "block"; - toggleBtn.textContent = "Hide Raw HTML"; - } else { - raw.style.display = "none"; - toggleBtn.textContent = "Show Raw HTML"; - } - }; + // Open the translated page in a real browser tab. A user + // click on an anchor is a genuine gesture, so it is not + // blocked like the automatic window.open popups we replaced. + const openTabLink = document.createElement("a"); + openTabLink.className = + "translate-raw-toggle-btn translate-open-tab-btn"; + openTabLink.href = blobUrl; + openTabLink.target = "_blank"; + openTabLink.rel = "noopener"; + openTabLink.textContent = "Open full page ↗"; + openTabLink.title = iframeTitle; const rawHtml = document.createElement("div"); rawHtml.className = "translate-raw-html"; rawHtml.style.display = "none"; rawHtml.textContent = translatedText; - toggleContainer.appendChild(toggleBtn); - toggleContainer.appendChild(rawHtml); + const toggleBtn = document.createElement("button"); + toggleBtn.className = "translate-raw-toggle-btn"; + toggleBtn.textContent = "Show Raw HTML"; + toggleBtn.onclick = () => { + if (rawHtml.style.display === "none") { + rawHtml.style.display = "block"; + toggleBtn.textContent = "Hide Raw HTML"; + } else { + rawHtml.style.display = "none"; + toggleBtn.textContent = "Show Raw HTML"; + } + }; + controls.appendChild(openTabLink); + controls.appendChild(toggleBtn); + + resultArea.appendChild(controls); resultArea.appendChild(iframe); - resultArea.appendChild(toggleContainer); + resultArea.appendChild(rawHtml); } else { resultArea.textContent = translatedText; resultArea.className = "translate-result-area pre-wrap"; diff --git a/public/src/uncloseai-modal-builtin.css b/public/src/uncloseai-modal-builtin.css index 035a055..d311a2f 100644 --- a/public/src/uncloseai-modal-builtin.css +++ b/public/src/uncloseai-modal-builtin.css @@ -451,6 +451,20 @@ dialog#translate-modal > article > .translate-btn { cursor: pointer; } +.translate-result-controls { + display: grid; + grid-auto-flow: column; + justify-content: start; + gap: 8px; + margin-bottom: 12px; +} + +.translate-open-tab-btn { + background: #0b7285; + text-decoration: none; + display: inline-block; +} + .translate-result-area { white-space: normal; } diff --git a/public/src/uncloseai-modal-pico.css b/public/src/uncloseai-modal-pico.css index 0771214..89f9032 100644 --- a/public/src/uncloseai-modal-pico.css +++ b/public/src/uncloseai-modal-pico.css @@ -500,6 +500,20 @@ dialog#translate-modal > article > .translate-btn { cursor: pointer; } +.translate-result-controls { + display: grid; + grid-auto-flow: column; + justify-content: start; + gap: 8px; + margin-bottom: 12px; +} + +.translate-open-tab-btn { + background: #0b7285; + text-decoration: none; + display: inline-block; +} + .translate-result-area { white-space: normal; }