166 lines
3.9 KiB
React
166 lines
3.9 KiB
React
import React, { useEffect, useRef, memo } from 'react';
|
|
import * as monaco from 'monaco-editor';
|
|
import {loader} from '@monaco-editor/react';
|
|
|
|
loader.config({
|
|
paths: {
|
|
vs: '/monaco-editor/min/vs'
|
|
},
|
|
'vs/nls': {
|
|
availableLanguages: {},
|
|
},
|
|
// Specify which languages to load
|
|
languages: ['javascript', 'json', 'xml','plaintext']
|
|
});
|
|
|
|
const MonacoEditor = memo(({
|
|
id,
|
|
value = '',
|
|
language = 'javascript',
|
|
readOnly = false,
|
|
height = '64',
|
|
onChange = () => {}
|
|
}) => {
|
|
const editorRef = useRef(null);
|
|
const containerRef = useRef(null);
|
|
const subscriptionRef = useRef(null);
|
|
|
|
// Ensure value is always a string
|
|
const normalizedValue = value ?? '';
|
|
|
|
const getEditorOptions = (lang) => {
|
|
const baseOptions = {
|
|
automaticLayout: true,
|
|
theme: 'vs-light',
|
|
minimap: { enabled: false },
|
|
readOnly,
|
|
scrollBeyondLastLine: false,
|
|
fontSize: 14,
|
|
lineHeight: 21,
|
|
padding: { top: 8, bottom: 8 },
|
|
tabSize: 2,
|
|
scrollbar: {
|
|
vertical: 'auto',
|
|
horizontal: 'auto'
|
|
}
|
|
};
|
|
|
|
switch (lang) {
|
|
case 'json':
|
|
return {
|
|
...baseOptions,
|
|
formatOnPaste: true,
|
|
formatOnType: true,
|
|
autoClosingBrackets: 'always',
|
|
autoClosingQuotes: 'always',
|
|
bracketPairColorization: { enabled: true }
|
|
};
|
|
case 'xml':
|
|
return {
|
|
...baseOptions,
|
|
formatOnType: true,
|
|
autoClosingTags: true,
|
|
autoClosingBrackets: 'always',
|
|
autoClosingQuotes: 'always'
|
|
};
|
|
default:
|
|
return baseOptions;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const options = getEditorOptions(language);
|
|
const editor = monaco.editor.create(containerRef.current, {
|
|
...options,
|
|
value: normalizedValue,
|
|
language: language
|
|
});
|
|
|
|
editorRef.current = editor;
|
|
|
|
const subscription = editor.onDidChangeModelContent(() => {
|
|
const newValue = editor.getValue();
|
|
onChange(newValue);
|
|
});
|
|
|
|
subscriptionRef.current = subscription;
|
|
|
|
if (language === 'json' && normalizedValue) {
|
|
try {
|
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
|
validate: true,
|
|
allowComments: false,
|
|
schemas: []
|
|
});
|
|
|
|
const action = editor.getAction('editor.action.formatDocument');
|
|
if (action) {
|
|
setTimeout(() => {
|
|
action.run();
|
|
}, 100);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to format JSON:', e);
|
|
}
|
|
}
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
editor.layout();
|
|
});
|
|
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
subscription.dispose();
|
|
|
|
// Ensure proper cleanup
|
|
if (editor) {
|
|
try {
|
|
const model = editor.getModel();
|
|
if (model) {
|
|
model.dispose();
|
|
}
|
|
editor.dispose();
|
|
} catch (e) {
|
|
console.warn('Editor disposal error:', e);
|
|
}
|
|
}
|
|
};
|
|
}, [language]); // Only recreate when language changes
|
|
|
|
// Handle value updates
|
|
useEffect(() => {
|
|
if (editorRef.current) {
|
|
const currentValue = editorRef.current.getValue();
|
|
if (currentValue !== normalizedValue) {
|
|
editorRef.current.setValue(normalizedValue);
|
|
}
|
|
}
|
|
}, [normalizedValue]);
|
|
|
|
// Convert height to pixel value if it's a number
|
|
const containerHeight = typeof height === 'number'
|
|
? `${height}px`
|
|
: height.endsWith('px') ? height : `${height}px`;
|
|
|
|
return (
|
|
<div
|
|
id={id}
|
|
ref={containerRef}
|
|
style={{
|
|
minHeight: '100px',
|
|
height: containerHeight,
|
|
border: '1px solid #e2e8f0',
|
|
marginTop: '0.5rem'
|
|
}}
|
|
/>
|
|
);
|
|
});
|
|
|
|
MonacoEditor.displayName = 'MonacoEditor';
|
|
|
|
export default MonacoEditor;
|