import React, { useState, useRef, useImperativeHandle, Fragment, useEffect, useCallback } from 'react'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { languages } from '@codemirror/language-data'; import { EditorView, type ViewUpdate } from '@codemirror/view'; import * as events from '@uiw/codemirror-extensions-events'; import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from '@uiw/react-codemirror'; import MarkdownPreview, { MarkdownPreviewProps } from '@uiw/react-markdown-preview'; import ToolBar, { type Commands } from './components/ToolBar/index'; import { getCommands, getModeCommands } from './commands/index'; import { defaultTheme } from './theme'; import './index.less'; export * from './theme'; export * from './commands/index'; export * from '@uiw/react-markdown-preview'; export const scrollerStyle = EditorView.theme({ '&.cm-editor, & .cm-scroller': { borderBottomRightRadius: '3px', borderBottomLeftRadius: '3px', }, }); export interface IMarkdownEditor extends ReactCodeMirrorProps { className?: string; prefixCls?: string; /** The raw markdown that will be converted to html (**required**) */ value?: string; /** Shows a preview that will be converted to html. */ visible?: boolean; visibleEditor?: boolean; /** Override the default preview component */ renderPreview?: (props: MarkdownPreviewProps, initVisible: boolean) => React.ReactNode; /** Preview expanded width @default `50%` */ previewWidth?: string; /** Whether to enable preview function @default `true` */ enablePreview?: boolean; /** Whether to enable scrolling */ enableScroll?: boolean; /** Tool display settings. */ toolbars?: Commands[]; /** The tool on the right shows the settings. */ toolbarsMode?: Commands[]; /** Tool display filter settings. */ toolbarsFilter?: (tool: Commands, idx: number) => boolean; /** Toolbar on bottom */ toolbarBottom?: boolean; /** Option to hide the tool bar. @deprecated The next major version will be deprecated. Please use `showToolbar`. */ hideToolbar?: boolean; /** Option to hide the tool bar. */ showToolbar?: boolean; /** [@uiw/react-markdown-preview](https://github.com/uiwjs/react-markdown-preview#options-props) options */ previewProps?: MarkdownPreviewProps; /** replace the default `extensions` */ reExtensions?: ReactCodeMirrorProps['extensions']; /** Edit mode and preview mode switching event */ onPreviewMode?: (isHide: boolean) => void; } export interface ToolBarProps { prefixCls?: string; editor: React.RefObject; preview: React.RefObject; container: React.RefObject; containerEditor: React.RefObject; editorProps: IMarkdownEditor; } export interface MarkdownEditorRef { editor: React.RefObject; preview: React.RefObject | null; } const MarkdownEditor: MarkdownEditorComponent = React.forwardRef( MarkdownEditorInternal, ) as unknown as MarkdownEditorComponent; type MarkdownEditorComponent = React.FC> & { Markdown: typeof MarkdownPreview; }; MarkdownEditor.Markdown = MarkdownPreview; export default MarkdownEditor; function MarkdownEditorInternal( props: IMarkdownEditor, ref?: ((instance: MarkdownEditorRef) => void) | React.RefObject | null, ) { const { prefixCls = 'md-editor', className, onChange, toolbars = getCommands(), toolbarsMode = getModeCommands(), toolbarsFilter, visible = true, renderPreview, visibleEditor = true, hideToolbar, showToolbar = true, toolbarBottom = false, enableScroll = true, enablePreview = true, previewProps = {}, extensions = [], previewWidth = '50%', reExtensions, onPreviewMode, ...codemirrorProps } = props; const [value, setValue] = useState(props.value || ''); const codeMirror = useRef(null); const container = useRef(null); const containerEditor = useRef(null); const preview = useRef(null); const active = useRef<'editor' | 'preview'>('editor'); useImperativeHandle( ref, () => ({ editor: codeMirror, preview: preview, }), [codeMirror], ); const toolBarProps: ToolBarProps = { prefixCls, preview: preview, editor: codeMirror, container: container, containerEditor: containerEditor, editorProps: { ...props, previewWidth }, }; const height = typeof codemirrorProps.height === 'number' ? `${codemirrorProps.height}px` : codemirrorProps.height; const preValue = props.value; useEffect(() => setValue(preValue ?? ''), [preValue]); const previewScrollHandle = useCallback( (event: Event) => { if (!enableScroll) return; const target = event.target as HTMLDivElement; const percent = target.scrollTop / target.scrollHeight; if (active.current === 'editor' && preview.current) { const previewHeihgt = preview.current?.scrollHeight || 0; preview.current!.scrollTop = previewHeihgt * percent; } else if (codeMirror.current && codeMirror.current.view) { const editorScrollDom = codeMirror.current.view.scrollDOM; const editorScrollHeihgt = codeMirror.current.view.scrollDOM.scrollHeight || 0; editorScrollDom.scrollTop = editorScrollHeihgt * percent; } }, [enableScroll], ); const mouseoverHandle = () => (active.current = 'preview'); const mouseleaveHandle = () => (active.current = 'editor'); useEffect(() => { const $preview = preview.current; if ($preview && enableScroll) { $preview.addEventListener('mouseover', mouseoverHandle, false); $preview.addEventListener('mouseleave', mouseleaveHandle, false); $preview.addEventListener('scroll', previewScrollHandle, false); } return () => { if ($preview && enableScroll) { $preview.removeEventListener('mouseover', mouseoverHandle); $preview.removeEventListener('mouseleave', mouseoverHandle); $preview.addEventListener('mouseleave', previewScrollHandle, false); } }; }, [preview, enableScroll, previewScrollHandle]); const scrollExtensions = events.scroll({ scroll: previewScrollHandle, }); let extensionsData: IMarkdownEditor['extensions'] = reExtensions ? reExtensions : [markdown({ base: markdownLanguage, codeLanguages: languages }), scrollerStyle, ...extensions]; if (enableScroll) { extensionsData.push(scrollExtensions); } const clsPreview = `${prefixCls}-preview`; const cls = [prefixCls, 'wmde-markdown-var', className].filter(Boolean).join(' '); previewProps['source'] = value; const handleChange = (value: string, viewUpdate: ViewUpdate) => { setValue(value); onChange && onChange(value, viewUpdate); }; const conentView = (
{visibleEditor && ( )}
{enablePreview && (
{renderPreview ? ( renderPreview(previewProps, !!visible) ) : ( )}
)}
); const clsToolbar = [ prefixCls && `${prefixCls}-toolbar-warp`, prefixCls && toolbarBottom && `${prefixCls}-toolbar-bottom`, ] .filter(Boolean) .join(' '); const tools = toolbarsFilter ? toolbars.filter(toolbarsFilter) : toolbars; const toolsMode = toolbarsFilter ? toolbarsMode.filter(toolbarsFilter) : toolbarsMode; const isShowToolbar = hideToolbar ?? showToolbar; const toolbarView = isShowToolbar && (
); const child = toolbarBottom ? ( {conentView} {toolbarView} ) : ( {toolbarView} {conentView} ); return (
{child}
); }