Initial commit: code playground with multi-framework support
Express + SQLite backend with Monaco editor frontend. Supports HTML/CSS/JS, TypeScript, React (JSX/TSX), Vue SFC, and Svelte with live preview, console output, save/fork/share. Includes CSS preprocessors (SCSS, Less), framework-specific compilation (Babel, TypeScript, Svelte compiler), and CDN-loaded runtime libraries for preview rendering.
This commit is contained in:
258
public/js/editors.js
Normal file
258
public/js/editors.js
Normal file
@@ -0,0 +1,258 @@
|
||||
const editorOpts = {
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
theme: 'vs-dark',
|
||||
tabSize: 2,
|
||||
renderWhitespace: 'none',
|
||||
padding: { top: 6 },
|
||||
};
|
||||
|
||||
export const MODE_TABS = {
|
||||
'html-css-js': [
|
||||
{ id: 'html', label: 'HTML', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
{ id: 'js', label: 'JavaScript', lang: 'javascript' },
|
||||
],
|
||||
'typescript': [
|
||||
{ id: 'html', label: 'HTML', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
{ id: 'js', label: 'TypeScript', lang: 'typescript' },
|
||||
],
|
||||
'react': [
|
||||
{ id: 'js', label: 'JSX', lang: 'javascript' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
{ id: 'html', label: 'HTML', lang: 'html' },
|
||||
],
|
||||
'react-ts': [
|
||||
{ id: 'js', label: 'TSX', lang: 'typescript' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
{ id: 'html', label: 'HTML', lang: 'html' },
|
||||
],
|
||||
'vue': [
|
||||
{ id: 'js', label: 'Vue SFC', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
],
|
||||
'svelte': [
|
||||
{ id: 'js', label: 'Svelte', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
],
|
||||
};
|
||||
|
||||
// Map mode names to js_type values stored in DB
|
||||
export const MODE_TO_JS_TYPE = {
|
||||
'html-css-js': 'javascript',
|
||||
'typescript': 'typescript',
|
||||
'react': 'jsx',
|
||||
'react-ts': 'tsx',
|
||||
'vue': 'vue',
|
||||
'svelte': 'svelte',
|
||||
};
|
||||
|
||||
export const JS_TYPE_TO_MODE = Object.fromEntries(
|
||||
Object.entries(MODE_TO_JS_TYPE).map(([k, v]) => [v, k])
|
||||
);
|
||||
|
||||
// Editor instances keyed by tab id ('html', 'css', 'js')
|
||||
let editors = {};
|
||||
let currentMode = 'html-css-js';
|
||||
let activeTab = null;
|
||||
let cssType = 'css';
|
||||
let onChangeCallback = null;
|
||||
|
||||
const tabBar = () => document.getElementById('tab-bar');
|
||||
const editorArea = () => document.getElementById('editor-area');
|
||||
|
||||
export function setOnChange(cb) {
|
||||
onChangeCallback = cb;
|
||||
}
|
||||
|
||||
export function getCurrentMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
export function getCssType() {
|
||||
return cssType;
|
||||
}
|
||||
|
||||
export function setCssType(type) {
|
||||
cssType = type;
|
||||
const sel = document.getElementById('css-type-select');
|
||||
if (sel) sel.value = type;
|
||||
// Update Monaco language for the CSS editor
|
||||
if (editors.css) {
|
||||
const model = editors.css.getModel();
|
||||
monaco.editor.setModelLanguage(model, type === 'scss' ? 'scss' : type === 'less' ? 'less' : 'css');
|
||||
}
|
||||
}
|
||||
|
||||
function configureJsxSupport(mode) {
|
||||
if (mode === 'react' || mode === 'react-ts') {
|
||||
// Enable JSX in JavaScript defaults
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
jsxFactory: 'React.createElement',
|
||||
allowNonTsExtensions: true,
|
||||
allowJs: true,
|
||||
});
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
jsxFactory: 'React.createElement',
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createEditor(tabDef) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'editor-container';
|
||||
container.id = `editor-${tabDef.id}`;
|
||||
editorArea().appendChild(container);
|
||||
|
||||
const editor = monaco.editor.create(container, {
|
||||
...editorOpts,
|
||||
language: tabDef.lang,
|
||||
value: '',
|
||||
});
|
||||
|
||||
if (onChangeCallback) {
|
||||
editor.onDidChangeModelContent(onChangeCallback);
|
||||
}
|
||||
|
||||
return { container, editor };
|
||||
}
|
||||
|
||||
function renderTabBar(tabs) {
|
||||
const bar = tabBar();
|
||||
bar.innerHTML = '';
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'tab-btn';
|
||||
btn.dataset.tab = tab.id;
|
||||
btn.textContent = tab.label;
|
||||
|
||||
// Add CSS type selector inside the CSS tab
|
||||
if (tab.id === 'css') {
|
||||
const sel = document.createElement('select');
|
||||
sel.className = 'tab-css-type';
|
||||
sel.id = 'css-type-select';
|
||||
sel.innerHTML = `
|
||||
<option value="css">CSS</option>
|
||||
<option value="scss">SCSS</option>
|
||||
<option value="less">Less</option>
|
||||
`;
|
||||
sel.value = cssType;
|
||||
sel.addEventListener('click', (e) => e.stopPropagation());
|
||||
sel.addEventListener('change', (e) => {
|
||||
cssType = e.target.value;
|
||||
if (editors.css) {
|
||||
const model = editors.css.getModel();
|
||||
monaco.editor.setModelLanguage(model, cssType === 'scss' ? 'scss' : cssType === 'less' ? 'less' : 'css');
|
||||
}
|
||||
if (onChangeCallback) onChangeCallback();
|
||||
});
|
||||
btn.appendChild(sel);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', () => switchTab(tab.id));
|
||||
bar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
export function initEditors(mode = 'html-css-js') {
|
||||
currentMode = mode;
|
||||
const tabs = MODE_TABS[mode];
|
||||
|
||||
configureJsxSupport(mode);
|
||||
renderTabBar(tabs);
|
||||
|
||||
// Create editors for each tab
|
||||
tabs.forEach((tab) => {
|
||||
const { container, editor } = createEditor(tab);
|
||||
editors[tab.id] = editor;
|
||||
editors[`_container_${tab.id}`] = container;
|
||||
});
|
||||
|
||||
// Activate the first tab
|
||||
switchTab(tabs[0].id);
|
||||
|
||||
return editors;
|
||||
}
|
||||
|
||||
export function switchMode(mode) {
|
||||
if (mode === currentMode) return;
|
||||
|
||||
// Dispose existing editors
|
||||
const oldTabs = MODE_TABS[currentMode];
|
||||
oldTabs.forEach((tab) => {
|
||||
if (editors[tab.id]) {
|
||||
editors[tab.id].dispose();
|
||||
}
|
||||
const container = editors[`_container_${tab.id}`];
|
||||
if (container && container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
}
|
||||
});
|
||||
editors = {};
|
||||
|
||||
currentMode = mode;
|
||||
const tabs = MODE_TABS[mode];
|
||||
|
||||
configureJsxSupport(mode);
|
||||
renderTabBar(tabs);
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const { container, editor } = createEditor(tab);
|
||||
editors[tab.id] = editor;
|
||||
editors[`_container_${tab.id}`] = container;
|
||||
});
|
||||
|
||||
switchTab(tabs[0].id);
|
||||
}
|
||||
|
||||
export function switchTab(tabId) {
|
||||
activeTab = tabId;
|
||||
const tabs = MODE_TABS[currentMode];
|
||||
|
||||
// Update tab button states
|
||||
tabBar().querySelectorAll('.tab-btn').forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabId);
|
||||
});
|
||||
|
||||
// Show/hide editor containers
|
||||
tabs.forEach((tab) => {
|
||||
const container = editors[`_container_${tab.id}`];
|
||||
if (container) {
|
||||
container.classList.toggle('active', tab.id === tabId);
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger layout on the active editor
|
||||
if (editors[tabId]) {
|
||||
editors[tabId].layout();
|
||||
editors[tabId].focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function getEditorValues() {
|
||||
const values = { html: '', css: '', js: '' };
|
||||
if (editors.html) values.html = editors.html.getValue();
|
||||
if (editors.css) values.css = editors.css.getValue();
|
||||
if (editors.js) values.js = editors.js.getValue();
|
||||
return values;
|
||||
}
|
||||
|
||||
export function setEditorValues({ html = '', css = '', js = '' }) {
|
||||
if (editors.html) editors.html.setValue(html);
|
||||
if (editors.css) editors.css.setValue(css);
|
||||
if (editors.js) editors.js.setValue(js);
|
||||
}
|
||||
|
||||
export function getActiveTab() {
|
||||
return activeTab;
|
||||
}
|
||||
Reference in New Issue
Block a user