- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%) - Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark) - Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.) - Code autocomplete with DOM/React type definitions and snippet completions - Devtools panels: console, network, elements, performance - Code formatter (Prettier), diff view, and linter integration
363 lines
9.6 KiB
JavaScript
363 lines
9.6 KiB
JavaScript
import { getPref } from './preferences.js';
|
|
|
|
const editorOpts = {
|
|
minimap: { enabled: false },
|
|
automaticLayout: true,
|
|
fontSize: 13,
|
|
lineNumbers: 'on',
|
|
scrollBeyondLastLine: false,
|
|
theme: getPref('editorTheme') || 'vs-dark',
|
|
tabSize: 2,
|
|
renderWhitespace: 'none',
|
|
padding: { top: 6 },
|
|
quickSuggestions: { other: true, comments: false, strings: true },
|
|
suggestOnTriggerCharacters: true,
|
|
acceptSuggestionOnEnter: 'on',
|
|
parameterHints: { enabled: true },
|
|
wordBasedSuggestions: 'currentDocument',
|
|
suggest: {
|
|
snippetsPreventQuickSuggestions: false,
|
|
showSnippets: true,
|
|
showWords: true,
|
|
showKeywords: true,
|
|
showMethods: true,
|
|
showFunctions: true,
|
|
showVariables: true,
|
|
showClasses: true,
|
|
showInterfaces: true,
|
|
showProperties: true,
|
|
showEvents: true,
|
|
showConstants: true,
|
|
},
|
|
autoClosingBrackets: 'always',
|
|
autoClosingQuotes: 'always',
|
|
autoSurround: 'languageDefined',
|
|
bracketPairColorization: { enabled: true },
|
|
};
|
|
|
|
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' },
|
|
],
|
|
'markdown': [
|
|
{ id: 'js', label: 'Markdown', lang: 'markdown' },
|
|
{ id: 'css', label: 'CSS', lang: 'css' },
|
|
],
|
|
'wasm': [
|
|
{ id: 'html', label: 'HTML', lang: 'html' },
|
|
{ id: 'css', label: 'CSS', lang: 'css' },
|
|
{ id: 'js', label: 'JavaScript', lang: 'javascript' },
|
|
],
|
|
};
|
|
|
|
// 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',
|
|
'markdown': 'markdown',
|
|
'wasm': 'wasm',
|
|
};
|
|
|
|
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 tabSwitchCallbacks = [];
|
|
let modeChangeCallbacks = [];
|
|
let onFormatCallback = null;
|
|
let onDiffCallback = null;
|
|
|
|
const tabBar = () => document.getElementById('tab-bar');
|
|
const editorArea = () => document.getElementById('editor-area');
|
|
|
|
export function setOnChange(cb) {
|
|
onChangeCallback = cb;
|
|
}
|
|
|
|
export function setOnTabSwitch(cb) {
|
|
tabSwitchCallbacks.push(cb);
|
|
}
|
|
|
|
export function setOnModeChange(cb) {
|
|
modeChangeCallbacks.push(cb);
|
|
}
|
|
|
|
export function setOnFormat(cb) {
|
|
onFormatCallback = cb;
|
|
}
|
|
|
|
export function setOnDiff(cb) {
|
|
onDiffCallback = 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') {
|
|
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
|
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
jsxFactory: 'React.createElement',
|
|
allowNonTsExtensions: true,
|
|
allowJs: true,
|
|
lib: ['esnext', 'dom', 'dom.iterable'],
|
|
});
|
|
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
jsxFactory: 'React.createElement',
|
|
allowNonTsExtensions: true,
|
|
lib: ['esnext', 'dom', 'dom.iterable'],
|
|
});
|
|
// Add React type definitions for IntelliSense
|
|
import('./autocomplete.js').then(m => m.addReactTypes());
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
// Spacer + Format + Diff buttons
|
|
const spacer = document.createElement('span');
|
|
spacer.className = 'tab-bar-spacer';
|
|
bar.appendChild(spacer);
|
|
|
|
const fmtBtn = document.createElement('button');
|
|
fmtBtn.className = 'tab-bar-btn format-btn';
|
|
fmtBtn.textContent = 'Format';
|
|
fmtBtn.title = 'Format code (Shift+Alt+F)';
|
|
fmtBtn.addEventListener('click', () => {
|
|
if (onFormatCallback) onFormatCallback();
|
|
});
|
|
bar.appendChild(fmtBtn);
|
|
|
|
const diffBtn = document.createElement('button');
|
|
diffBtn.className = 'tab-bar-btn diff-btn';
|
|
diffBtn.textContent = 'Diff';
|
|
diffBtn.title = 'Toggle diff view (Ctrl+D)';
|
|
diffBtn.addEventListener('click', () => {
|
|
if (onDiffCallback) onDiffCallback();
|
|
});
|
|
bar.appendChild(diffBtn);
|
|
}
|
|
|
|
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);
|
|
|
|
modeChangeCallbacks.forEach(cb => cb(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();
|
|
}
|
|
|
|
tabSwitchCallbacks.forEach(cb => cb(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;
|
|
}
|
|
|
|
export function setEditorTheme(themeId) {
|
|
editorOpts.theme = themeId;
|
|
monaco.editor.setTheme(themeId);
|
|
}
|