- 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
286 lines
7.4 KiB
JavaScript
286 lines
7.4 KiB
JavaScript
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 = `
|
|
<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);
|
|
|
|
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;
|
|
}
|