- Auto-run toggle with localStorage persistence (toolbar checkbox) - Layout selector: default, top/bottom, editor-only, preview-only - Resizable panels via drag dividers with double-click reset - Emmet abbreviation expansion for HTML, CSS, and JSX - Vim and Emacs keybinding modes with lazy-loaded CDN libraries - Shared preferences module for localStorage management - Editor hooks for tab switch and mode change callbacks
235 lines
7.9 KiB
JavaScript
235 lines
7.9 KiB
JavaScript
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 } 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';
|
|
|
|
let currentId = null;
|
|
let debounceTimer = null;
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
|
|
const STARTER_TEMPLATES = {
|
|
'html-css-js': { html: '', css: '', js: '' },
|
|
'typescript': {
|
|
html: '<div id="app"></div>',
|
|
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 = \`<h1>\${result}</h1>\`;\nconsole.log(result);`,
|
|
},
|
|
'react': {
|
|
js: `const App = () => {\n const [count, setCount] = React.useState(0);\n return (\n <div>\n <h1>Hello React</h1>\n <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>\n </div>\n );\n};\n\nReactDOM.createRoot(document.getElementById('root')).render(<App />);`,
|
|
css: '',
|
|
html: '',
|
|
},
|
|
'react-ts': {
|
|
js: `const App: React.FC = () => {\n const [count, setCount] = React.useState<number>(0);\n return (\n <div>\n <h1>Hello React + TypeScript</h1>\n <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>\n </div>\n );\n};\n\nReactDOM.createRoot(document.getElementById('root')!).render(<App />);`,
|
|
css: '',
|
|
html: '',
|
|
},
|
|
'vue': {
|
|
js: `<template>\n <div>\n <h1>{{ msg }}</h1>\n <button @click="count++">Count: {{ count }}</button>\n </div>\n</template>\n\n<script>\nconst component = {\n data() {\n return {\n msg: 'Hello Vue!',\n count: 0,\n };\n },\n};\n</script>`,
|
|
css: '',
|
|
},
|
|
'svelte': {
|
|
js: `<script>\n let count = 0;\n</script>\n\n<h1>Hello Svelte</h1>\n<button on:click={() => count++}>Count: {count}</button>\n\n<style>\n h1 { color: #ff3e00; }\n button { padding: 8px 16px; cursor: pointer; }\n</style>`,
|
|
css: '',
|
|
},
|
|
};
|
|
|
|
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] }, '*');
|
|
});
|
|
}
|
|
|
|
renderPreview(html, compiledCss, result.js, mode, result.extraCss || '');
|
|
} catch (e) {
|
|
clearConsole();
|
|
renderPreview(html, '', '', mode);
|
|
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';
|
|
try {
|
|
if (currentId) {
|
|
await updateFiddle(currentId, { title, html, css, css_type, js, js_type });
|
|
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
|
} else {
|
|
const result = await createFiddle({ title, html, css, css_type, js, js_type });
|
|
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';
|
|
try {
|
|
const result = await createFiddle({ title, html, css, css_type, js, js_type });
|
|
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');
|
|
|
|
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));
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
// Load fiddle from URL if present
|
|
loadFromUrl();
|
|
|
|
// Handle browser back/forward
|
|
window.addEventListener('popstate', () => {
|
|
currentId = null;
|
|
loadFromUrl();
|
|
});
|
|
}
|
|
|
|
init();
|