React wangEditor5 使用说明

1、支持包安装

yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save

2、使用

import '@wangeditor/editor/dist/css/style.css' // 引入 css

import { 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 请求,异步设置 html
    useEffect(() => {
        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') return
			const { $elem } = modalOrPanel; // modal element
			const 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) return
            editor.destroy()
            setEditor(null)
        }
    }, [editor])

    return (
        <>
            <div style={{ border: '1px solid #ccc', zIndex: 100}}>
                <Toolbar
                    editor={editor}
                    defaultConfig={toolbarConfig}
                    mode="default"
                    style={{ borderBottom: '1px solid #ccc' }}
                />
                <Editor
                    defaultConfig={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;
  }

  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
  isActive(): boolean {
    // 任何时候,都不用激活 menu
    return false
  }

  // 获取菜单执行时的 value ,用不到则返回空 字符串或 false
  getValue(): string | boolean {
    // 插入菜单,不需要 value
    return ''
  }

  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
  isDisabled(editor: IDomEditor): boolean {
    if (editor.selection == null) return true

    const 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 Element
  getModalContentElem(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, // 插入的位置,基于当前的 toolbarKeys
      keys: ['editImageSize']
    }
  }

4. 挂载到组件hover菜单

  // 编辑器配置
  const editorConfig: Partial<IEditorConfig> = {
    hoverbarKeys: {
      image: {
        menuKeys: ['editImageSize']  // 注意:要保留原有的菜单需加上之前的菜单key
      }
    }
  }