Replace translate preview popup with inline iframe

Firefox (and other browsers) block window.open popups from the
translate modal. Render translated HTML in an iframe via blob URL
instead. Fix translate modal header layout by removing negative
margin hack, use proper article padding structure. Add CSS for
iframe preview and raw HTML toggle.
This commit is contained in:
russell@unturf.com 2026-03-14 11:46:35 -04:00
parent dfc1cb2a36
commit 00ef650b0e
4 changed files with 176 additions and 94 deletions

View file

@ -487,101 +487,91 @@ ${samplePreserved}`;
translatedText.includes("<section")));
if (isHTML) {
// Auto-open preview in new tab
const previewWindow = window.open("", "_blank");
if (previewWindow) {
// Inject base URL for relative resources to work
const baseUrl =
window.location.origin +
window.location.pathname.substring(
0,
window.location.pathname.lastIndexOf("/") + 1,
);
// Inject base URL for relative resources to work
const baseUrl =
window.location.origin +
window.location.pathname.substring(
0,
window.location.pathname.lastIndexOf("/") + 1,
);
let htmlWithBase;
const scriptTag = `
let htmlWithBase;
const scriptTag = `
<script>
window.UNCLOSEAI_SKIP_INIT = true;
</script>
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>`;
<script src="https://uncloseai.com/uncloseai.js" type="module"><\/script>`;
if (translatedText.includes("<!DOCTYPE")) {
// Already has DOCTYPE, just add base and script
htmlWithBase = translatedText.includes("<head>")
? translatedText.replace(
"<head>",
`<head><base href="${baseUrl}">${scriptTag}`,
)
: translatedText.replace(
"<html>",
`<html><head><base href="${baseUrl}">${scriptTag}</head>`,
);
} else if (translatedText.includes("<head>")) {
// Has head but no DOCTYPE
htmlWithBase = `<!DOCTYPE html>\n${translatedText.replace("<head>", `<head><base href="${baseUrl}">${scriptTag}`)}`;
} else {
// No DOCTYPE or head
htmlWithBase = `<!DOCTYPE html>\n<html><head><base href="${baseUrl}">${scriptTag}</head><body>${translatedText}</body></html>`;
}
previewWindow.document.write(htmlWithBase);
previewWindow.document.close();
// Set title based on translation source
let windowTitle;
if (activeTab === "remote") {
const urlInput = document.querySelector('.translate-url-input');
const sourceUrl = urlInput ? urlInput.value.trim() : 'Remote URL';
windowTitle = `${sourceUrl} - translated into ${targetLanguageName}`;
} else if (activeTab === "custom") {
windowTitle = `Custom Text - translated into ${targetLanguageName}`;
} else {
windowTitle = `${document.title} - translated into ${targetLanguageName}`;
}
previewWindow.document.title = windowTitle;
// Add a notice about AI functionality only in preview window (wait for body to be available)
const addNotice = () => {
if (
previewWindow &&
previewWindow.document &&
previewWindow.document.body
) {
console.log("Adding AI notice to preview window");
const notice = previewWindow.document.createElement("div");
notice.className = "translate-ai-notice";
notice.textContent = `🤖 AI features should work in ${targetLanguageName}!`;
previewWindow.document.body.appendChild(notice);
// Auto-hide notice after 5 seconds
setTimeout(() => {
if (notice.parentNode) notice.parentNode.removeChild(notice);
}, 5000);
} else if (previewWindow) {
// Wait a bit and try again
setTimeout(addNotice, 100);
}
};
// Only add notice if we have a valid preview window
if (previewWindow) {
addNotice();
}
if (translatedText.includes("<!DOCTYPE")) {
htmlWithBase = translatedText.includes("<head>")
? translatedText.replace(
"<head>",
`<head><base href="${baseUrl}">${scriptTag}`,
)
: translatedText.replace(
"<html>",
`<html><head><base href="${baseUrl}">${scriptTag}</head>`,
);
} else if (translatedText.includes("<head>")) {
htmlWithBase = `<!DOCTYPE html>\n${translatedText.replace("<head>", `<head><base href="${baseUrl}">${scriptTag}`)}`;
} else {
htmlWithBase = `<!DOCTYPE html>\n<html><head><base href="${baseUrl}">${scriptTag}</head><body>${translatedText}</body></html>`;
}
// Show raw HTML toggle in modal
resultArea.innerHTML = `
<div style="margin-bottom: 15px;">
<button onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent.includes('Raw') ? 'Show Raw HTML' : 'Hide Raw HTML'" class="translate-raw-toggle-btn">Show Raw HTML</button>
<div style="display: none; margin: 10px 0; padding: 15px; background: #f0f0f0; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 0.85em; max-height: 300px; overflow-y: auto; border: 1px solid #ddd;">${translatedText.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>
</div>
<div style="border: 2px solid #4CAF50; border-radius: 8px; background: #f0fff0; padding: 15px; text-align: center;">
<div style="color: #4CAF50; font-weight: bold; margin-bottom: 10px;">✅ Preview Opened in New Window</div>
<p style="margin: 0; color: #666;">The translated HTML page has opened in a new window with full AI functionality!</p>
${previewWindow ? "" : '<p style="margin: 10px 0 0 0; color: #d32f2f;"><strong>Note:</strong> Please allow popups for preview window</p>'}
</div>
`;
// 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";
const iframe = document.createElement("iframe");
iframe.src = blobUrl;
iframe.className = "translate-preview-iframe";
// Set title for accessibility
let iframeTitle;
if (activeTab === "remote") {
const urlEl = document.querySelector('.translate-url-input');
const sourceUrl = urlEl ? urlEl.value.trim() : 'Remote URL';
iframeTitle = `${sourceUrl} translated into ${targetLanguageName}`;
} else if (activeTab === "custom") {
iframeTitle = `Custom text translated into ${targetLanguageName}`;
} else {
iframeTitle = `${document.title} translated into ${targetLanguageName}`;
}
iframe.title = iframeTitle;
// Clean up blob URL when iframe loads
iframe.addEventListener("load", () => URL.revokeObjectURL(blobUrl));
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";
}
};
const rawHtml = document.createElement("div");
rawHtml.className = "translate-raw-html";
rawHtml.style.display = "none";
rawHtml.textContent = translatedText;
toggleContainer.appendChild(toggleBtn);
toggleContainer.appendChild(rawHtml);
resultArea.appendChild(iframe);
resultArea.appendChild(toggleContainer);
} else {
resultArea.textContent = translatedText;
resultArea.className = "translate-result-area pre-wrap";

View file

@ -314,12 +314,31 @@ dialog#translate-modal {
background: white;
}
/* Translate modal article layout */
dialog#translate-modal > article {
padding: 0;
margin: 0;
}
/* Translate modal body content padding */
dialog#translate-modal > article > :not(.translate-modal-header) {
padding-left: 20px;
padding-right: 20px;
}
dialog#translate-modal > article > .translate-btn {
margin-left: 20px;
margin-right: 20px;
width: calc(100% - 40px);
}
/* Translate modal header */
.translate-modal-header {
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
color: white;
padding: 16px 20px;
margin: -1em -1em 1em -1em;
margin: 0;
border-radius: 8px 8px 0 0;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
@ -440,6 +459,29 @@ dialog#translate-modal {
white-space: pre-wrap;
}
/* Inline translated page preview */
.translate-preview-iframe {
width: 100%;
height: 60vh;
border: 1px solid #dee2e6;
border-radius: 6px;
margin-bottom: 12px;
background: white;
}
.translate-raw-html {
margin: 10px 0;
padding: 15px;
background: #f0f0f0;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.85em;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
}
.translate-tab-nav li {
margin: 0;
}

View file

@ -359,12 +359,35 @@ dialog#translate-modal {
--pico-modal-overlay-background-color: transparent !important;
}
/* Translate modal article resets PicoCSS padding */
dialog#translate-modal > article {
padding: 0;
margin: 0;
border: none;
box-shadow: none;
background: white;
border-radius: 8px;
overflow: hidden;
}
/* Translate modal body content padding */
dialog#translate-modal > article > :not(.translate-modal-header) {
padding-left: 20px;
padding-right: 20px;
}
dialog#translate-modal > article > .translate-btn {
margin-left: 20px;
margin-right: 20px;
width: calc(100% - 40px);
}
/* Translate modal header */
.translate-modal-header {
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
color: white;
padding: 16px 20px;
margin: -1em -1em 1em -1em;
margin: 0;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
@ -485,6 +508,29 @@ dialog#translate-modal {
white-space: pre-wrap;
}
/* Inline translated page preview */
.translate-preview-iframe {
width: 100%;
height: 60vh;
border: 1px solid #dee2e6;
border-radius: 6px;
margin-bottom: 12px;
background: white;
}
.translate-raw-html {
margin: 10px 0;
padding: 15px;
background: #f0f0f0;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.85em;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
}
.translate-tab-nav li {
margin: 0;
}

View file

@ -351,11 +351,15 @@ export async function handleSmartTranslate(button) {
// Translate the page
const translatedHtml = await translateCurrentPage(code);
// Open in new tab
const newWindow = window.open("", "_blank");
newWindow.document.write(translatedHtml);
newWindow.document.close();
newWindow.document.title = `${document.title} - translated into ${nativeName}`;
// Render inline via blob URL (no popup)
const blob = new Blob([translatedHtml], { type: "text/html" });
const blobUrl = URL.createObjectURL(blob);
const previewWindow = window.open(blobUrl, "_blank");
if (previewWindow) {
previewWindow.addEventListener("load", () => URL.revokeObjectURL(blobUrl));
} else {
URL.revokeObjectURL(blobUrl);
}
// Close dropdown
dropdown.remove();