aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/editor/EditorArea.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/editor/EditorArea.tsx')
-rw-r--r--language-web/src/main/js/editor/EditorArea.tsx170
1 files changed, 140 insertions, 30 deletions
diff --git a/language-web/src/main/js/editor/EditorArea.tsx b/language-web/src/main/js/editor/EditorArea.tsx
index 531a57c9..678a632d 100644
--- a/language-web/src/main/js/editor/EditorArea.tsx
+++ b/language-web/src/main/js/editor/EditorArea.tsx
@@ -1,41 +1,151 @@
1import { Command, EditorView } from '@codemirror/view';
2import { closeSearchPanel, openSearchPanel } from '@codemirror/search';
3import { closeLintPanel, openLintPanel } from '@codemirror/lint';
1import { observer } from 'mobx-react-lite'; 4import { observer } from 'mobx-react-lite';
2import React, { useRef } from 'react'; 5import React, {
6 useCallback,
7 useEffect,
8 useRef,
9 useState,
10} from 'react';
3 11
12import { EditorParent } from './EditorParent';
4import { useRootStore } from '../RootStore'; 13import { useRootStore } from '../RootStore';
14import { getLogger } from '../utils/logger';
15
16const log = getLogger('editor.EditorArea');
17
18function usePanel(
19 panelId: string,
20 stateToSet: boolean,
21 editorView: EditorView | null,
22 openCommand: Command,
23 closeCommand: Command,
24 closeCallback: () => void,
25) {
26 const [cachedViewState, setCachedViewState] = useState<boolean>(false);
27 useEffect(() => {
28 if (editorView === null || cachedViewState === stateToSet) {
29 return;
30 }
31 if (stateToSet) {
32 openCommand(editorView);
33 const buttonQuery = `.cm-${panelId}.cm-panel button[name="close"]`;
34 const closeButton = editorView.dom.querySelector(buttonQuery);
35 if (closeButton) {
36 log.debug('Addig close button callback to', panelId, 'panel');
37 // We must remove the event listener added by CodeMirror from the button
38 // that dispatches a transaction without going through `EditorStorre`.
39 // Cloning a DOM node removes event listeners,
40 // see https://stackoverflow.com/a/9251864
41 const closeButtonWithoutListeners = closeButton.cloneNode(true);
42 closeButtonWithoutListeners.addEventListener('click', (event) => {
43 closeCallback();
44 event.preventDefault();
45 });
46 closeButton.replaceWith(closeButtonWithoutListeners);
47 } else {
48 log.error('Opened', panelId, 'panel has no close button');
49 }
50 } else {
51 closeCommand(editorView);
52 }
53 setCachedViewState(stateToSet);
54 }, [
55 stateToSet,
56 editorView,
57 cachedViewState,
58 panelId,
59 openCommand,
60 closeCommand,
61 closeCallback,
62 ]);
63 return setCachedViewState;
64}
65
66function fixCodeMirrorAccessibility(editorView: EditorView) {
67 // Reported by Lighthouse 8.3.0.
68 const { contentDOM } = editorView;
69 contentDOM.removeAttribute('aria-expanded');
70 contentDOM.setAttribute('aria-label', 'Code editor');
71}
5 72
6export const EditorArea = observer(() => { 73export const EditorArea = observer(() => {
7 const { editorStore } = useRootStore(); 74 const { editorStore } = useRootStore();
8 const { CodeMirror } = editorStore.chunk || {}; 75 const editorParentRef = useRef<HTMLDivElement | null>(null);
9 const fallbackTextarea = useRef<HTMLTextAreaElement>(null); 76 const [editorViewState, setEditorViewState] = useState<EditorView | null>(null);
10 77
11 if (!CodeMirror) { 78 const setSearchPanelOpen = usePanel(
12 return ( 79 'search',
13 <textarea 80 editorStore.showSearchPanel,
14 value={editorStore.value} 81 editorViewState,
15 onChange={(e) => editorStore.updateValue(e.target.value)} 82 openSearchPanel,
16 ref={fallbackTextarea} 83 closeSearchPanel,
17 className={`problem-fallback-editor cm-s-${editorStore.codeMirrorTheme}`} 84 useCallback(() => editorStore.setSearchPanelOpen(false), [editorStore]),
18 /> 85 );
19 ); 86
20 } 87 const setLintPanelOpen = usePanel(
21 88 'panel-lint',
22 const textarea = fallbackTextarea.current; 89 editorStore.showLintPanel,
23 if (textarea) { 90 editorViewState,
24 editorStore.setInitialSelection( 91 openLintPanel,
25 textarea.selectionStart, 92 closeLintPanel,
26 textarea.selectionEnd, 93 useCallback(() => editorStore.setLintPanelOpen(false), [editorStore]),
27 document.activeElement === textarea, 94 );
28 ); 95
29 } 96 useEffect(() => {
97 if (editorParentRef.current === null) {
98 // Nothing to clean up.
99 return () => {};
100 }
101
102 const editorView = new EditorView({
103 state: editorStore.state,
104 parent: editorParentRef.current,
105 dispatch: (transaction) => {
106 editorStore.onTransaction(transaction);
107 editorView.update([transaction]);
108 if (editorView.state !== editorStore.state) {
109 log.error(
110 'Failed to synchronize editor state - store state:',
111 editorStore.state,
112 'view state:',
113 editorView.state,
114 );
115 }
116 },
117 });
118 fixCodeMirrorAccessibility(editorView);
119 setEditorViewState(editorView);
120 setSearchPanelOpen(false);
121 setLintPanelOpen(false);
122 // `dispatch` is bound to the view instance,
123 // so it does not have to be called as a method.
124 // eslint-disable-next-line @typescript-eslint/unbound-method
125 editorStore.updateDispatcher(editorView.dispatch);
126 log.info('Editor created');
127
128 return () => {
129 editorStore.updateDispatcher(null);
130 editorView.destroy();
131 log.info('Editor destroyed');
132 };
133 }, [
134 editorParentRef,
135 editorStore,
136 setSearchPanelOpen,
137 setLintPanelOpen,
138 ]);
30 139
31 return ( 140 return (
32 <CodeMirror 141 <EditorParent
33 value={editorStore.value} 142 className="dark"
34 options={editorStore.codeMirrorOptions} 143 sx={{
35 editorDidMount={(editor) => editorStore.editorDidMount(editor)} 144 '.cm-lineNumbers': editorStore.showLineNumbers ? {} : {
36 editorWillUnmount={() => editorStore.editorWillUnmount()} 145 display: 'none !important',
37 onBeforeChange={(_editor, _data, value) => editorStore.updateValue(value)} 146 },
38 onChange={() => editorStore.reportChanged()} 147 }}
148 ref={editorParentRef}
39 /> 149 />
40 ); 150 );
41}); 151});