React wangEditor5 使用说明

1、支持包安装

powershell 复制代码
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

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

2、使用

typescript 复制代码
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. 添加自定义菜单弹窗

typescript 复制代码
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

typescript 复制代码
// 生成输入框
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. 注册自定义菜单

typescript 复制代码
  // 注册自定义菜单
  useEffect(() => {
    try  {
      Boot.registerMenu(EditImageSizeConf);
    } catch (e) {}
  }, [])

3. 挂载到工具栏

typescript 复制代码
  // 工具栏配置
  const toolbarConfig: Partial<IToolbarConfig> = {
    insertKeys: {
      index: 5, // 插入的位置,基于当前的 toolbarKeys
      keys: ['editImageSize']
    }
  }

4. 挂载到组件hover菜单

typescript 复制代码
  // 编辑器配置
  const editorConfig: Partial<IEditorConfig> = {
    hoverbarKeys: {
      image: {
        menuKeys: ['editImageSize']  // 注意:要保留原有的菜单需加上之前的菜单key
      }
    }
  }
相关推荐
光影少年2 分钟前
react navite高频手写/实操题
前端·javascript·react native·react.js·前端框架
hunterandroid15 分钟前
HarmonyOS 弱网与离线优先架构实战:请求队列、本地缓存与增量同步
前端·前端框架
lhldsg19 分钟前
宠物同城领养平台开发实战:从需求分析到上线部署指南
java·前端·小程序·需求分析·宠物
hunterandroid20 分钟前
Android 内存泄漏排查实战:从 LeakCanary 报警到根因定位
android·前端
leoZ23122 分钟前
AI+前端提效-12 AI辅助前端性能优化与监控:从开发到线上全流程提效
前端·人工智能·神经网络·自然语言处理·性能优化·keras·知识图谱
阿图灵24 分钟前
LangGraph 实战 03:Workflows 与 Agents——六种工作流模式与智能体实战(附 6 个可运行示例)
java·前端·javascript·工作流·ai agent·智能体·langgraph
狂炫冰美式28 分钟前
电脑合盖之后 Cursor 还在偷我电?看看为啥
前端·人工智能·后端
leeyi29 分钟前
微前端:14 个子应用是怎么做到不散架的——DeepFlux 前端工程拆解(第102篇)
前端·aigc·agent
天道kabuto1 小时前
踩坑实录:JS Map迭代器为什么“用完即焚”?兼谈技术知识沉淀
前端
字节跳动开源1 小时前
VisActor全新图可视化开源项目:VGraph
前端·设计模式·开源