diff --git a/core/README.md b/core/README.md index 54d628aa6..95f3ef3ad 100644 --- a/core/README.md +++ b/core/README.md @@ -137,6 +137,34 @@ export default function App() { } ``` +### Remove Code Highlight + +The following example can help you _exclude code highlighting code_ from being included in the bundle. `@uiw/react-md-editor/nohighlight` component does not contain the ~~`rehype-prism-plus`~~ code highlighting package, ~~`highlightEnable`~~, ~~`showLineNumbers`~~ and ~~`highlight line`~~ functions will no longer work. ([#586](https://github.com/uiwjs/react-md-editor/issues/586)) + +```jsx mdx:preview +import React from "react"; +import MDEditor from '@uiw/react-md-editor/nohighlight'; + +const code = `**Hello world!!!** +\`\`\`js +function demo() {} +\`\`\` +` + +export default function App() { + const [value, setValue] = React.useState(code); + return ( +
+ + +
+ ); +} +``` + ### Custom Toolbars [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?logo=codesandbox)](https://codesandbox.io/embed/react-md-editor-custom-toolbars-m2n10?fontsize=14&hidenavigation=1&theme=dark) diff --git a/core/nohighlight.d.ts b/core/nohighlight.d.ts new file mode 100644 index 000000000..f619286c2 --- /dev/null +++ b/core/nohighlight.d.ts @@ -0,0 +1,13 @@ +declare module '@uiw/react-md-editor/nohighlight' { + import MDEditor from '@uiw/react-md-editor/esm/Editor.nohighlight'; + import * as commands from '@uiw/react-md-editor/esm/commands'; + import * as MarkdownUtil from '@uiw/react-md-editor/esm/utils/markdownUtils'; + export * from '@uiw/react-md-editor/esm/commands'; + export * from '@uiw/react-md-editor/esm/commands/group'; + export * from '@uiw/react-md-editor/esm/utils/markdownUtils'; + export * from '@uiw/react-md-editor/esm/utils/InsertTextAtPosition'; + export * from '@uiw/react-md-editor/esm/Editor.nohighlight'; + export * from '@uiw/react-md-editor/esm/Context'; + export { MarkdownUtil, commands }; + export default MDEditor; +} diff --git a/core/package.json b/core/package.json index 2863d4e61..542feaa3b 100644 --- a/core/package.json +++ b/core/package.json @@ -6,6 +6,20 @@ "author": "kenny wang ", "main": "lib/index.js", "module": "esm/index.js", + "exports": { + "./README.md": "./README.md", + "./package.json": "./package.json", + ".": { + "import": "./esm/index.js", + "types": "./lib/index.d.ts", + "require": "./lib/index.js" + }, + "./nohighlight": { + "import": "./esm/index.nohighlight.js", + "types": "./lib/index.nohighlight.d.ts", + "require": "./lib/index.nohighlight.js" + } + }, "scripts": { "css:build": "compile-less -d src -o esm", "css:watch": "compile-less -d src -o esm --watch", diff --git a/core/src/Context.tsx b/core/src/Context.tsx index 8711d531f..b5979b849 100644 --- a/core/src/Context.tsx +++ b/core/src/Context.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ICommand, TextAreaCommandOrchestrator } from './commands'; -import { MDEditorProps } from './Editor'; +import { MDEditorProps } from './Types'; export type PreviewType = 'live' | 'edit' | 'preview'; diff --git a/core/src/Editor.nohighlight.tsx b/core/src/Editor.nohighlight.tsx new file mode 100644 index 000000000..3b63f81cc --- /dev/null +++ b/core/src/Editor.nohighlight.tsx @@ -0,0 +1,264 @@ +import React, { useEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react'; +import MarkdownPreview from '@uiw/react-markdown-preview/nohighlight'; +import TextArea from './components/TextArea/index.nohighlight'; +import Toolbar from './components/Toolbar'; +import DragBar from './components/DragBar'; +import { getCommands, getExtraCommands, ICommand, TextState, TextAreaCommandOrchestrator } from './commands'; +import { reducer, EditorContext, ContextStore } from './Context'; +import type { MDEditorProps } from './Types'; + +function setGroupPopFalse(data: Record = {}) { + Object.keys(data).forEach((keyname) => { + data[keyname] = false; + }); + return data; +} + +export interface RefMDEditor extends ContextStore {} + +const InternalMDEditor = React.forwardRef( + (props: MDEditorProps, ref: React.ForwardedRef) => { + const { + prefixCls = 'w-md-editor', + className, + value: propsValue, + commands = getCommands(), + commandsFilter, + direction, + extraCommands = getExtraCommands(), + height = 200, + enableScroll = true, + visibleDragbar = typeof props.visiableDragbar === 'boolean' ? props.visiableDragbar : true, + highlightEnable = true, + preview: previewType = 'live', + fullscreen = false, + overflow = true, + previewOptions = {}, + textareaProps, + maxHeight = 1200, + minHeight = 100, + autoFocus, + tabSize = 2, + defaultTabEnable = false, + onChange, + onStatistics, + onHeightChange, + hideToolbar, + toolbarBottom = false, + components, + renderTextarea, + ...other + } = props || {}; + const cmds = commands + .map((item) => (commandsFilter ? commandsFilter(item, false) : item)) + .filter(Boolean) as ICommand[]; + const extraCmds = extraCommands + .map((item) => (commandsFilter ? commandsFilter(item, true) : item)) + .filter(Boolean) as ICommand[]; + let [state, dispatch] = useReducer(reducer, { + markdown: propsValue, + preview: previewType, + components, + height, + highlightEnable, + tabSize, + defaultTabEnable, + scrollTop: 0, + scrollTopPreview: 0, + commands: cmds, + extraCommands: extraCmds, + fullscreen, + barPopup: {}, + }); + const container = useRef(null); + const previewRef = useRef(null); + const enableScrollRef = useRef(enableScroll); + + useImperativeHandle(ref, () => ({ ...state, container: container.current, dispatch })); + useMemo(() => (enableScrollRef.current = enableScroll), [enableScroll]); + useEffect(() => { + const stateInit: ContextStore = {}; + if (container.current) { + stateInit.container = container.current || undefined; + } + stateInit.markdown = propsValue || ''; + stateInit.barPopup = {}; + if (dispatch) { + dispatch({ ...state, ...stateInit }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const cls = [ + className, + 'wmde-markdown-var', + direction ? `${prefixCls}-${direction}` : null, + prefixCls, + state.preview ? `${prefixCls}-show-${state.preview}` : null, + state.fullscreen ? `${prefixCls}-fullscreen` : null, + ] + .filter(Boolean) + .join(' ') + .trim(); + + useMemo( + () => propsValue !== state.markdown && dispatch({ markdown: propsValue || '' }), + [propsValue, state.markdown], + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => previewType !== state.preview && dispatch({ preview: previewType }), [previewType]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => tabSize !== state.tabSize && dispatch({ tabSize }), [tabSize]); + useMemo( + () => highlightEnable !== state.highlightEnable && dispatch({ highlightEnable }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [highlightEnable], + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => autoFocus !== state.autoFocus && dispatch({ autoFocus: autoFocus }), [autoFocus]); + useMemo( + () => fullscreen !== state.fullscreen && dispatch({ fullscreen: fullscreen }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [fullscreen], + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => height !== state.height && dispatch({ height: height }), [height]); + useMemo( + () => height !== state.height && onHeightChange && onHeightChange(state.height, height, state), + [height, onHeightChange, state], + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => commands !== state.commands && dispatch({ commands: cmds }), [props.commands]); + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo( + () => extraCommands !== state.extraCommands && dispatch({ extraCommands: extraCmds }), + [props.extraCommands], + ); + + const textareaDomRef = useRef(); + const active = useRef<'text' | 'preview'>('preview'); + const initScroll = useRef(false); + + useMemo(() => { + textareaDomRef.current = state.textareaWarp; + if (state.textareaWarp) { + state.textareaWarp.addEventListener('mouseover', () => { + active.current = 'text'; + }); + state.textareaWarp.addEventListener('mouseleave', () => { + active.current = 'preview'; + }); + } + }, [state.textareaWarp]); + + const handleScroll = (e: React.UIEvent, type: 'text' | 'preview') => { + if (!enableScrollRef.current) return; + const textareaDom = textareaDomRef.current; + const previewDom = previewRef.current ? previewRef.current : undefined; + if (!initScroll.current) { + active.current = type; + initScroll.current = true; + } + if (textareaDom && previewDom) { + const scale = + (textareaDom.scrollHeight - textareaDom.offsetHeight) / (previewDom.scrollHeight - previewDom.offsetHeight); + if (e.target === textareaDom && active.current === 'text') { + previewDom.scrollTop = textareaDom.scrollTop / scale; + } + if (e.target === previewDom && active.current === 'preview') { + textareaDom.scrollTop = previewDom.scrollTop * scale; + } + let scrollTop = 0; + if (active.current === 'text') { + scrollTop = textareaDom.scrollTop || 0; + } else if (active.current === 'preview') { + scrollTop = previewDom.scrollTop || 0; + } + dispatch({ scrollTop }); + } + }; + + const previewClassName = `${prefixCls}-preview ${previewOptions.className || ''}`; + const handlePreviewScroll = (e: React.UIEvent) => handleScroll(e, 'preview'); + let mdPreview = useMemo( + () => ( +
+ +
+ ), + [previewClassName, previewOptions, state.markdown], + ); + const preview = components?.preview && components?.preview(state.markdown || '', state, dispatch); + if (preview && React.isValidElement(preview)) { + mdPreview = ( +
+ {preview} +
+ ); + } + + const containerStyle = { ...other.style, height: state.height || '100%' }; + const containerClick = () => dispatch({ barPopup: { ...setGroupPopFalse(state.barPopup) } }); + const dragBarChange = (newHeight: number) => dispatch({ height: newHeight }); + + const changeHandle = (evn: React.ChangeEvent) => { + onChange && onChange(evn.target.value, evn, state); + if (textareaProps && textareaProps.onChange) { + textareaProps.onChange(evn); + } + if (state.textarea && state.textarea instanceof HTMLTextAreaElement && onStatistics) { + const obj = new TextAreaCommandOrchestrator(state.textarea!); + const objState = (obj.getState() || {}) as TextState; + onStatistics({ + ...objState, + lineCount: evn.target.value.split('\n').length, + length: evn.target.value.length, + }); + } + }; + return ( + +
+ {!hideToolbar && !toolbarBottom && ( + + )} +
+ {/(edit|live)/.test(state.preview || '') && ( +