import { initEditors, switchMode, getEditorValues, setEditorValues, setOnChange, getCurrentMode, getCssType, setCssType, relayoutEditors, MODE_TABS, MODE_TO_JS_TYPE, JS_TYPE_TO_MODE, } from './editors.js'; import { renderPreview } from './preview.js'; import { initConsole, clearConsole } from './console-panel.js'; import { compileCss } from './preprocessors.js'; import { compileJs } from './js-preprocessors.js'; import { createFiddle, loadFiddle, updateFiddle, listTags } from './api.js'; import { getPref, setPref } from './preferences.js'; import { initEmmet } from './emmet.js'; import { initKeybindings } from './keybindings.js'; import { initResizer, clearInlineSizes } from './resizer.js'; import { exportHtml } from './export.js'; import { showQrModal } from './qr.js'; let currentId = null; let debounceTimer = null; let currentTags = []; let currentResources = []; const $ = (sel) => document.querySelector(sel); const STARTER_TEMPLATES = { 'html-css-js': { html: '', css: '', js: '' }, 'typescript': { html: '
', css: '', js: `interface Greeting {\n name: string;\n message: string;\n}\n\nconst greet = (g: Greeting): string => \`\${g.message}, \${g.name}!\`;\n\nconst result = greet({ name: "World", message: "Hello" });\ndocument.getElementById("app")!.innerHTML = \`

\${result}

\`;\nconsole.log(result);`, }, 'react': { js: `const App = () => {\n const [count, setCount] = React.useState(0);\n return (\n
\n

Hello React

\n \n
\n );\n};\n\nReactDOM.createRoot(document.getElementById('root')).render();`, css: '', html: '', }, 'react-ts': { js: `const App: React.FC = () => {\n const [count, setCount] = React.useState(0);\n return (\n
\n

Hello React + TypeScript

\n \n
\n );\n};\n\nReactDOM.createRoot(document.getElementById('root')!).render();`, css: '', html: '', }, 'vue': { js: `\n\n`, css: '', }, 'svelte': { js: `\n\n

Hello Svelte

\n\n\n`, css: '', }, 'markdown': { js: `# Hello Markdown\n\nThis is a **Markdown** fiddle. Write your content here and see it rendered in the preview.\n\n## Features\n\n- Headers, **bold**, *italic*\n- Lists (ordered and unordered)\n- Code blocks with syntax highlighting\n- Links, images, and more\n\n### Code Example\n\n\`\`\`javascript\nconst greeting = "Hello, World!";\nconsole.log(greeting);\n\`\`\`\n\n> Blockquotes work too!\n\n| Column 1 | Column 2 |\n|----------|----------|\n| Cell A | Cell B |`, css: `body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n max-width: 720px;\n margin: 0 auto;\n padding: 24px;\n line-height: 1.6;\n color: #1a1a1a;\n}\n\nh1, h2, h3 { margin-top: 1.5em; margin-bottom: 0.5em; }\nh1 { border-bottom: 2px solid #eee; padding-bottom: 0.3em; }\n\ncode {\n background: #f4f4f4;\n padding: 2px 6px;\n border-radius: 3px;\n font-size: 0.9em;\n}\n\npre {\n background: #f4f4f4;\n padding: 16px;\n border-radius: 6px;\n overflow-x: auto;\n}\n\npre code { background: none; padding: 0; }\n\nblockquote {\n border-left: 4px solid #ddd;\n margin: 1em 0;\n padding: 0.5em 1em;\n color: #555;\n}\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 1em 0;\n}\n\nth, td {\n border: 1px solid #ddd;\n padding: 8px 12px;\n text-align: left;\n}\n\nth { background: #f4f4f4; font-weight: 600; }`, }, 'wasm': { html: '

WebAssembly Demo

\n
', css: `body {\n font-family: monospace;\n padding: 24px;\n background: #1a1a2e;\n color: #0f0;\n}\n\nh1 { color: #00d4ff; margin-bottom: 16px; }\n#output { white-space: pre; font-size: 14px; line-height: 1.8; }`, js: `// Inline WebAssembly "add" module (no external URL needed)\n// WAT source: (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))\nconst wasmBytes = new Uint8Array([\n 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,\n 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,\n 0x03, 0x02, 0x01, 0x00,\n 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,\n 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b\n]);\n\nconst out = document.getElementById('output');\n\ntry {\n const { instance } = await WebAssembly.instantiate(wasmBytes);\n const add = instance.exports.add;\n\n out.textContent = [\n \`WASM loaded successfully!\`,\n \`\`,\n \`add(2, 3) = \${add(2, 3)}\`,\n \`add(100, 200) = \${add(100, 200)}\`,\n \`add(-5, 10) = \${add(-5, 10)}\`,\n ].join('\\n');\n} catch (e) {\n out.textContent = 'Error: ' + e.message;\n}`, }, }; function getTailwindChecked() { const cb = $('#tailwind-checkbox'); return cb ? cb.checked : false; } async function run() { const mode = getCurrentMode(); const { html, css, js } = getEditorValues(); const cssType = getCssType(); try { const compiledCss = await compileCss(css, cssType); const result = await compileJs(js, mode); clearConsole(); // Show warnings from compilation (e.g., Svelte) if (result.warnings && result.warnings.length) { result.warnings.forEach((w) => { window.postMessage({ type: 'console', method: 'warn', args: [w] }, '*'); }); } const options = { tailwind: getTailwindChecked(), isModule: result.isModule || false, renderedHtml: result.renderedHtml || null, previewTheme: getPref('previewTheme'), resources: currentResources, }; renderPreview(html, compiledCss, result.js, mode, result.extraCss || '', options); } catch (e) { clearConsole(); renderPreview(html, '', '', mode, '', { tailwind: getTailwindChecked(), previewTheme: getPref('previewTheme'), resources: currentResources }); window.postMessage({ type: 'console', method: 'error', args: [`Compile error: ${e.message}`] }, '*'); } } function scheduleRun() { if (!getPref('autoRun')) return; clearTimeout(debounceTimer); debounceTimer = setTimeout(run, 500); } function showToast(msg) { const toast = $('#share-toast'); toast.textContent = msg; toast.classList.remove('hidden'); setTimeout(() => toast.classList.add('hidden'), 3000); } async function save() { const { html, css, js } = getEditorValues(); const title = $('#title-input').value || 'Untitled'; const css_type = getCssType(); const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript'; const listed = $('#listed-checkbox').checked ? 1 : 0; const tags = currentTags.slice(); const options = JSON.stringify({ tailwind: getTailwindChecked(), resources: currentResources }); try { if (currentId) { await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags }); showToast(`Saved! Share: ${location.origin}/f/${currentId}`); } else { const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags }); currentId = result.id; history.pushState(null, '', `/f/${currentId}`); showToast(`Saved! Share: ${location.origin}/f/${currentId}`); } } catch (e) { showToast(`Save failed: ${e.message}`); } } async function fork() { const { html, css, js } = getEditorValues(); const title = ($('#title-input').value || 'Untitled') + ' (fork)'; const css_type = getCssType(); const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript'; const listed = $('#listed-checkbox').checked ? 1 : 0; const tags = currentTags.slice(); const options = JSON.stringify({ tailwind: getTailwindChecked(), resources: currentResources }); try { const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags }); currentId = result.id; $('#title-input').value = title; history.pushState(null, '', `/f/${currentId}`); showToast(`Forked! New URL: ${location.origin}/f/${currentId}`); } catch (e) { showToast(`Fork failed: ${e.message}`); } } async function loadFromUrl() { const match = location.pathname.match(/^\/f\/([a-zA-Z0-9_-]+)$/); if (!match) return; try { const fiddle = await loadFiddle(match[1]); currentId = fiddle.id; $('#title-input').value = fiddle.title; // Restore mode from js_type const mode = JS_TYPE_TO_MODE[fiddle.js_type] || 'html-css-js'; $('#framework-mode').value = mode; switchMode(mode); // Restore CSS type setCssType(fiddle.css_type || 'css'); // Restore listed/tags $('#listed-checkbox').checked = fiddle.listed !== 0; currentTags = (fiddle.tags || []).map(t => t.name); renderTags(); // Restore options (tailwind checkbox, resources) const opts = JSON.parse(fiddle.options || '{}'); const twCb = $('#tailwind-checkbox'); if (twCb) twCb.checked = !!opts.tailwind; currentResources = opts.resources || []; renderResourceList(); setEditorValues(fiddle); setTimeout(run, 100); } catch (e) { showToast(`Failed to load fiddle: ${e.message}`); } } function handleModeChange(newMode) { const oldMode = getCurrentMode(); if (newMode === oldMode) return; switchMode(newMode); // Insert starter template if editors are empty const { html, css, js } = getEditorValues(); if (!html && !css && !js) { const template = STARTER_TEMPLATES[newMode]; if (template) { setEditorValues(template); } } scheduleRun(); } function applyLayout(layout) { const grid = $('.grid'); // Remove all layout classes grid.classList.remove('layout-top-bottom', 'layout-editor-only', 'layout-preview-only'); // Clear resizer inline styles when switching layouts clearInlineSizes(); if (layout === 'top-bottom') grid.classList.add('layout-top-bottom'); else if (layout === 'editor-only') grid.classList.add('layout-editor-only'); else if (layout === 'preview-only') grid.classList.add('layout-preview-only'); setPref('layout', layout); // Give DOM time to reflow then relayout editors requestAnimationFrame(() => relayoutEditors()); } async function init() { // Load Emmet before editors so completion providers are registered await initEmmet(); initEditors('html-css-js'); setOnChange(scheduleRun); initConsole(); initResizer(); initKeybindings(); // Auto-run checkbox const autoRunCb = $('#auto-run-checkbox'); autoRunCb.checked = getPref('autoRun'); autoRunCb.addEventListener('change', (e) => setPref('autoRun', e.target.checked)); // Tailwind checkbox const twCb = $('#tailwind-checkbox'); if (twCb) { twCb.addEventListener('change', () => scheduleRun()); } // Layout selector const layoutSel = $('#layout-mode'); const savedLayout = getPref('layout') || 'default'; layoutSel.value = savedLayout; if (savedLayout !== 'default') applyLayout(savedLayout); layoutSel.addEventListener('change', (e) => applyLayout(e.target.value)); // Mode selector $('#framework-mode').addEventListener('change', (e) => { handleModeChange(e.target.value); }); // Toolbar buttons $('#btn-run').addEventListener('click', run); $('#btn-save').addEventListener('click', save); $('#btn-fork').addEventListener('click', fork); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); save(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); run(); } // Escape closes any open modal if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden')); } // ? key opens shortcuts (only when not typing in an input/editor) if (e.key === '?' && !e.ctrlKey && !e.metaKey) { const tag = document.activeElement?.tagName; if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !document.activeElement?.closest('.editor-area')) { e.preventDefault(); $('#shortcuts-modal').classList.remove('hidden'); } } }); // Tags input const tagsInput = $('#tags-input'); tagsInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); const val = tagsInput.value.trim().replace(/,/g, ''); if (val && !currentTags.includes(val)) { currentTags.push(val); renderTags(); } tagsInput.value = ''; } }); // Load tag suggestions loadTagSuggestions(); // Export & QR buttons $('#btn-export').addEventListener('click', async () => { const mode = getCurrentMode(); const { html, css, js } = getEditorValues(); const cssType = getCssType(); const title = $('#title-input').value || 'Untitled'; try { const compiledCss = await compileCss(css, cssType); const result = await compileJs(js, mode); exportHtml({ title, html, css: compiledCss, js: result.js, mode, extraCss: result.extraCss, tailwind: getTailwindChecked(), isModule: result.isModule || false, renderedHtml: result.renderedHtml || null, previewTheme: getPref('previewTheme'), resources: currentResources, }); } catch (e) { showToast(`Export failed: ${e.message}`); } }); $('#btn-qr').addEventListener('click', () => { const url = currentId ? `${location.origin}/f/${currentId}` : location.href; showQrModal(url); }); // Preview theme selector const themeSel = $('#preview-theme'); const savedTheme = getPref('previewTheme'); themeSel.value = savedTheme; themeSel.addEventListener('change', (e) => { setPref('previewTheme', e.target.value); scheduleRun(); }); // Resources modal const resModal = $('#resources-modal'); $('#btn-resources').addEventListener('click', () => resModal.classList.remove('hidden')); $('#resources-modal-close').addEventListener('click', () => resModal.classList.add('hidden')); resModal.addEventListener('click', (e) => { if (e.target === resModal) resModal.classList.add('hidden'); }); $('#btn-add-css').addEventListener('click', () => { const input = $('#resource-css-input'); const url = input.value.trim(); if (url) { currentResources.push({ type: 'css', url }); input.value = ''; renderResourceList(); scheduleRun(); } }); $('#btn-add-js').addEventListener('click', () => { const input = $('#resource-js-input'); const url = input.value.trim(); if (url) { currentResources.push({ type: 'js', url }); input.value = ''; renderResourceList(); scheduleRun(); } }); $('#resource-css-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('#btn-add-css').click(); }); $('#resource-js-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('#btn-add-js').click(); }); // Shortcuts modal const scModal = $('#shortcuts-modal'); $('#btn-shortcuts').addEventListener('click', () => scModal.classList.remove('hidden')); $('#shortcuts-modal-close').addEventListener('click', () => scModal.classList.add('hidden')); scModal.addEventListener('click', (e) => { if (e.target === scModal) scModal.classList.add('hidden'); }); // Load fiddle from URL if present loadFromUrl(); // Handle browser back/forward window.addEventListener('popstate', () => { currentId = null; loadFromUrl(); }); } function renderTags() { const container = $('#tags-display'); container.innerHTML = ''; for (const tag of currentTags) { const pill = document.createElement('span'); pill.className = 'tag-pill'; pill.innerHTML = `${tag}`; pill.querySelector('.tag-remove').addEventListener('click', () => { currentTags = currentTags.filter(t => t !== tag); renderTags(); }); container.appendChild(pill); } } function renderResourceList() { const container = $('#resource-list'); container.innerHTML = ''; currentResources.forEach((r, i) => { const item = document.createElement('div'); item.className = 'resource-item'; item.innerHTML = `${r.type}${r.url}`; item.querySelector('.resource-remove').addEventListener('click', () => { currentResources.splice(i, 1); renderResourceList(); scheduleRun(); }); container.appendChild(item); }); } async function loadTagSuggestions() { try { const { tags } = await listTags(); const datalist = $('#tags-datalist'); datalist.innerHTML = ''; for (const t of tags) { const opt = document.createElement('option'); opt.value = t.name; datalist.appendChild(opt); } } catch (_) { /* ignore */ } } init();