import { initEditors, switchMode, getEditorValues, setEditorValues, setOnChange, setOnTabSwitch, getCurrentMode, getCssType, setCssType, setOnFormat, setOnDiff, setEditorTheme, setEditorFont, 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, listVersions, getVersion, revertVersion, listCollections, createCollection, addToCollection, publishFiddle, unpublishFiddle, getPublishStatus, importGist, } 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, generateStandaloneHtml } from './export.js'; import { showQrModal } from './qr.js'; import { initDevtools } from './devtools.js'; import { initNetwork, clearNetwork } from './network-panel.js'; import { initElements, clearElements } from './elements-panel.js'; import { initPerformance, clearPerformance } from './performance-panel.js'; import { formatActiveEditor, formatAll } from './formatter.js'; import { initLinter, lintOnChange } from './linter.js'; import { toggleDiff, snapshotValues, onTabSwitch as diffOnTabSwitch } from './diff-view.js'; import { registerCustomThemes, THEMES } from './editor-themes.js'; import { GALLERY_TEMPLATES } from './templates.js'; import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js'; import { initNpmSearch } from './npm-search.js'; import { openSlideManager, addCurrentSlide, startPresentation, stopPresentation } from './presentation.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; }`, }, 'python': { js: `print("Hello from Python!")\n\nimport math\nprint(f"Pi is {math.pi:.4f}")\n\n# Use the sys module\nimport sys\nprint(f"Python {sys.version}")\n\n# List comprehension\nsquares = [x**2 for x in range(10)]\nprint(f"Squares: {squares}")`, html: '', }, '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(); clearNetwork(); clearElements(); clearPerformance(); // 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() { // Format on save if enabled if (getPref('formatOnSave')) { try { await formatAll(); } catch (_) { /* best effort */ } } 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 }); // Capture screenshot from preview iframe let screenshot = undefined; try { screenshot = await captureScreenshot(); } catch (_) { /* screenshot is optional */ } try { if (currentId) { await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags, screenshot }); snapshotValues(); 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; // Capture screenshot for new fiddle too if (screenshot) { try { await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags, screenshot }); } catch (_) {} } snapshotValues(); history.pushState(null, '', `/f/${currentId}`); showToast(`Saved! Share: ${location.origin}/f/${currentId}`); } } catch (e) { showToast(`Save failed: ${e.message}`); } } async function captureScreenshot() { const iframe = $('#preview-frame'); if (!iframe || !iframe.contentDocument) return undefined; try { // Use html2canvas to capture the iframe content if (!window.html2canvas) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; await new Promise((resolve, reject) => { script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } const canvas = await html2canvas(iframe.contentDocument.body, { width: 600, height: 400, scale: 1, useCORS: true, logging: false, backgroundColor: '#ffffff', }); // Resize to 600x400 const resized = document.createElement('canvas'); resized.width = 600; resized.height = 400; const ctx = resized.getContext('2d'); ctx.drawImage(canvas, 0, 0, 600, 400); return resized.toDataURL('image/jpeg', 0.7); } catch (_) { return undefined; } } 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); snapshotValues(); 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 ALL CDN scripts before editor creation so window.define // (Monaco's RequireJS) is never clobbered during Monarch tokenizer init await initEmmet(); await initLinter(); // Register custom Monaco themes before creating editors registerCustomThemes(); // Configure autocomplete: type defaults + snippet providers configureTypeDefaults(); registerSnippetProviders(); initEditors('html-css-js'); setOnChange(() => { scheduleRun(); lintOnChange(); }); setOnFormat(() => formatActiveEditor()); setOnDiff(() => toggleDiff()); setOnTabSwitch(diffOnTabSwitch); initConsole(); initDevtools(); initNetwork(); initElements(); initPerformance(); 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)); // Editor theme selector const themeSel2 = $('#editor-theme'); const savedEditorTheme = getPref('editorTheme') || 'vs-dark'; themeSel2.value = savedEditorTheme; themeSel2.addEventListener('change', (e) => { setEditorTheme(e.target.value); setPref('editorTheme', e.target.value); }); // Device preview toggles const viewport = $('#preview-viewport'); const savedDevice = getPref('previewDevice') || 'desktop'; if (savedDevice !== 'desktop') { viewport.classList.add(`device-${savedDevice}`); document.querySelectorAll('.device-btn').forEach(b => { b.classList.toggle('active', b.dataset.device === savedDevice); }); } document.querySelectorAll('.device-btn').forEach(btn => { btn.addEventListener('click', () => { const device = btn.dataset.device; viewport.classList.remove('device-tablet', 'device-mobile'); if (device !== 'desktop') viewport.classList.add(`device-${device}`); document.querySelectorAll('.device-btn').forEach(b => b.classList.toggle('active', b === btn)); setPref('previewDevice', device); }); }); // Templates gallery const tplModal = $('#templates-modal'); const tplGrid = $('#templates-grid'); $('#btn-templates').addEventListener('click', () => tplModal.classList.remove('hidden')); $('#templates-modal-close').addEventListener('click', () => tplModal.classList.add('hidden')); tplModal.addEventListener('click', (e) => { if (e.target === tplModal) tplModal.classList.add('hidden'); }); // Render template cards tplGrid.innerHTML = ''; for (const tpl of GALLERY_TEMPLATES) { const card = document.createElement('div'); card.className = 'template-card'; card.innerHTML = `
${tpl.icon}
${tpl.title}
${tpl.description}
${tpl.mode}
`; card.addEventListener('click', () => { // Switch mode $('#framework-mode').value = tpl.mode; handleModeChange(tpl.mode); // Set editor values setEditorValues({ html: tpl.html || '', css: tpl.css || '', js: tpl.js || '' }); // Close modal and run tplModal.classList.add('hidden'); run(); }); tplGrid.appendChild(card); } // Mode selector $('#framework-mode').addEventListener('change', (e) => { handleModeChange(e.target.value); }); // Format on save toggle const fmtCb = $('#format-save-checkbox'); fmtCb.checked = getPref('formatOnSave'); fmtCb.addEventListener('change', (e) => setPref('formatOnSave', e.target.checked)); // Editor font selector const fontSel = $('#editor-font'); const savedFont = getPref('editorFont') || 'default'; fontSel.value = savedFont; if (savedFont !== 'default') loadGoogleFont(savedFont); setEditorFont(savedFont); fontSel.addEventListener('change', (e) => { const font = e.target.value; if (font !== 'default') loadGoogleFont(font); setEditorFont(font); setPref('editorFont', font); }); // 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')); } // Shift+Alt+F — format code if (e.key === 'F' && e.shiftKey && e.altKey) { e.preventDefault(); formatActiveEditor(); } // Ctrl/Cmd+D — toggle diff if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); toggleDiff(); } // ? 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}`); } }); // Publish button $('#btn-publish').addEventListener('click', async () => { if (!currentId) { showToast('Save the fiddle first before publishing'); return; } 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); const standaloneHtml = generateStandaloneHtml({ 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, }); const pubResult = await publishFiddle(currentId, standaloneHtml); const fullUrl = `${location.origin}${pubResult.url}`; try { await navigator.clipboard.writeText(fullUrl); } catch (_) {} showToast(`Published! ${fullUrl} (copied)`); } catch (e) { showToast(`Publish failed: ${e.message}`); } }); // Import Gist button $('#btn-import-gist').addEventListener('click', async () => { const gistUrl = prompt('Paste a GitHub Gist URL:'); if (!gistUrl || !gistUrl.trim()) return; try { const data = await importGist(gistUrl.trim()); // Switch mode if (data.mode) { $('#framework-mode').value = data.mode; handleModeChange(data.mode); } // Set CSS type if (data.css_type && data.css_type !== 'css') { setCssType(data.css_type); } // Set editor values setEditorValues({ html: data.html || '', css: data.css || '', js: data.js || '' }); // Set title if (data.title) $('#title-input').value = data.title; run(); showToast('Gist imported successfully'); } catch (e) { showToast(`Import failed: ${e.message}`); } }); $('#btn-qr').addEventListener('click', () => { const url = currentId ? `${location.origin}/f/${currentId}` : location.href; showQrModal(url); }); // Version History modal const histModal = $('#history-modal'); $('#btn-history').addEventListener('click', () => openHistoryModal()); $('#history-modal-close').addEventListener('click', () => histModal.classList.add('hidden')); histModal.addEventListener('click', (e) => { if (e.target === histModal) histModal.classList.add('hidden'); }); // Embed modal const embedModal = $('#embed-modal'); $('#btn-embed').addEventListener('click', () => openEmbedModal()); $('#embed-modal-close').addEventListener('click', () => embedModal.classList.add('hidden')); embedModal.addEventListener('click', (e) => { if (e.target === embedModal) embedModal.classList.add('hidden'); }); ['embed-theme', 'embed-tabs', 'embed-autorun', 'embed-width', 'embed-height'].forEach(id => { $(`#${id}`).addEventListener('change', updateEmbedCode); $(`#${id}`).addEventListener('input', updateEmbedCode); }); $('#embed-copy').addEventListener('click', () => { navigator.clipboard.writeText($('#embed-code').textContent).then(() => showToast('Embed code copied!')); }); // Collection modal const colModal = $('#collection-modal'); $('#btn-collection').addEventListener('click', () => openCollectionModal()); $('#collection-modal-close').addEventListener('click', () => colModal.classList.add('hidden')); colModal.addEventListener('click', (e) => { if (e.target === colModal) colModal.classList.add('hidden'); }); $('#btn-create-collection').addEventListener('click', async () => { const name = $('#new-collection-name').value.trim(); if (!name) return; await createCollection({ name }); $('#new-collection-name').value = ''; openCollectionModal(); // refresh list }); // Presentation mode const presModal = $('#presentation-modal'); $('#btn-presentation').addEventListener('click', () => openSlideManager(currentId)); $('#presentation-modal-close').addEventListener('click', () => presModal.classList.add('hidden')); presModal.addEventListener('click', (e) => { if (e.target === presModal) presModal.classList.add('hidden'); }); $('#btn-add-slide').addEventListener('click', () => addCurrentSlide(currentId)); $('#btn-start-presentation').addEventListener('click', () => startPresentation(currentId)); $('#pres-exit').addEventListener('click', () => stopPresentation()); $('#pres-prev').addEventListener('click', () => { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); }); $('#pres-next').addEventListener('click', () => { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); }); // npm search in resources modal initNpmSearch((pkg) => { currentResources.push({ type: pkg.type, url: pkg.url }); renderResourceList(); scheduleRun(); }); // 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'); }); // Settings popover toggle const settingsPopover = $('#settings-popover'); $('#btn-settings').addEventListener('click', (e) => { e.stopPropagation(); settingsPopover.classList.toggle('hidden'); }); document.addEventListener('click', (e) => { if (!settingsPopover.classList.contains('hidden') && !e.target.closest('.settings-popover-wrap')) { settingsPopover.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 */ } } // ===================== Version History ===================== async function openHistoryModal() { if (!currentId) { showToast('Save the fiddle first to see history'); return; } const modal = $('#history-modal'); const list = $('#history-list'); const preview = $('#history-preview'); preview.classList.add('hidden'); modal.classList.remove('hidden'); try { const { versions } = await listVersions(currentId); if (!versions.length) { list.innerHTML = '
No versions yet. Versions are created each time you save.
'; return; } list.innerHTML = versions.map(v => `
v${v.version} ${new Date(v.created_at + 'Z').toLocaleString()}
`).join(''); list.querySelectorAll('.history-item').forEach(item => { item.addEventListener('click', async () => { list.querySelectorAll('.history-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); const ver = parseInt(item.dataset.version, 10); const vData = await getVersion(currentId, ver); showVersionPreview(vData); }); }); } catch (e) { list.innerHTML = `
Error: ${e.message}
`; } } function showVersionPreview(vData) { const preview = $('#history-preview'); const label = $('#history-preview-label'); const diff = $('#history-diff'); preview.classList.remove('hidden'); label.textContent = `Version ${vData.version} — ${new Date(vData.created_at + 'Z').toLocaleString()}`; const current = getEditorValues(); diff.innerHTML = ''; const sections = [ { label: 'HTML', old: vData.html, cur: current.html }, { label: 'CSS', old: vData.css, cur: current.css }, { label: 'JS', old: vData.js, cur: current.js }, ]; for (const s of sections) { if (s.old === s.cur) continue; const sec = document.createElement('div'); sec.className = 'history-diff-section'; sec.innerHTML = `
${s.label}
${escHtml(s.old || '(empty)')}
`; diff.appendChild(sec); } if (!diff.children.length) { diff.innerHTML = '
No changes from current version
'; } // Wire restore button const restoreBtn = $('#history-restore-btn'); restoreBtn.onclick = async () => { try { const result = await revertVersion(currentId, vData.version); setEditorValues({ html: result.html, css: result.css, js: result.js }); snapshotValues(); $('#history-modal').classList.add('hidden'); showToast('Restored version ' + vData.version); run(); } catch (e) { showToast('Restore failed: ' + e.message); } }; } function escHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); } // ===================== Embed Modal ===================== function openEmbedModal() { if (!currentId) { showToast('Save the fiddle first to generate embed code'); return; } $('#embed-modal').classList.remove('hidden'); updateEmbedCode(); } function updateEmbedCode() { if (!currentId) return; const theme = $('#embed-theme').value; const tabs = $('#embed-tabs').value; const autorun = $('#embed-autorun').value; const width = $('#embed-width').value || '100%'; const height = $('#embed-height').value || '400'; const params = new URLSearchParams(); if (theme !== 'light') params.set('theme', theme); if (tabs === '0') params.set('tabs', '0'); if (autorun === '0') params.set('run', '0'); const qs = params.toString(); const url = `${location.origin}/embed/${currentId}${qs ? '?' + qs : ''}`; const heightVal = /^\d+$/.test(height) ? height + 'px' : height; const snippet = ``; $('#embed-code').textContent = snippet; $('#embed-preview-frame').src = url; } // ===================== Collection Modal ===================== async function openCollectionModal() { if (!currentId) { showToast('Save the fiddle first to add to a collection'); return; } const modal = $('#collection-modal'); const list = $('#collection-list'); modal.classList.remove('hidden'); try { const { collections } = await listCollections(); if (!collections.length) { list.innerHTML = '
No collections yet
'; return; } list.innerHTML = collections.map(c => `
${escHtml(c.name)} ${c.fiddle_count} fiddles
`).join(''); list.querySelectorAll('.collection-add-btn').forEach(btn => { btn.addEventListener('click', async () => { const id = btn.closest('.collection-item').dataset.id; try { await addToCollection(id, currentId); btn.textContent = 'Added'; btn.disabled = true; } catch (e) { showToast('Failed to add: ' + e.message); } }); }); } catch (e) { list.innerHTML = `
Error: ${e.message}
`; } } // ===================== Google Fonts Loader ===================== const loadedFonts = new Set(); function loadGoogleFont(fontName) { if (loadedFonts.has(fontName)) return; loadedFonts.add(fontName); const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(/ /g, '+')}:wght@400;500;600;700&display=swap`; document.head.appendChild(link); } init();