byos_laravel/resources/js/codemirror-core.js
2026-01-05 14:43:30 +01:00

265 lines
8.4 KiB
JavaScript

import { EditorView, lineNumbers, keymap } from '@codemirror/view';
import { ViewPlugin } from '@codemirror/view';
import { indentWithTab, selectAll } from '@codemirror/commands';
import { foldGutter, foldKeymap } from '@codemirror/language';
import { history, historyKeymap } from '@codemirror/commands';
import { searchKeymap } from '@codemirror/search';
import { html } from '@codemirror/lang-html';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { css } from '@codemirror/lang-css';
import { liquid } from '@codemirror/lang-liquid';
import { oneDark } from '@codemirror/theme-one-dark';
import { githubLight } from '@fsegurai/codemirror-theme-github-light';
// Language support mapping
const LANGUAGE_MAP = {
'javascript': javascript,
'js': javascript,
'json': json,
'css': css,
'liquid': liquid,
'html': html,
};
// Theme support mapping
const THEME_MAP = {
'light': githubLight,
'dark': oneDark,
};
/**
* Get language support based on language parameter
* @param {string} language - Language name or comma-separated list
* @returns {Array|Extension} Language extension(s)
*/
function getLanguageSupport(language) {
// Handle comma-separated languages
if (language.includes(',')) {
const languages = language.split(',').map(lang => lang.trim().toLowerCase());
const languageExtensions = [];
languages.forEach(lang => {
const languageFn = LANGUAGE_MAP[lang];
if (languageFn) {
languageExtensions.push(languageFn());
}
});
return languageExtensions;
}
// Handle single language
const languageFn = LANGUAGE_MAP[language.toLowerCase()] || LANGUAGE_MAP.html;
return languageFn();
}
/**
* Get theme support
* @param {string} theme - Theme name
* @returns {Array} Theme extensions
*/
function getThemeSupport(theme) {
const themeFn = THEME_MAP[theme] || THEME_MAP.light;
return [themeFn];
}
/**
* Create a resize plugin that handles container resizing
* @returns {ViewPlugin} Resize plugin
*/
function createResizePlugin() {
return ViewPlugin.fromClass(class {
constructor(view) {
this.view = view;
this.resizeObserver = null;
this.setupResizeObserver();
}
setupResizeObserver() {
const container = this.view.dom.parentElement;
if (container) {
this.resizeObserver = new ResizeObserver(() => {
// Use requestAnimationFrame to ensure proper timing
requestAnimationFrame(() => {
this.view.requestMeasure();
});
});
this.resizeObserver.observe(container);
}
}
destroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
});
}
/**
* Get Flux-like theme styling based on theme
* @param {string} theme - Theme name ('light', 'dark', or 'auto')
* @returns {Object} Theme-specific styling
*/
function getFluxThemeStyling(theme) {
const isDark = theme === 'dark' || (theme === 'auto' && getSystemTheme() === 'dark');
if (isDark) {
return {
backgroundColor: 'oklab(0.999994 0.0000455678 0.0000200868 / 0.1)',
gutterBackgroundColor: 'oklch(26.9% 0 0)',
borderColor: '#374151',
focusBorderColor: 'rgb(224 91 68)',
};
} else {
return {
backgroundColor: '#fff', // zinc-50
gutterBackgroundColor: '#fafafa', // zinc-50
borderColor: '#e5e7eb', // gray-200
focusBorderColor: 'rgb(224 91 68)', // red-500
};
}
}
/**
* Create CodeMirror editor instance
* @param {HTMLElement} element - DOM element to mount editor
* @param {Object} options - Editor options
* @returns {EditorView} CodeMirror editor instance
*/
export function createCodeMirror(element, options = {}) {
const {
value = '',
language = 'html',
theme = 'light',
readOnly = false,
onChange = () => {},
onUpdate = () => {},
onBlur = () => {}
} = options;
// Get language and theme support
const languageSupport = getLanguageSupport(language);
const themeSupport = getThemeSupport(theme);
const fluxStyling = getFluxThemeStyling(theme);
// Create editor
const editor = new EditorView({
doc: value,
extensions: [
lineNumbers(),
foldGutter(),
history(),
EditorView.lineWrapping,
createResizePlugin(),
...(Array.isArray(languageSupport) ? languageSupport : [languageSupport]),
...themeSupport,
keymap.of([
indentWithTab,
...foldKeymap,
...historyKeymap,
...searchKeymap,
{
key: 'Mod-a',
run: selectAll,
},
]),
EditorView.theme({
'&': {
fontSize: '14px',
border: `1px solid ${fluxStyling.borderColor}`,
borderRadius: '0.375rem',
height: '100%',
maxHeight: '100%',
overflow: 'hidden',
backgroundColor: fluxStyling.backgroundColor + ' !important',
resize: 'vertical',
minHeight: '200px',
},
'.cm-gutters': {
borderTopLeftRadius: '0.375rem',
backgroundColor: fluxStyling.gutterBackgroundColor + ' !important',
},
'.cm-gutter': {
backgroundColor: fluxStyling.gutterBackgroundColor + ' !important',
},
'&.cm-focused': {
outline: 'none',
borderColor: fluxStyling.focusBorderColor,
},
'.cm-content': {
padding: '12px',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
height: '100%',
overflow: 'auto',
},
'.cm-editor': {
height: '100%',
},
'.cm-editor .cm-scroller': {
height: '100%',
overflow: 'auto',
},
'.cm-foldGutter': {
width: '12px',
},
'.cm-foldGutter .cm-gutterElement': {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: '12px',
color: '#6b7280',
},
'.cm-foldGutter .cm-gutterElement:hover': {
color: '#374151',
},
'.cm-foldGutter .cm-gutterElement.cm-folded': {
color: '#3b82f6',
}
}),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newValue = update.state.doc.toString();
onChange(newValue);
onUpdate(newValue);
}
}),
EditorView.domEventHandlers({
blur: onBlur
}),
EditorView.editable.of(!readOnly),
],
parent: element
});
return editor;
}
/**
* Auto-detect system theme preference
* @returns {string} 'dark' or 'light'
*/
export function getSystemTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
/**
* Watch for system theme changes
* @param {Function} callback - Callback function when theme changes
* @returns {Function} Unwatch function
*/
export function watchThemeChange(callback) {
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', callback);
return () => mediaQuery.removeEventListener('change', callback);
}
return () => {};
}