Files
fiddle/public/js/preview.js
root b18c9c1dc8 Add QoL features: preview theme, external resources, shortcuts, mobile layout
- Dark/light preview theme toggle with localStorage persistence and
  dark CSS injection in preview, export, and embed
- External CSS/JS resources modal with per-fiddle persistence in
  options column, injected as link/script tags
- Keyboard shortcuts cheat sheet modal (? button or ? key)
- Mobile-responsive CSS with breakpoints at 768px and 480px
  for both editor and browse pages
2026-02-26 15:39:16 -06:00

141 lines
4.3 KiB
JavaScript

import { getFrameworkRuntime } from './js-preprocessors.js';
import { extractBareImports, buildImportMapTag } from './import-map.js';
const consoleInterceptor = `
<script>
(function() {
const methods = ['log', 'warn', 'error', 'info', 'debug', 'clear'];
methods.forEach(function(method) {
var original = console[method];
console[method] = function() {
if (method === 'clear') {
parent.postMessage({ type: 'console', method: 'clear' }, '*');
if (original) original.apply(console, arguments);
return;
}
var args = Array.from(arguments).map(function(a) {
if (a === null) return 'null';
if (a === undefined) return 'undefined';
if (typeof a === 'object') {
try { return JSON.stringify(a, null, 2); } catch(e) { return String(a); }
}
return String(a);
});
parent.postMessage({ type: 'console', method: method, args: args }, '*');
if (original) original.apply(console, arguments);
};
});
window.onerror = function(msg, url, line, col) {
parent.postMessage({ type: 'console', method: 'error', args: ['Error: ' + msg + ' (line ' + line + ')'] }, '*');
};
})();
<\/script>
`;
/**
* Escape </script in user code to prevent premature HTML script tag closing.
*/
function escapeScriptClose(code) {
return code.replace(/<\/script/gi, '<\\/script');
}
/**
* Build script tags for the preview iframe.
* Uses static <script src> tags for runtime libraries (parser-blocking per HTML spec)
* followed by an inline <script> for user code.
*/
function buildLoaderScript(runtimeUrls, userJs, isModule) {
if (isModule) {
return `\n<script type="module">\n${escapeScriptClose(userJs)}\n<\/script>`;
}
let parts = '';
for (const url of runtimeUrls) {
parts += `<script src="${url}"><\/script>\n`;
}
parts += `<script>\n${escapeScriptClose(userJs)}\n<\/script>`;
return parts;
}
/**
* Render compiled code into the preview iframe.
* @param {string} html - HTML content
* @param {string} css - Compiled CSS
* @param {string} js - Compiled JS
* @param {string} mode - Framework mode
* @param {string} extraCss - Extra CSS (e.g. from Vue/Svelte)
* @param {object} options - { tailwind, isModule, renderedHtml }
*/
export function renderPreview(html, css, js, mode = 'html-css-js', extraCss = '', options = {}) {
const frame = document.getElementById('preview-frame');
const runtime = getFrameworkRuntime(mode);
// Combine CSS
const allCss = extraCss ? `${css}\n${extraCss}` : css;
// If renderedHtml is provided (e.g. Markdown), use it as the body and skip JS
const finalHtml = options.renderedHtml || html;
const finalJs = options.renderedHtml ? '' : js;
// Determine body content
let bodyContent;
if (mode === 'vue' || mode === 'svelte') {
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
} else if (runtime.bodyHtml) {
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
} else {
bodyContent = finalHtml;
}
const isModule = options.isModule || mode === 'svelte';
// Build importmap for module scripts with bare imports
let importMapTag = '';
if (isModule && finalJs) {
const bareImports = extractBareImports(finalJs);
if (bareImports.length) {
importMapTag = buildImportMapTag(bareImports);
}
}
const loaderScript = finalJs
? buildLoaderScript(runtime.scripts, finalJs, isModule)
: '';
// Tailwind CDN injection
const tailwindScript = options.tailwind
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
: '';
// Dark preview theme
const darkCss = options.previewTheme === 'dark'
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
: '';
// External resources
let resourceTags = '';
if (options.resources && options.resources.length) {
for (const r of options.resources) {
if (r.type === 'css') resourceTags += `<link rel="stylesheet" href="${r.url}">\n`;
else if (r.type === 'js') resourceTags += `<script src="${r.url}"><\/script>\n`;
}
}
const doc = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
${consoleInterceptor}
${darkCss}${resourceTags}${tailwindScript}<style>${allCss}</style>
${importMapTag}
</head>
<body>
${bodyContent}
${loaderScript}
</body>
</html>`;
// Update iframe bg class
frame.classList.toggle('preview-dark', options.previewTheme === 'dark');
frame.srcdoc = doc;
}