Skip to content

Commit e0e4fc9

Browse files
ersecoclaude
andcommitted
Use external popup instead of iframe for Playground WASM conversion
The previous iframe approach failed because iframes inside WordPress Playground cannot enable Cross-Origin Isolation (COOP/COEP headers). SharedArrayBuffer is not available without Cross-Origin Isolation. Changed to open the external converter (erseco.github.io/document-converter) in a popup window instead. External domain popups may work in Playground even when internal popups are blocked. Key changes: - Replace initExternalConverter() iframe with window.open() popup - Add polling mechanism to wait for converter ready via postMessage - Handle conversion result via postMessage from popup - Close popup automatically after conversion completes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5c0d7e1 commit e0e4fc9

1 file changed

Lines changed: 130 additions & 29 deletions

File tree

admin/js/documentate-actions.js

Lines changed: 130 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,18 @@
452452
}
453453

454454
/**
455-
* Handle conversion using external converter service.
456-
* Used in WordPress Playground where we can't register Service Workers.
455+
* External converter popup reference (for Playground mode).
456+
* We use a popup instead of iframe because iframes inside Playground
457+
* cannot have Cross-Origin Isolation enabled.
458+
*/
459+
let externalConverterPopup = null;
460+
461+
/**
462+
* Handle conversion using external converter service via popup window.
463+
* Used in WordPress Playground where:
464+
* - Internal popups are blocked
465+
* - Iframes cannot have Cross-Origin Isolation
466+
* - But external popups (to different domains) may work
457467
*
458468
* @param {jQuery} $btn The button element.
459469
* @param {string} action Action type (preview, download).
@@ -462,9 +472,6 @@
462472
*/
463473
async function handleExternalConverterConversion($btn, action, targetFormat, sourceFormat) {
464474
try {
465-
// Initialize external converter if needed
466-
initExternalConverter();
467-
468475
// Step 1: Generate source document via AJAX
469476
updateModal(
470477
strings.generating || 'Generating document...',
@@ -501,55 +508,149 @@
501508
}
502509
const docBuffer = await docResponse.arrayBuffer();
503510

504-
// Step 3: Wait for external converter to be ready
511+
// Step 3: Open external converter in a popup window
512+
// This is different from internal popups - external domains may work in Playground
505513
updateModal(
506-
strings.loadingWasm || 'Loading LibreOffice...',
507-
'Waiting for converter to initialize...'
514+
strings.loadingWasm || 'Opening converter...',
515+
'A popup window will open. Please allow popups if blocked.'
508516
);
509517

510-
// Poll for ready status if not ready yet
511-
if (!externalConverterReady && externalConverterIframe) {
512-
const expectedOrigin = new URL(config.externalConverterUrl).origin;
518+
// Open the external converter
519+
const popupWidth = 600;
520+
const popupHeight = 400;
521+
const left = (window.screen.width - popupWidth) / 2;
522+
const top = (window.screen.height - popupHeight) / 2;
513523

514-
// Send ping and wait
515-
for (let i = 0; i < 60; i++) { // Wait up to 60 seconds
516-
externalConverterIframe.contentWindow.postMessage({
517-
type: 'ping',
518-
requestId: 'poll_' + Date.now()
519-
}, expectedOrigin);
524+
externalConverterPopup = window.open(
525+
config.externalConverterUrl,
526+
'documentate_external_converter',
527+
`width=${popupWidth},height=${popupHeight},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=yes,resizable=yes`
528+
);
520529

521-
await new Promise(resolve => setTimeout(resolve, 1000));
530+
if (!externalConverterPopup) {
531+
throw new Error('Popup blocked. Please allow popups for this site and try again.');
532+
}
522533

523-
if (externalConverterReady) {
524-
break;
534+
// Step 4: Wait for converter to be ready
535+
updateModal(
536+
strings.loadingWasm || 'Loading LibreOffice...',
537+
'Downloading WASM (~50MB). This may take a while the first time.'
538+
);
539+
540+
const expectedOrigin = new URL(config.externalConverterUrl).origin;
541+
542+
// Wait for ready message from popup
543+
const waitForReady = new Promise((resolve, reject) => {
544+
const timeout = setTimeout(() => {
545+
reject(new Error('Converter initialization timeout (120s)'));
546+
}, 120000);
547+
548+
const readyHandler = (event) => {
549+
if (event.origin !== expectedOrigin) return;
550+
551+
if (event.data.type === 'ready') {
552+
clearTimeout(timeout);
553+
window.removeEventListener('message', readyHandler);
554+
resolve();
555+
} else if (event.data.type === 'pong' && event.data.ready) {
556+
clearTimeout(timeout);
557+
window.removeEventListener('message', readyHandler);
558+
resolve();
525559
}
526-
}
560+
};
561+
562+
window.addEventListener('message', readyHandler);
563+
564+
// Poll for ready status
565+
const pollInterval = setInterval(() => {
566+
if (externalConverterPopup && !externalConverterPopup.closed) {
567+
try {
568+
externalConverterPopup.postMessage({
569+
type: 'ping',
570+
requestId: 'poll_' + Date.now()
571+
}, expectedOrigin);
572+
} catch (e) {
573+
// Popup might not be ready yet
574+
}
575+
} else {
576+
clearInterval(pollInterval);
577+
clearTimeout(timeout);
578+
window.removeEventListener('message', readyHandler);
579+
reject(new Error('Converter window was closed'));
580+
}
581+
}, 1000);
582+
583+
// Clear interval when done
584+
const origResolve = resolve;
585+
resolve = () => {
586+
clearInterval(pollInterval);
587+
origResolve();
588+
};
589+
});
527590

528-
if (!externalConverterReady) {
529-
throw new Error('External converter timeout');
530-
}
531-
}
591+
await waitForReady;
532592

533-
// Step 4: Send document to external converter
593+
// Step 5: Send document to converter
534594
updateModal(
535595
strings.convertingBrowser || 'Converting...',
536596
'Processing with LibreOffice WASM...'
537597
);
538598

539-
const expectedOrigin = new URL(config.externalConverterUrl).origin;
540-
externalConverterIframe.contentWindow.postMessage({
599+
// Set up result handler
600+
const waitForResult = new Promise((resolve, reject) => {
601+
const timeout = setTimeout(() => {
602+
reject(new Error('Conversion timeout (120s)'));
603+
}, 120000);
604+
605+
const resultHandler = (event) => {
606+
if (event.origin !== expectedOrigin) return;
607+
608+
if (event.data.type === 'result') {
609+
clearTimeout(timeout);
610+
window.removeEventListener('message', resultHandler);
611+
resolve(event.data);
612+
} else if (event.data.type === 'error') {
613+
clearTimeout(timeout);
614+
window.removeEventListener('message', resultHandler);
615+
reject(new Error(event.data.error || 'Conversion failed'));
616+
}
617+
};
618+
619+
window.addEventListener('message', resultHandler);
620+
});
621+
622+
// Send the document
623+
externalConverterPopup.postMessage({
541624
type: 'convert',
542625
buffer: docBuffer,
543626
format: targetFormat,
544627
requestId: Date.now().toString()
545628
}, expectedOrigin);
546629

547-
// Result will be handled by the message listener
630+
// Wait for result
631+
const result = await waitForResult;
632+
633+
// Step 6: Handle result
634+
if (result.blob) {
635+
handleExternalConversionResult(result.blob, action, targetFormat);
636+
} else {
637+
throw new Error('No result blob received');
638+
}
639+
640+
// Close popup after success
641+
if (externalConverterPopup && !externalConverterPopup.closed) {
642+
externalConverterPopup.close();
643+
}
548644

549645
} catch (error) {
550646
console.error('Documentate external conversion error:', error);
551647
showError(error.message || strings.errorGeneric || 'Conversion error.');
552648
pendingConversion = null;
649+
650+
// Close popup on error
651+
if (externalConverterPopup && !externalConverterPopup.closed) {
652+
externalConverterPopup.close();
653+
}
553654
}
554655
}
555656

0 commit comments

Comments
 (0)