2

背景

在网上找了一些wangEditor编辑器实现格式刷的代码,但是都是基于v4版本的,完全不适用于v5版本,所以只能根据文档和源码,自己实现了一个简易版本的

实现

第一步

注册新菜单(https://www.wangeditor.com/v5...
格式刷内部主要实现,点击格式刷时保存选中文字的样式

// FormatterBtn.tsx 
import type { IButtonMenu, IDomEditor } from '@wangeditor/editor';
import { SlateEditor } from '@wangeditor/editor';

class FormatterBtn implements IButtonMenu {
  title = '格式刷';
  tag = 'button'; // JS 语法
  iconSvg = '<svg t="1665739261235" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6325" width="200" height="200"><path d="M1024 1012.48a2119.68 2119.68 0 0 1-45.312-409.6v-228.864a145.92 145.92 0 0 0-146.944-146.176h-128v-128a100.096 100.096 0 0 0-99.84-99.84h-182.016a100.608 100.608 0 0 0-100.352 99.84v128h-128a145.92 145.92 0 0 0-145.664 146.176v228.608a2139.904 2139.904 0 0 1-45.312 409.6L0 1024h1024zM421.888 99.84h182.016v128h-182.272v-128z m274.432 742.4l3.328-146.944a51.2 51.2 0 0 0-25.6-43.52 48.896 48.896 0 0 0-51.2 0 51.2 51.2 0 0 0-25.6 43.52l-2.304 121.344-4.352 69.12L588.8 921.6h-185.856a532.992 532.992 0 0 0 23.296-160.256 51.2 51.2 0 1 0-100.096 0A420.352 420.352 0 0 1 296.448 921.6H120.832l2.304-13.824a2033.92 2033.92 0 0 0 25.6-307.2v-39.168h728.576v67.072a2063.104 2063.104 0 0 0 25.6 281.6l2.304 13.824h-216.064zM877.312 460.8H148.48v-91.392a45.568 45.568 0 0 1 45.312-40.96h642.56a45.312 45.312 0 0 1 40.96 45.312z" p-id="6326"></path></svg>';

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

  // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
  isActive(editor: IDomEditor & { copyStyleObject: any }): boolean {
    return !!editor?.copyStyleObject;
  }

  // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
  isDisabled(editor: IDomEditor): boolean {
    return !editor.getSelectionText();
  }

  // 点击菜单时触发的函数
  exec(editor: IDomEditor & { copyStyleObject: any }) {
    if (this.isDisabled(editor)) {
      return;
    }
    editor.copyStyleObject = SlateEditor.marks(editor);
  }
}

export default FormatterBtn;

第二步

格式刷插入到菜单中,文档中使用insertKeys可以插入对应位置,我在代码中尝试没生效,只能采用笨办法,重写编辑器的菜单,你们可以再尝试下

第三步

监听编辑器内部鼠标按下和抬起来给选中内容赋值

import React, { useCallback, useEffect, useState, useMemo } from 'react';
import type { IButtonMenu, IDomEditor } from '@wangeditor/editor';
import { SlateEditor, Boot, SlateText } from '@wangeditor/editor';
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
import FormatterBtn from './FormatterBtn';

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

const defaultToolbarConfig = {
  // 和编辑器默认的配置一样,只多加了格式刷
  toolbarKeys: [
    'headerSelect',
    'blockquote',
    '|',
    'bold',
    'underline',
    'italic',
    {
      key: 'group-more-style', // 以 group 开头
      title: '更多',
      iconSvg:
        '<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
      menuKeys: ['through', 'code', 'sup', 'sub', 'clearStyle'],
    },
    'color',
    'bgColor',
    '|',
    'fontSize',
    'fontFamily',
    'lineHeight',
    '|',
    'bulletedList',
    'numberedList',
    'todo',
    {
      key: 'group-justify', // 以 group 开头
      title: '两端对齐',
      iconSvg:
        '<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
      menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'],
    },
    {
      key: 'group-indent', // 以 group 开头
      title: '缩进',
      iconSvg:
        '<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z"></path></svg>',
      menuKeys: ['indent', 'delIndent'],
    },
    '|',
    'emotion',
    'insertLink',
    {
      key: 'group-image', // 以 group 开头
      title: '图片',
      iconSvg:
        '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
      menuKeys: ['insertImage', 'uploadImage'],
    },
    {
      key: 'group-video', // 以 group 开头
      title: '视频',
      iconSvg:
        '<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
      menuKeys: ['insertVideo', 'uploadVideo'],
    },
    'insertTable',
    'codeBlock',
    'divider',
    '|',
    'undo',
    'redo',
    'formatBrush',
    '|',
    'fullScreen',
  ],
};

const menu1Conf = {
  key: 'formatBrush', // 定义 menu key :要保证唯一、不重复(重要)
  factory() {
    return new FormatterBtn() as IButtonMenu;
  },
};

// 注册格式刷菜单
Boot.registerMenu(menu1Conf);

let isMouseDown = false;

const MyEditor = () => {
  // editor 实例
  const [editor, setEditor] = useState<(IDomEditor & { copyStyleObject?: any }) | null>(null); // TS 语法

  const changeMouseDown = () => {
    isMouseDown = true;
  };

  const changeMouseup = useCallback(() => {
    const selectText = editor.getSelectionText();
    if (isMouseDown && editor?.copyStyleObject && selectText) {
      // 先清除选中节点的样式
      // 获取所有 text node
      const nodeEntries = SlateEditor.nodes(editor, {
        match: (n) => SlateText.isText(n),
        universal: true,
      });
      for (const nodeEntry of nodeEntries) {
        // 单个 text node
        const n = nodeEntry[0];
        const keys = Object.keys(n as object);
        keys.forEach((key) => {
          if (key === 'text') {
            // 保留 text 属性,text node 必须的
            return;
          }
          // 其他属性,全部清除
          SlateEditor.removeMark(editor, key);
        });
      }

      // 再赋值新样式
      Object.entries(editor.copyStyleObject).forEach(([key, value]) => {
        if (key === 'text') {
          // 保留 text 属性,text node 必须的
          return;
        }
        editor.addMark(key, value);
      });

      editor.copyStyleObject = undefined;
      isMouseDown = false;
    }
  }, [editor]);

// 因为本次需求编辑器在抽屉里 开不同的抽屉会注册多个编辑器,导致编辑器的id不是一个 监听时要监听对应的编辑器,如果只有一个编辑器,这块可以删除,监听直接用w-e-textarea-1
  const domId = useMemo(() => {
    return editor?.id?.split('-')?.[1] ? `w-e-textarea-${editor?.id?.split('-')?.[1]}` : undefined;
  }, [editor]);


  useEffect(() => {
    if (domId) {
      document.getElementById(domId)?.addEventListener('mousedown', changeMouseDown);
      document.getElementById(domId)?.addEventListener('mouseup', changeMouseup);
    }
  }, [domId, changeMouseup]);

  useEffect(() => {
    return () => {
      document.getElementById(domId)?.removeEventListener('mousedown', changeMouseDown);
      document.getElementById(domId)?.removeEventListener('mouseup', changeMouseup);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div style={{ border: '1px solid #ccc', zIndex: 100 }}>
      <Toolbar
        editor={editor}
        defaultConfig={defaultToolbarConfig}
        style={{ borderBottom: '1px solid #ccc' }}
      />
      <Editor onCreated={setEditor} />
    </div>
  );
};

export default MyEditor;

喝冬瓜汤的丁小白
45 声望4 粉丝

« 上一篇
Nextjs