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;
let onTabSwitchCallback = null;
let onModeChangeCallback = null;
const tabBar = () => document.getElementById('tab-bar');
const editorArea = () => document.getElementById('editor-area');
export function setOnChange(cb) {
onChangeCallback = cb;
}
export function setOnTabSwitch(cb) {
onTabSwitchCallback = cb;
}
export function setOnModeChange(cb) {
onModeChangeCallback = cb;
}
export function getActiveEditor() {
if (activeTab && editors[activeTab]) return editors[activeTab];
return null;
}
export function relayoutEditors() {
const tabs = MODE_TABS[currentMode];
if (!tabs) return;
tabs.forEach((tab) => {
if (editors[tab.id]) editors[tab.id].layout();
});
}
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 = `
`;
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);
if (onModeChangeCallback) onModeChangeCallback(mode);
}
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();
}
if (onTabSwitchCallback) onTabSwitchCallback(tabId, editors[tabId]);
}
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;
}