Files
fiddle/public/js/editors.js
root 26e232fd41 Fix syntax coloring, modernize toolbar UI, and clean up CSS
- Fix Monarch tokenizer loading: await initLinter() before editor creation
  so loadScript() doesn't clobber window.define during lazy tokenizer init
- Fix JSX/TSX coloring: use file URIs with proper extensions (.jsx/.tsx)
  so Monaco enables JSX tokenization via the TypeScript language service
- Modernize toolbar: move settings to gear popover, replace text buttons
  with SVG icons, consolidate toggle checkboxes into compact group
- Clean up CSS: remove duplicate toggle classes, dead selectors, orphaned rules
2026-02-27 15:19:10 -06:00

386 lines
11 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', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
],
'typescript': [
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'TypeScript', lang: 'typescript', ext: 'ts' },
],
'react': [
{ id: 'js', label: 'JSX', lang: 'javascript', ext: 'jsx' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
],
'react-ts': [
{ id: 'js', label: 'TSX', lang: 'typescript', ext: 'tsx' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
],
'vue': [
{ id: 'js', label: 'Vue SFC', lang: 'html', ext: 'vue' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'svelte': [
{ id: 'js', label: 'Svelte', lang: 'html', ext: 'svelte' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'markdown': [
{ id: 'js', label: 'Markdown', lang: 'markdown', ext: 'md' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'wasm': [
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
],
};
// 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());
}
}
let modelCounter = 0;
function createEditor(tabDef) {
const container = document.createElement('div');
container.className = 'editor-container';
container.id = `editor-${tabDef.id}`;
editorArea().appendChild(container);
const uri = monaco.Uri.parse(`file:///fiddle-${++modelCounter}.${tabDef.ext}`);
const model = monaco.editor.createModel('', tabDef.lang, uri);
const editor = monaco.editor.create(container, {
...editorOpts,
model,
});
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 tab-lang-${tab.lang}`;
btn.dataset.tab = tab.id;
const dot = document.createElement('span');
dot.className = 'tab-color-dot';
btn.appendChild(dot);
const label = document.createElement('span');
label.textContent = tab.label;
btn.appendChild(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 and their models
const oldTabs = MODE_TABS[currentMode];
oldTabs.forEach((tab) => {
if (editors[tab.id]) {
const model = editors[tab.id].getModel();
editors[tab.id].dispose();
if (model) model.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);
}
export function setEditorFont(fontFamily) {
const family = fontFamily === 'default' ? "'Cascadia Code', 'Fira Code', monospace" : `'${fontFamily}', monospace`;
editorOpts.fontFamily = family;
const tabs = MODE_TABS[currentMode];
if (!tabs) return;
tabs.forEach((tab) => {
if (editors[tab.id]) editors[tab.id].updateOptions({ fontFamily: family });
});
}