diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-08-16 21:14:50 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-08-16 21:14:50 +0200 |
commit | 19cd11118cde7160cd447c81bc965007c0437479 (patch) | |
tree | 5fea613e7a46d69380995368a68cc72f186078a4 /subprojects/frontend/src/editor/EditorArea.tsx | |
parent | chore(deps): bump frontend dependencies (diff) | |
download | refinery-19cd11118cde7160cd447c81bc965007c0437479.tar.gz refinery-19cd11118cde7160cd447c81bc965007c0437479.tar.zst refinery-19cd11118cde7160cd447c81bc965007c0437479.zip |
refactor(frondend): improve editor store and theme
Also bumps frontend dependencies.
Diffstat (limited to 'subprojects/frontend/src/editor/EditorArea.tsx')
-rw-r--r-- | subprojects/frontend/src/editor/EditorArea.tsx | 138 |
1 files changed, 15 insertions, 123 deletions
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index d4305610..e5712461 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx | |||
@@ -1,139 +1,31 @@ | |||
1 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; | 1 | import { useTheme } from '@mui/material/styles'; |
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { type Command, EditorView } from '@codemirror/view'; | ||
4 | import { observer } from 'mobx-react-lite'; | 2 | import { observer } from 'mobx-react-lite'; |
5 | import React, { useCallback, useEffect, useRef, useState } from 'react'; | 3 | import React, { useCallback, useEffect } from 'react'; |
6 | 4 | ||
7 | import { useRootStore } from '../RootStore'; | 5 | import { useRootStore } from '../RootStore'; |
8 | import getLogger from '../utils/getLogger'; | ||
9 | 6 | ||
10 | import EditorParent from './EditorParent'; | 7 | import EditorTheme from './EditorTheme'; |
11 | |||
12 | const log = getLogger('editor.EditorArea'); | ||
13 | |||
14 | function usePanel( | ||
15 | panelId: string, | ||
16 | stateToSet: boolean, | ||
17 | editorView: EditorView | null, | ||
18 | openCommand: Command, | ||
19 | closeCommand: Command, | ||
20 | closeCallback: () => void, | ||
21 | ) { | ||
22 | const [cachedViewState, setCachedViewState] = useState<boolean>(false); | ||
23 | useEffect(() => { | ||
24 | if (editorView === null || cachedViewState === stateToSet) { | ||
25 | return; | ||
26 | } | ||
27 | if (stateToSet) { | ||
28 | openCommand(editorView); | ||
29 | const buttonQuery = `.cm-${panelId}.cm-panel button[name="close"]`; | ||
30 | const closeButton = editorView.dom.querySelector(buttonQuery); | ||
31 | if (closeButton) { | ||
32 | log.debug('Addig close button callback to', panelId, 'panel'); | ||
33 | // We must remove the event listener added by CodeMirror from the button | ||
34 | // that dispatches a transaction without going through `EditorStorre`. | ||
35 | // Cloning a DOM node removes event listeners, | ||
36 | // see https://stackoverflow.com/a/9251864 | ||
37 | const closeButtonWithoutListeners = closeButton.cloneNode(true); | ||
38 | closeButtonWithoutListeners.addEventListener('click', (event) => { | ||
39 | closeCallback(); | ||
40 | event.preventDefault(); | ||
41 | }); | ||
42 | closeButton.replaceWith(closeButtonWithoutListeners); | ||
43 | } else { | ||
44 | log.error('Opened', panelId, 'panel has no close button'); | ||
45 | } | ||
46 | } else { | ||
47 | closeCommand(editorView); | ||
48 | } | ||
49 | setCachedViewState(stateToSet); | ||
50 | }, [ | ||
51 | stateToSet, | ||
52 | editorView, | ||
53 | cachedViewState, | ||
54 | panelId, | ||
55 | openCommand, | ||
56 | closeCommand, | ||
57 | closeCallback, | ||
58 | ]); | ||
59 | return setCachedViewState; | ||
60 | } | ||
61 | |||
62 | function fixCodeMirrorAccessibility(editorView: EditorView) { | ||
63 | // Reported by Lighthouse 8.3.0. | ||
64 | const { contentDOM } = editorView; | ||
65 | contentDOM.removeAttribute('aria-expanded'); | ||
66 | contentDOM.setAttribute('aria-label', 'Code editor'); | ||
67 | } | ||
68 | 8 | ||
69 | function EditorArea(): JSX.Element { | 9 | function EditorArea(): JSX.Element { |
70 | const { editorStore } = useRootStore(); | 10 | const { editorStore } = useRootStore(); |
71 | const editorParentRef = useRef<HTMLDivElement | null>(null); | 11 | const { |
72 | const [editorViewState, setEditorViewState] = useState<EditorView | null>( | 12 | palette: { mode: paletteMode }, |
73 | null, | 13 | } = useTheme(); |
74 | ); | ||
75 | 14 | ||
76 | const setSearchPanelOpen = usePanel( | 15 | useEffect( |
77 | 'search', | 16 | () => editorStore.setDarkMode(paletteMode === 'dark'), |
78 | editorStore.showSearchPanel, | 17 | [editorStore, paletteMode], |
79 | editorViewState, | ||
80 | openSearchPanel, | ||
81 | closeSearchPanel, | ||
82 | useCallback(() => editorStore.setSearchPanelOpen(false), [editorStore]), | ||
83 | ); | 18 | ); |
84 | 19 | ||
85 | const setLintPanelOpen = usePanel( | 20 | const editorParentRef = useCallback( |
86 | 'panel-lint', | 21 | (editorParent: HTMLDivElement | null) => { |
87 | editorStore.showLintPanel, | 22 | editorStore.setEditorParent(editorParent); |
88 | editorViewState, | 23 | }, |
89 | openLintPanel, | 24 | [editorStore], |
90 | closeLintPanel, | ||
91 | useCallback(() => editorStore.setLintPanelOpen(false), [editorStore]), | ||
92 | ); | 25 | ); |
93 | 26 | ||
94 | useEffect(() => { | ||
95 | if (editorParentRef.current === null) { | ||
96 | return () => { | ||
97 | // Nothing to clean up. | ||
98 | }; | ||
99 | } | ||
100 | |||
101 | const editorView = new EditorView({ | ||
102 | state: editorStore.state, | ||
103 | parent: editorParentRef.current, | ||
104 | dispatch: (transaction) => { | ||
105 | editorStore.onTransaction(transaction); | ||
106 | editorView.update([transaction]); | ||
107 | if (editorView.state !== editorStore.state) { | ||
108 | log.error( | ||
109 | 'Failed to synchronize editor state - store state:', | ||
110 | editorStore.state, | ||
111 | 'view state:', | ||
112 | editorView.state, | ||
113 | ); | ||
114 | } | ||
115 | }, | ||
116 | }); | ||
117 | fixCodeMirrorAccessibility(editorView); | ||
118 | setEditorViewState(editorView); | ||
119 | setSearchPanelOpen(false); | ||
120 | setLintPanelOpen(false); | ||
121 | // `dispatch` is bound to the view instance, | ||
122 | // so it does not have to be called as a method. | ||
123 | // eslint-disable-next-line @typescript-eslint/unbound-method | ||
124 | editorStore.updateDispatcher(editorView.dispatch); | ||
125 | log.info('Editor created'); | ||
126 | |||
127 | return () => { | ||
128 | editorStore.updateDispatcher(null); | ||
129 | editorView.destroy(); | ||
130 | log.info('Editor destroyed'); | ||
131 | }; | ||
132 | }, [editorStore, setSearchPanelOpen, setLintPanelOpen]); | ||
133 | |||
134 | return ( | 27 | return ( |
135 | <EditorParent | 28 | <EditorTheme |
136 | className="dark" | ||
137 | showLineNumbers={editorStore.showLineNumbers} | 29 | showLineNumbers={editorStore.showLineNumbers} |
138 | ref={editorParentRef} | 30 | ref={editorParentRef} |
139 | /> | 31 | /> |