|
1 | | -import componentMap from "./components/componentMap" |
2 | | -import keyDownTypeFor from "./keyDownTypeFor" |
3 | 1 | import React from "react" |
4 | | -import ReactDOM from "react-dom" |
5 | | -import testForSelection from "./useEditor/testForSelection" |
6 | 2 |
|
7 | | -import { |
8 | | - initElementsFromChildren, |
9 | | - parseRenderedChildren, |
10 | | -} from "./parsers" |
11 | | - |
12 | | -import { |
13 | | - convRangeToUserLiteral, |
14 | | - getCurrentRange, |
15 | | -} from "./Range" |
16 | | - |
17 | | -import "./Editor.css" |
18 | | - |
19 | | -const MemoElements = React.memo(({ elements }) => ( |
20 | | - elements.map(each => ( |
21 | | - React.createElement(componentMap[each.type], { |
22 | | - ...each.props, |
23 | | - key: each.key, // For React |
24 | | - id: each.key, // For the DOM |
25 | | - }) |
26 | | - )) |
27 | | -)) |
28 | | - |
29 | | -// Renders a read-write editor. |
30 | | -const Editor = ({ id, className, style, state, dispatch, children }) => { |
31 | | - const ref = React.useRef(null) |
32 | | - const pointerdownRef = React.useRef(false) |
33 | | - |
34 | | - // Mounts elements from props.children (once). |
35 | | - // |
36 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
37 | | - React.useLayoutEffect( |
38 | | - React.useCallback(() => { |
39 | | - const elements = initElementsFromChildren(children) |
40 | | - dispatch({ |
41 | | - type: "MOUNT_ELEMENTS", |
42 | | - elements, |
43 | | - }) |
44 | | - }, [dispatch, children]), |
45 | | - [], |
46 | | - ) |
47 | | - |
48 | | - // Rerenders on state.shouldRerender. |
49 | | - // |
50 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
51 | | - React.useLayoutEffect( |
52 | | - React.useCallback(() => { |
53 | | - // https://bugs.chromium.org/p/chromium/issues/detail?id=138439#c10 |
54 | | - const selection = document.getSelection() |
55 | | - if (selection.rangeCount) { |
56 | | - selection.removeAllRanges() |
57 | | - } |
58 | | - ReactDOM.render( |
59 | | - <MemoElements elements={state.elements} />, |
60 | | - ref.current, |
61 | | - () => { |
62 | | - if (!state.focused) { |
63 | | - // No-op |
64 | | - return |
65 | | - } |
66 | | - try { |
67 | | - const userRange = convRangeToUserLiteral(state.range) |
68 | | - selection.addRange(userRange) |
69 | | - } catch (error) { |
70 | | - console.error(error) |
71 | | - } |
72 | | - }, |
73 | | - ) |
74 | | - }, [state]), |
75 | | - [state.shouldRerender], |
76 | | - ) |
| 3 | +const Editor = ({ |
| 4 | + state, |
| 5 | + dispatch, |
| 6 | +}) => { |
| 7 | + // ... |
77 | 8 |
|
78 | 9 | return ( |
79 | | - <article |
80 | | - ref={ref} |
81 | | - |
82 | | - id={id} |
83 | | - className={`em-context ${className}`.trim()} |
84 | | - style={style} |
85 | | - |
86 | | - onFocus={e => { |
87 | | - dispatch({ |
88 | | - type: "FOCUS", |
89 | | - }) |
90 | | - }} |
91 | | - |
92 | | - onBlur={e => { |
93 | | - dispatch({ |
94 | | - type: "BLUR", |
95 | | - }) |
96 | | - }} |
97 | | - |
98 | | - onPointerDown={e => { |
99 | | - pointerdownRef.current = true |
100 | | - }} |
101 | | - |
102 | | - onPointerMove={e => { |
103 | | - if (!state.focused || !pointerdownRef.current) { |
104 | | - if (!state.focused && pointerdownRef.current) { |
105 | | - pointerdownRef.current = false |
106 | | - } |
107 | | - return |
108 | | - } |
109 | | - const range = getCurrentRange(ref.current) |
110 | | - if (!range) { |
111 | | - // No-op |
112 | | - return |
113 | | - } |
114 | | - dispatch({ |
115 | | - type: "SELECT", |
116 | | - range, |
117 | | - }) |
118 | | - }} |
119 | | - |
120 | | - onPointerUp={e => { |
121 | | - pointerdownRef.current = false |
122 | | - }} |
123 | | - |
124 | | - // TODO: Add COMPAT guard for select-all or prevent |
125 | | - // default? |
126 | | - onSelect={e => { |
127 | | - try { |
128 | | - const range = getCurrentRange(ref.current) |
129 | | - if (!range) { |
130 | | - // No-op |
131 | | - return |
132 | | - } |
133 | | - dispatch({ |
134 | | - type: "SELECT", |
135 | | - range, |
136 | | - }) |
137 | | - } catch (error) { |
138 | | - console.error(error) |
139 | | - } |
140 | | - }} |
141 | | - |
142 | | - // TODO: e.nativeEvent.isComposing or |
143 | | - // "insert-text-composed" needs to negate |
144 | | - // state.rangeTypes because the insertion point is |
145 | | - // advanced during composition. |
146 | | - onKeyDown={e => { |
147 | | - let formatType = "" |
148 | | - let text = "" |
149 | | - let deleteType = "" |
150 | | - |
151 | | - const keyDownType = keyDownTypeFor(e) |
152 | | - if (keyDownType) { |
153 | | - console.log(keyDownType) |
154 | | - } |
155 | | - |
156 | | - switch (keyDownType) { |
157 | | - case "apply-format-plaintext": |
158 | | - case "apply-format-em": |
159 | | - case "apply-format-strong": |
160 | | - case "apply-format-code": |
161 | | - case "apply-format-strike": |
162 | | - case "apply-format-a": |
163 | | - // NOTE: Formatting events must always be |
164 | | - // prevented. |
165 | | - e.preventDefault() |
166 | | - if (testForSelection(state)) { |
167 | | - formatType = keyDownType.slice("apply-format-".length) |
168 | | - const types = {} |
169 | | - if (formatType !== "plaintext") { |
170 | | - types[formatType] = {} // TODO |
171 | | - } |
172 | | - dispatch({ |
173 | | - type: "APPLY_TYPES", |
174 | | - types, |
175 | | - }) |
176 | | - } |
177 | | - break |
178 | | - case "apply-format-markdown-em": |
179 | | - case "apply-format-markdown-strong": |
180 | | - case "apply-format-markdown-code": |
181 | | - case "apply-format-markdown-strike": |
182 | | - case "apply-format-markdown-a": |
183 | | - |
184 | | - // // NOTE: Formatting events must always be |
185 | | - // // prevented. |
186 | | - // e.preventDefault() |
187 | | - |
188 | | - if (testForSelection(state)) { |
189 | | - e.preventDefault() |
190 | | - formatType = keyDownType.slice("apply-format-markdown-".length) |
191 | | - const types = {} |
192 | | - if (formatType !== "plaintext") { |
193 | | - // types[formatType] = {} |
194 | | - types[formatType] = formatType !== "a" ? {} : { |
195 | | - href: "TODO", |
196 | | - } |
197 | | - } |
198 | | - dispatch({ |
199 | | - type: "APPLY_TYPES", |
200 | | - types, |
201 | | - }) |
202 | | - } |
203 | | - break |
204 | | - case "insert-text": |
205 | | - if (testForSelection(state)) { |
206 | | - e.preventDefault() |
207 | | - text = e.key |
208 | | - dispatch({ |
209 | | - type: "INSERT_TEXT", |
210 | | - text, |
211 | | - }) |
212 | | - } |
213 | | - break |
214 | | - case "insert-composed-text-unidentified": |
215 | | - case "insert-composed-text-identified": |
216 | | - if (testForSelection(state) && state.range.start.key !== state.range.end.key) { |
217 | | - e.preventDefault() |
218 | | - // COMPAT: e.preventDefault(...) on |
219 | | - // "insert-composed-text-unidentified" breaks |
220 | | - // composition and oncompositionend is never |
221 | | - // emitted. In Chrome 84, use of |
222 | | - // document.activeElement.blur(...) appears to |
223 | | - // emit oncompositionend. |
224 | | - const selection = document.getSelection() |
225 | | - if (selection.rangeCount) { |
226 | | - document.activeElement.blur() |
227 | | - const range = { |
228 | | - ...state.range, |
229 | | - end: state.range.start, |
230 | | - } |
231 | | - setTimeout(() => { |
232 | | - const userRange = convRangeToUserLiteral(range) |
233 | | - selection.addRange(userRange) |
234 | | - }, 0) |
235 | | - } |
236 | | - dispatch({ |
237 | | - type: "INSERT_TEXT", |
238 | | - text: "", |
239 | | - }) |
240 | | - } |
241 | | - break |
242 | | - case "insert-tab": |
243 | | - e.preventDefault() |
244 | | - text = "\t" |
245 | | - dispatch({ |
246 | | - type: "INSERT_TEXT", |
247 | | - text, |
248 | | - }) |
249 | | - break |
250 | | - case "insert-soft-paragraph": |
251 | | - case "insert-hard-paragraph": |
252 | | - case "insert-horizontal-rule": |
253 | | - e.preventDefault() |
254 | | - dispatch({ |
255 | | - type: "INSERT_HARD_PARAGRAPH", |
256 | | - }) |
257 | | - break |
258 | | - case "delete-rtl-rune": |
259 | | - case "delete-rtl-word": |
260 | | - case "delete-rtl-line": |
261 | | - case "delete-ltr-rune": |
262 | | - case "delete-ltr-word": |
263 | | - e.preventDefault() |
264 | | - deleteType = keyDownType.slice("delete-".length) |
265 | | - dispatch({ |
266 | | - type: "DELETE", |
267 | | - deleteType, |
268 | | - }) |
269 | | - break |
270 | | - case "undo": |
271 | | - case "redo": |
272 | | - e.preventDefault() |
273 | | - // TODO |
274 | | - break |
275 | | - default: |
276 | | - // No-op |
277 | | - break |
278 | | - } |
279 | | - }} |
280 | | - |
281 | | - onCompositionEnd={e => { |
282 | | - const range = getCurrentRange(ref.current) |
283 | | - const children = parseRenderedChildren(document.getElementById(range.start.key)) |
284 | | - dispatch({ |
285 | | - type: "UNCONTROLLED_INPUT", |
286 | | - range, |
287 | | - children, |
288 | | - noopRender: false, // onCompositionEnd must rerender |
289 | | - }) |
290 | | - }} |
291 | | - |
292 | | - onInput={e => { |
293 | | - const range = getCurrentRange(ref.current) |
294 | | - const children = parseRenderedChildren(document.getElementById(range.start.key)) |
295 | | - dispatch({ |
296 | | - type: "UNCONTROLLED_INPUT", |
297 | | - range, |
298 | | - children, |
299 | | - noopRender: e.nativeEvent.isComposing, |
300 | | - }) |
301 | | - }} |
302 | | - |
303 | | - onCut={e => { |
304 | | - e.preventDefault() |
305 | | - // TODO |
306 | | - }} |
307 | | - |
308 | | - // onCopy={e => { |
309 | | - // e.preventDefault() |
310 | | - // // TODO |
311 | | - // }} |
312 | | - |
313 | | - onPaste={e => { |
314 | | - e.preventDefault() |
315 | | - // TODO |
316 | | - }} |
317 | | - |
318 | | - onDragStart={e => { |
319 | | - e.preventDefault() |
320 | | - }} |
321 | | - |
322 | | - contentEditable={state.mounted} |
323 | | - suppressContentEditableWarning={state.mounted} |
324 | | - |
325 | | - data-root |
326 | | - /> |
| 10 | + "Hello, world!" |
327 | 11 | ) |
328 | 12 | } |
329 | 13 |
|
330 | | -// {process.env.NODE_ENV !== "production" && ( |
331 | | -// <pre className="mt-6 text-xs whitespace-pre-wrap break-words" style={tabSize(2)}> |
332 | | -// {JSON.stringify(state, null, "\t")} |
333 | | -// </pre> |
334 | | -// )} |
335 | | - |
336 | 14 | export default Editor |
0 commit comments