all repos — snow-editor @ 65993a713586363acceab129af67161a5fbe71cc

small and cozy markdown, and orgmode editor

src/components/OrgEditor.jsx (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
import { Compartment, EditorState } from '@codemirror/state';
import {
  drawSelection,
  EditorView,
  highlightActiveLine,
  keymap,
  lineNumbers,
} from '@codemirror/view';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { org } from '@orgajs/cm-lang';
import { useEffect, useRef } from 'react';
import { checklistPlugin } from '../lib/org/checklistPlugin.js';
import { orgKeymap } from '../lib/org/keymap.js';
import { orgTheme } from '../lib/org/orgTheme.js';

export default function OrgEditor({
  value,
  onChange,
  readOnly = false,
  editorRef,
  ariaLabel,
  onRegisterScroll,
}) {
  const containerRef = useRef(null);
  const viewRef = useRef(null);
  const readOnlyRef = useRef(readOnly);
  const onChangeRef = useRef(onChange);
  const editableCompartment = useRef(new Compartment());

  readOnlyRef.current = readOnly;
  onChangeRef.current = onChange;

  useEffect(() => {
    if (!containerRef.current) return undefined;

    const updateListener = EditorView.updateListener.of((update) => {
      if (update.docChanged && onChangeRef.current) {
        onChangeRef.current(update.state.doc.toString());
      }
    });

    const state = EditorState.create({
      doc: value,
      extensions: [
        org(),
        orgTheme,
        lineNumbers(),
        highlightActiveLine(),
        drawSelection(),
        editableCompartment.current.of(EditorView.editable.of(!readOnlyRef.current)),
        EditorState.readOnly.of(readOnlyRef.current),
        EditorView.contentAttributes.of({ 'aria-label': ariaLabel }),
        updateListener,
        checklistPlugin(() => readOnlyRef.current),
        orgKeymap,
        keymap.of([...defaultKeymap, indentWithTab]),
      ],
    });

    const view = new EditorView({ state, parent: containerRef.current });
    viewRef.current = view;

    if (editorRef) {
      editorRef.current = {
        focus: () => view.focus(),
        getView: () => view,
      };
    }

    onRegisterScroll?.((lineNumber) => {
      const line = view.state.doc.line(
        Math.min(Math.max(1, lineNumber), view.state.doc.lines),
      );
      view.dispatch({
        effects: EditorView.scrollIntoView(line.from, { y: 'center' }),
        selection: { anchor: line.from },
      });
      view.focus();
    });

    return () => {
      view.destroy();
      viewRef.current = null;
      if (editorRef) editorRef.current = null;
    };
  }, [ariaLabel, editorRef, onRegisterScroll]);

  useEffect(() => {
    const view = viewRef.current;
    if (!view) return;

    const current = view.state.doc.toString();
    if (current !== value) {
      view.dispatch({
        changes: { from: 0, to: current.length, insert: value },
      });
    }
  }, [value]);

  useEffect(() => {
    const view = viewRef.current;
    if (!view) return;

    view.dispatch({
      effects: editableCompartment.current.reconfigure(
        EditorView.editable.of(!readOnly),
      ),
    });
  }, [readOnly]);

  return <div ref={containerRef} className="org-editor cm-host" />;
}