readysite / website / frontend / components / MonacoEditor.jsx
2.9 KB
MonacoEditor.jsx
import * as React from 'react';
import { useEffect, useRef } from 'react';
import { useMonaco } from '../hooks/useMonaco.js';

export function MonacoEditor({
  value = '',
  onChange,
  onSave,
  language = 'html',
  theme = 'vs-dark',
}) {
  const containerRef = useRef(null);
  const editorRef = useRef(null);
  const isUpdatingRef = useRef(false);
  // Use refs for callbacks to avoid stale closures
  const onChangeRef = useRef(onChange);
  const onSaveRef = useRef(onSave);
  const { monaco, loading, error } = useMonaco();

  // Keep callback refs up to date
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);

  useEffect(() => {
    onSaveRef.current = onSave;
  }, [onSave]);

  // Create editor when Monaco is ready
  useEffect(() => {
    if (!monaco || !containerRef.current || editorRef.current) return;

    const editor = monaco.editor.create(containerRef.current, {
      value: value,
      language: language,
      theme: theme,
      automaticLayout: true,
      fontSize: 14,
      lineNumbers: 'on',
      minimap: { enabled: false },
      wordWrap: 'on',
      scrollBeyondLastLine: false,
      tabSize: 2,
    });

    editorRef.current = editor;

    // Handle content changes
    editor.onDidChangeModelContent(() => {
      if (isUpdatingRef.current) return;
      const newValue = editor.getValue();
      onChangeRef.current?.(newValue);
    });

    // Add Ctrl+S save command
    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
      onSaveRef.current?.();
    });

    return () => {
      editor.dispose();
      editorRef.current = null;
    };
  }, [monaco]);

  // Update value from external changes (e.g., version restore)
  useEffect(() => {
    const editor = editorRef.current;
    if (!editor) return;

    const currentValue = editor.getValue();
    if (value !== currentValue) {
      isUpdatingRef.current = true;

      // Preserve cursor position
      const position = editor.getPosition();
      editor.setValue(value);
      if (position) {
        editor.setPosition(position);
      }

      isUpdatingRef.current = false;
    }
  }, [value]);

  // Update theme when it changes
  useEffect(() => {
    if (monaco) {
      monaco.editor.setTheme(theme);
    }
  }, [monaco, theme]);

  // Update language when it changes
  useEffect(() => {
    const editor = editorRef.current;
    if (!editor || !monaco) return;

    const model = editor.getModel();
    if (model) {
      monaco.editor.setModelLanguage(model, language);
    }
  }, [monaco, language]);

  if (error) {
    return (
      <div className="flex items-center justify-center h-full bg-base-300 text-error p-4">
        Failed to load editor: {error.message}
      </div>
    );
  }

  if (loading) {
    return (
      <div className="flex items-center justify-center h-full bg-base-300">
        <span className="loading loading-spinner loading-lg" />
      </div>
    );
  }

  return <div ref={containerRef} className="w-full h-full" />;
}
← Back