Skip to content

Commit fa21df9

Browse files
committed
refactor: deduplicate editor implementations
1 parent d6cba67 commit fa21df9

8 files changed

Lines changed: 424 additions & 1142 deletions

File tree

core/src/Editor.common.tsx

Lines changed: 3 additions & 272 deletions
Original file line numberDiff line numberDiff line change
@@ -1,276 +1,7 @@
1-
import React, { useEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react';
21
import MarkdownPreview from '@uiw/react-markdown-preview/common';
3-
import { ToolbarVisibility } from './components/Toolbar/';
42
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';
94

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';
166

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

Comments
 (0)