1、支持包安装
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --saveyarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save
2、使用
import '@wangeditor/editor/dist/css/style.css' // 引入 cssimport { useState, useEffect } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'type InsertImgType = (url: string, alt: string, href: string) => void;
type InsertVideoType = (url: string, poster?: string) => void;const MyEditor: FunctionComponent = () => {// editor 实例const [editor, setEditor] = useState<IDomEditor | null>(null);// 编辑器内容const [html, setHtml] = useState('<p>hello</p>')// 模拟 ajax 请求,异步设置 htmluseEffect(() => {setTimeout(() => {setHtml('<p>hello world</p>')}, 1500)}, [])// 工具栏配置const toolbarConfig: Partial<IToolbarConfig> = {excludeKeys: ['group-video']};// 编辑器配置const editorConfig: Partial<IEditorConfig> = {placeholder: '请输入内容...',readOnly: false,MENU_CONF: {uploadImage: {// 自定义上传 -- 图片customUpload: (file: File, insertFn: InsertImgType) => {if(file.type.startsWith('image/')) {// file 即选中的文件// 自己实现上传,并得到图片 url alt href// 最后插入图片insertFn(url, alt, href)} else {// 错误提示}}},uploadVideo: {// 自定义上传 -- 视频customUpload: (file: File, insertFn: InsertVideoType) => {// file 即选中的文件// 自己实现上传,并得到视频 url poster// 最后插入视频insertFn(url, poster)}}}}useEffect(() => {// 修改弹窗位置为编译器居中editor?.on('modalOrPanelShow', modalOrPanel => {if (modalOrPanel.type !== 'modal') returnconst { $elem } = modalOrPanel; // modal elementconst width = $elem.width();const height = $elem.height();// set modal position z-index$elem.css({left: '50%',top: '50%',bottom: 'auto', // 需要修改底部间距,不然会受组件自身计算影响marginLeft: `-${width / 2}px`,marginTop: `-${height / 2}px`,zIndex: 1000});});// 及时销毁 editor ,重要!return () => {if (editor == null) returneditor.destroy()setEditor(null)}}, [editor])return (<><div style={{ border: '1px solid #ccc', zIndex: 100}}><Toolbareditor={editor}defaultConfig={toolbarConfig}mode="default"style={{ borderBottom: '1px solid #ccc' }}/><EditordefaultConfig={editorConfig}value={html}onCreated={setEditor}onChange={editor => setHtml(editor.getHtml())}mode="default"style={{ height: '500px', overflowY: 'hidden' }}/></div></>)
}export default MyEditor;
3、自定义菜单
1. 添加自定义菜单弹窗
import { DomEditor, IDomEditor, IModalMenu, SlateNode, SlateTransforms, t } from '@wangeditor/editor';
import { DOMElement } from '@wangeditor/editor/dist/editor/src/utils/dom';
import { genModalButtonElems, genModalInputElems } from './utils';class EditImageSize implements IModalMenu {showModal: boolean;modalWidth: number;title: string;iconSvg?: string;hotkey?: string;alwaysEnable?: boolean;tag: string;width?: number;private $content: DOMElement | null = null;private getSelectedImageNode(editor: IDomEditor): SlateNode | null {return DomEditor.getSelectedNodeByType(editor, 'image')}constructor() {this.title = t('videoModule.editSize');// this.iconSvg = '<svg >...</svg>';this.tag = 'button';this.showModal = true;this.modalWidth = 320;}// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 falseisActive(): boolean {// 任何时候,都不用激活 menureturn false}// 获取菜单执行时的 value ,用不到则返回空 字符串或 falsegetValue(): string | boolean {// 插入菜单,不需要 valuereturn ''}// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 falseisDisabled(editor: IDomEditor): boolean {if (editor.selection == null) return trueconst videoNode = this.getSelectedImageNode(editor)if (videoNode == null) {// 选区未处于 image node ,则禁用return true}return false}// 点击菜单时触发的函数exec() {// 点击菜单时,弹出 modal 之前,不需要执行其他代码// 此处空着即可}// 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)getModalPositionNode(editor: IDomEditor): SlateNode | null {return this.getSelectedImageNode(editor);}// 定义 modal 内部的 DOM ElementgetModalContentElem(editor: IDomEditor): DOMElement {const $content = this.$content || document.createElement('div');const [inputWidthContainerElem, inputWidthElem] = genModalInputElems(t('videoModule.width'),`input-width-${Math.random().toString(36).slice(2)}`,'auto');const [inputHeightContainerElem, inputHeightElem] = genModalInputElems(t('videoModule.height'),`input-height-${Math.random().toString(36).slice(2)}`,'auto');const buttonContainerElem = genModalButtonElems(`button-${Math.random().toString(36).slice(2)}`,t('videoModule.ok'));$content.append(inputWidthContainerElem);$content.append(inputHeightContainerElem);$content.append(buttonContainerElem);const imageNode = this.getSelectedImageNode(editor) as unknown as HTMLElement;// 绑定事件(第一次渲染时绑定,不要重复绑定)if (this.$content == null) {buttonContainerElem.onclick = () => {const width = Number(inputWidthElem.value);const height = Number(inputHeightElem.value);console.log(editor, isNaN(width) ? inputWidthElem.value : width ? width +'px' : 'auto', isNaN(height) ? inputHeightElem.value : height ? height +'px' : 'auto')editor.restoreSelection();// 修改尺寸SlateTransforms.setNodes(editor,{style: {width: isNaN(width) ? inputWidthElem.value : width ? width +'px' : 'auto',height: isNaN(height) ? inputHeightElem.value : height ? height +'px' : 'auto',}} as any,{match: n => DomEditor.checkNodeType(n, 'image'),})editor.hidePanelOrModal(); // 隐藏 modal}}if (imageNode == null) return $content;// 初始化 input 值const { width = 'auto', height = 'auto' } = imageNode.style;inputWidthElem.value = width || 'auto';inputHeightElem.value = height || 'auto';setTimeout(() => {inputWidthElem.focus()});return $content // 返回 DOM Element 类型// PS:也可以把 $content 缓存下来,这样不用每次重复创建、重复绑定事件,优化性能}
}export const EditImageSizeConf = {key: 'editImageSize', // 定义 menu key :要保证唯一、不重复(重要)factory() {return new EditImageSize() // 把 `YourMenuClass` 替换为你菜单的 class},
}
公用工具utils
// 生成输入框
export const genModalInputElems = (label: string, id: string, val: string): [HTMLLabelElement, HTMLInputElement] => {const $label = document.createElement('label');$label.className = 'babel-container';const $span = document.createElement('span');$span.textContent = label;const $input = document.createElement('input');$input.type = 'text';$input.id = id;$input.value = val;$label.append($span);$label.append($input);return [$label, $input];
};// 生成按钮
export const genModalButtonElems = (id: string, text: string) => {const $content = document.createElement('div');$content.className = 'button-container';const $button = document.createElement('button');$button.id = id;$button.textContent = text;$content.append($button);return $content;
};
2. 注册自定义菜单
// 注册自定义菜单useEffect(() => {try {Boot.registerMenu(EditImageSizeConf);} catch (e) {}}, [])
3. 挂载到工具栏
// 工具栏配置const toolbarConfig: Partial<IToolbarConfig> = {insertKeys: {index: 5, // 插入的位置,基于当前的 toolbarKeyskeys: ['editImageSize']}}
4. 挂载到组件hover菜单
// 编辑器配置const editorConfig: Partial<IEditorConfig> = {hoverbarKeys: {image: {menuKeys: ['editImageSize'] // 注意:要保留原有的菜单需加上之前的菜单key}}}