OrgEditor.jsx (3046B)
1 import { Compartment, EditorState } from '@codemirror/state'; 2 import { 3 drawSelection, 4 EditorView, 5 highlightActiveLine, 6 keymap, 7 lineNumbers, 8 } from '@codemirror/view'; 9 import { defaultKeymap, indentWithTab } from '@codemirror/commands'; 10 import { org } from '@orgajs/cm-lang'; 11 import { useEffect, useRef } from 'react'; 12 import { checklistPlugin } from '../lib/org/checklistPlugin.js'; 13 import { orgKeymap } from '../lib/org/keymap.js'; 14 import { orgTheme } from '../lib/org/orgTheme.js'; 15 16 export default function OrgEditor({ 17 value, 18 onChange, 19 readOnly = false, 20 editorRef, 21 ariaLabel, 22 onRegisterScroll, 23 }) { 24 const containerRef = useRef(null); 25 const viewRef = useRef(null); 26 const readOnlyRef = useRef(readOnly); 27 const onChangeRef = useRef(onChange); 28 const editableCompartment = useRef(new Compartment()); 29 30 readOnlyRef.current = readOnly; 31 onChangeRef.current = onChange; 32 33 useEffect(() => { 34 if (!containerRef.current) return undefined; 35 36 const updateListener = EditorView.updateListener.of((update) => { 37 if (update.docChanged && onChangeRef.current) { 38 onChangeRef.current(update.state.doc.toString()); 39 } 40 }); 41 42 const state = EditorState.create({ 43 doc: value, 44 extensions: [ 45 org(), 46 orgTheme, 47 lineNumbers(), 48 highlightActiveLine(), 49 drawSelection(), 50 editableCompartment.current.of(EditorView.editable.of(!readOnlyRef.current)), 51 EditorState.readOnly.of(readOnlyRef.current), 52 EditorView.contentAttributes.of({ 'aria-label': ariaLabel }), 53 updateListener, 54 checklistPlugin(() => readOnlyRef.current), 55 orgKeymap, 56 keymap.of([...defaultKeymap, indentWithTab]), 57 ], 58 }); 59 60 const view = new EditorView({ state, parent: containerRef.current }); 61 viewRef.current = view; 62 63 if (editorRef) { 64 editorRef.current = { 65 focus: () => view.focus(), 66 getView: () => view, 67 }; 68 } 69 70 onRegisterScroll?.((lineNumber) => { 71 const line = view.state.doc.line( 72 Math.min(Math.max(1, lineNumber), view.state.doc.lines), 73 ); 74 view.dispatch({ 75 effects: EditorView.scrollIntoView(line.from, { y: 'center' }), 76 selection: { anchor: line.from }, 77 }); 78 view.focus(); 79 }); 80 81 return () => { 82 view.destroy(); 83 viewRef.current = null; 84 if (editorRef) editorRef.current = null; 85 }; 86 }, [ariaLabel, editorRef, onRegisterScroll]); 87 88 useEffect(() => { 89 const view = viewRef.current; 90 if (!view) return; 91 92 const current = view.state.doc.toString(); 93 if (current !== value) { 94 view.dispatch({ 95 changes: { from: 0, to: current.length, insert: value }, 96 }); 97 } 98 }, [value]); 99 100 useEffect(() => { 101 const view = viewRef.current; 102 if (!view) return; 103 104 view.dispatch({ 105 effects: editableCompartment.current.reconfigure( 106 EditorView.editable.of(!readOnly), 107 ), 108 }); 109 }, [readOnly]); 110 111 return <div ref={containerRef} className="org-editor cm-host" />; 112 }