|
1 | | -import React, { useEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react'; |
2 | 1 | import MarkdownPreview from '@uiw/react-markdown-preview/common'; |
3 | | -import { ToolbarVisibility } from './components/Toolbar/'; |
4 | 2 | import TextArea from './components/TextArea/index.common'; |
5 | | -import DragBar from './components/DragBar/'; |
6 | | -import { getCommands, getExtraCommands, type ICommand, type TextState, TextAreaCommandOrchestrator } from './commands/'; |
7 | | -import { reducer, EditorContext, type ContextStore } from './Context'; |
8 | | -import type { MDEditorProps } from './Types'; |
| 3 | +import { createMDEditor } from './Editor.factory'; |
9 | 4 |
|
10 | | -function setGroupPopFalse(data: Record<string, boolean> = {}) { |
11 | | - Object.keys(data).forEach((keyname) => { |
12 | | - data[keyname] = false; |
13 | | - }); |
14 | | - return data; |
15 | | -} |
| 5 | +export type { RefMDEditor } from './Editor.factory'; |
16 | 6 |
|
17 | | -export interface RefMDEditor extends ContextStore {} |
18 | | - |
19 | | -const InternalMDEditor = React.forwardRef<RefMDEditor, MDEditorProps>( |
20 | | - (props: MDEditorProps, ref: React.ForwardedRef<RefMDEditor>) => { |
21 | | - const { |
22 | | - prefixCls = 'w-md-editor', |
23 | | - className, |
24 | | - value: propsValue, |
25 | | - commands = getCommands(), |
26 | | - commandsFilter, |
27 | | - direction, |
28 | | - extraCommands = getExtraCommands(), |
29 | | - height = 200, |
30 | | - enableScroll = true, |
31 | | - visibleDragbar = typeof props.visiableDragbar === 'boolean' ? props.visiableDragbar : true, |
32 | | - highlightEnable = true, |
33 | | - preview: previewType = 'live', |
34 | | - fullscreen = false, |
35 | | - overflow = true, |
36 | | - previewOptions = {}, |
37 | | - textareaProps, |
38 | | - maxHeight = 1200, |
39 | | - minHeight = 100, |
40 | | - autoFocus, |
41 | | - autoFocusEnd = false, |
42 | | - tabSize = 2, |
43 | | - defaultTabEnable = false, |
44 | | - onChange, |
45 | | - onStatistics, |
46 | | - onHeightChange, |
47 | | - hideToolbar, |
48 | | - toolbarBottom = false, |
49 | | - components, |
50 | | - renderTextarea, |
51 | | - ...other |
52 | | - } = props || {}; |
53 | | - const cmds = commands |
54 | | - .map((item) => (commandsFilter ? commandsFilter(item, false) : item)) |
55 | | - .filter(Boolean) as ICommand[]; |
56 | | - const extraCmds = extraCommands |
57 | | - .map((item) => (commandsFilter ? commandsFilter(item, true) : item)) |
58 | | - .filter(Boolean) as ICommand[]; |
59 | | - let [state, dispatch] = useReducer(reducer, { |
60 | | - markdown: propsValue, |
61 | | - preview: previewType, |
62 | | - components, |
63 | | - height, |
64 | | - minHeight, |
65 | | - highlightEnable, |
66 | | - tabSize, |
67 | | - defaultTabEnable, |
68 | | - scrollTop: 0, |
69 | | - scrollTopPreview: 0, |
70 | | - commands: cmds, |
71 | | - extraCommands: extraCmds, |
72 | | - fullscreen, |
73 | | - barPopup: {}, |
74 | | - }); |
75 | | - const container = useRef<HTMLDivElement>(null); |
76 | | - const previewRef = useRef<HTMLDivElement>(null); |
77 | | - const enableScrollRef = useRef(enableScroll); |
78 | | - |
79 | | - useImperativeHandle(ref, () => ({ ...state, container: container.current, dispatch })); |
80 | | - useMemo(() => (enableScrollRef.current = enableScroll), [enableScroll]); |
81 | | - useEffect(() => { |
82 | | - const stateInit: ContextStore = {}; |
83 | | - if (container.current) { |
84 | | - stateInit.container = container.current || undefined; |
85 | | - } |
86 | | - stateInit.markdown = propsValue || ''; |
87 | | - stateInit.barPopup = {}; |
88 | | - if (dispatch) { |
89 | | - dispatch({ ...state, ...stateInit }); |
90 | | - } |
91 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
92 | | - }, []); |
93 | | - |
94 | | - const cls = [ |
95 | | - className, |
96 | | - 'wmde-markdown-var', |
97 | | - direction ? `${prefixCls}-${direction}` : null, |
98 | | - prefixCls, |
99 | | - state.preview ? `${prefixCls}-show-${state.preview}` : null, |
100 | | - state.fullscreen ? `${prefixCls}-fullscreen` : null, |
101 | | - ] |
102 | | - .filter(Boolean) |
103 | | - .join(' ') |
104 | | - .trim(); |
105 | | - |
106 | | - useMemo( |
107 | | - () => propsValue !== state.markdown && dispatch({ markdown: propsValue || '' }), |
108 | | - [propsValue, state.markdown], |
109 | | - ); |
110 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
111 | | - useMemo(() => previewType !== state.preview && dispatch({ preview: previewType }), [previewType]); |
112 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
113 | | - useMemo(() => tabSize !== state.tabSize && dispatch({ tabSize }), [tabSize]); |
114 | | - useMemo( |
115 | | - () => highlightEnable !== state.highlightEnable && dispatch({ highlightEnable }), |
116 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
117 | | - [highlightEnable], |
118 | | - ); |
119 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
120 | | - useMemo(() => autoFocus !== state.autoFocus && dispatch({ autoFocus: autoFocus }), [autoFocus]); |
121 | | - useMemo(() => autoFocusEnd !== state.autoFocusEnd && dispatch({ autoFocusEnd: autoFocusEnd }), [autoFocusEnd]); |
122 | | - useMemo( |
123 | | - () => fullscreen !== state.fullscreen && dispatch({ fullscreen: fullscreen }), |
124 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
125 | | - [fullscreen], |
126 | | - ); |
127 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
128 | | - useMemo(() => height !== state.height && dispatch({ height: height }), [height]); |
129 | | - useMemo( |
130 | | - () => height !== state.height && onHeightChange && onHeightChange(state.height, height, state), |
131 | | - [height, onHeightChange, state], |
132 | | - ); |
133 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
134 | | - useMemo(() => commands !== state.commands && dispatch({ commands: cmds }), [props.commands]); |
135 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
136 | | - useMemo( |
137 | | - () => extraCommands !== state.extraCommands && dispatch({ extraCommands: extraCmds }), |
138 | | - [props.extraCommands], |
139 | | - ); |
140 | | - |
141 | | - const textareaDomRef = useRef<HTMLDivElement>(); |
142 | | - const active = useRef<'text' | 'preview'>('preview'); |
143 | | - const initScroll = useRef(false); |
144 | | - |
145 | | - useMemo(() => { |
146 | | - textareaDomRef.current = state.textareaWarp; |
147 | | - if (state.textareaWarp) { |
148 | | - state.textareaWarp.addEventListener('mouseover', () => { |
149 | | - active.current = 'text'; |
150 | | - }); |
151 | | - state.textareaWarp.addEventListener('mouseleave', () => { |
152 | | - active.current = 'preview'; |
153 | | - }); |
154 | | - } |
155 | | - }, [state.textareaWarp]); |
156 | | - |
157 | | - const handleScroll = (e: React.UIEvent<HTMLDivElement>, type: 'text' | 'preview') => { |
158 | | - if (!enableScrollRef.current) return; |
159 | | - const textareaDom = textareaDomRef.current; |
160 | | - const previewDom = previewRef.current ? previewRef.current : undefined; |
161 | | - if (!initScroll.current) { |
162 | | - active.current = type; |
163 | | - initScroll.current = true; |
164 | | - } |
165 | | - if (textareaDom && previewDom) { |
166 | | - const scale = |
167 | | - (textareaDom.scrollHeight - textareaDom.offsetHeight) / (previewDom.scrollHeight - previewDom.offsetHeight); |
168 | | - if (e.target === textareaDom && active.current === 'text') { |
169 | | - previewDom.scrollTop = textareaDom.scrollTop / scale; |
170 | | - } |
171 | | - if (e.target === previewDom && active.current === 'preview') { |
172 | | - textareaDom.scrollTop = previewDom.scrollTop * scale; |
173 | | - } |
174 | | - let scrollTop = 0; |
175 | | - if (active.current === 'text') { |
176 | | - scrollTop = textareaDom.scrollTop || 0; |
177 | | - } else if (active.current === 'preview') { |
178 | | - scrollTop = previewDom.scrollTop || 0; |
179 | | - } |
180 | | - dispatch({ scrollTop }); |
181 | | - } |
182 | | - }; |
183 | | - |
184 | | - const previewClassName = `${prefixCls}-preview ${previewOptions.className || ''}`; |
185 | | - const handlePreviewScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => handleScroll(e, 'preview'); |
186 | | - let mdPreview = useMemo( |
187 | | - () => ( |
188 | | - <div ref={previewRef} className={previewClassName}> |
189 | | - <MarkdownPreview {...previewOptions} onScroll={handlePreviewScroll} source={state.markdown || ''} /> |
190 | | - </div> |
191 | | - ), |
192 | | - [previewClassName, previewOptions, state.markdown], |
193 | | - ); |
194 | | - const preview = components?.preview && components?.preview(state.markdown || '', state, dispatch); |
195 | | - if (preview && React.isValidElement(preview)) { |
196 | | - mdPreview = ( |
197 | | - <div className={previewClassName} ref={previewRef} onScroll={handlePreviewScroll}> |
198 | | - {preview} |
199 | | - </div> |
200 | | - ); |
201 | | - } |
202 | | - |
203 | | - const containerStyle = { ...other.style, height: state.height || '100%' }; |
204 | | - const containerClick = () => dispatch({ barPopup: { ...setGroupPopFalse(state.barPopup) } }); |
205 | | - const dragBarChange = (newHeight: number) => dispatch({ height: newHeight }); |
206 | | - |
207 | | - const changeHandle = (evn: React.ChangeEvent<HTMLTextAreaElement>) => { |
208 | | - onChange && onChange(evn.target.value, evn, state); |
209 | | - if (textareaProps && textareaProps.onChange) { |
210 | | - textareaProps.onChange(evn); |
211 | | - } |
212 | | - if (state.textarea && state.textarea instanceof HTMLTextAreaElement && onStatistics) { |
213 | | - const obj = new TextAreaCommandOrchestrator(state.textarea!); |
214 | | - const objState = (obj.getState() || {}) as TextState; |
215 | | - onStatistics({ |
216 | | - ...objState, |
217 | | - lineCount: evn.target.value.split('\n').length, |
218 | | - length: evn.target.value.length, |
219 | | - }); |
220 | | - } |
221 | | - }; |
222 | | - return ( |
223 | | - <EditorContext.Provider value={{ ...state, dispatch }}> |
224 | | - <div ref={container} className={cls} {...other} onClick={containerClick} style={containerStyle}> |
225 | | - <ToolbarVisibility |
226 | | - hideToolbar={hideToolbar} |
227 | | - toolbarBottom={toolbarBottom} |
228 | | - prefixCls={prefixCls} |
229 | | - overflow={overflow} |
230 | | - placement="top" |
231 | | - /> |
232 | | - <div className={`${prefixCls}-content`}> |
233 | | - {/(edit|live)/.test(state.preview || '') && ( |
234 | | - <TextArea |
235 | | - className={`${prefixCls}-input`} |
236 | | - prefixCls={prefixCls} |
237 | | - autoFocus={autoFocus} |
238 | | - {...textareaProps} |
239 | | - onChange={changeHandle} |
240 | | - renderTextarea={components?.textarea || renderTextarea} |
241 | | - onScroll={(e) => handleScroll(e, 'text')} |
242 | | - /> |
243 | | - )} |
244 | | - {/(live|preview)/.test(state.preview || '') && mdPreview} |
245 | | - </div> |
246 | | - {visibleDragbar && !state.fullscreen && ( |
247 | | - <DragBar |
248 | | - prefixCls={prefixCls} |
249 | | - height={state.height as number} |
250 | | - maxHeight={maxHeight!} |
251 | | - minHeight={minHeight!} |
252 | | - onChange={dragBarChange} |
253 | | - /> |
254 | | - )} |
255 | | - <ToolbarVisibility |
256 | | - hideToolbar={hideToolbar} |
257 | | - toolbarBottom={toolbarBottom} |
258 | | - prefixCls={prefixCls} |
259 | | - overflow={overflow} |
260 | | - placement="bottom" |
261 | | - /> |
262 | | - </div> |
263 | | - </EditorContext.Provider> |
264 | | - ); |
265 | | - }, |
266 | | -); |
267 | | - |
268 | | -type EditorComponent = typeof InternalMDEditor & { |
269 | | - Markdown: typeof MarkdownPreview; |
270 | | -}; |
271 | | - |
272 | | -const Editor = InternalMDEditor as EditorComponent; |
273 | | -Editor.Markdown = MarkdownPreview; |
274 | | -Editor.displayName = 'MDEditor'; |
275 | | - |
276 | | -export default Editor; |
| 7 | +export default createMDEditor({ MarkdownPreview, TextArea }); |
0 commit comments