wangEditor5自定义格式刷功能实现

前言

​ 最近呢,公司需要对正在使用的wangeditor编辑器增加一个格式刷功能,个人觉得wangEditor是一款体验不错的富文本编辑器,还有详细的中文文档,使用起来也十分的便捷。

实现效果:

在这里插入图片描述

实现过程

1. 首先通过官方文档写一个类

https://www.wangeditor.com/v5/development.html#droppanelmenu 这个我就不细说比较简单,根据文档直接复制粘贴就行,区分下ts和js写法就行。

2. 注册菜单
import { Boot } from '@wangeditor/editor'									

const menu1Conf = {
  key: 'menu1', // 定义 menu key :要保证唯一、不重复(重要)
  factory() {
    return new YourMenuClass() // 把 `YourMenuClass` 替换为你菜单的 class
  },
}
Boot.registerMenu(menu1Conf);
3. 注入到工具栏
toolbarConfig.insertKeys = {
    index: 5, // 插入的位置,基于当前的 toolbarKeys
    keys: ['menu1']//这里注册名跟你注册时定义的key要一致
}
4. 具体实现

​ 我们运用 SlateEditor.nodes 定位所选内容,并使用 SlateEditor.marks 获取文本样式。通过 SlateEditor.removeMark,我们取消了文本的样式标记,然后用 SlateEditor.addMark 为文本添加了新样式标记。这确保了我们在修改文本样式的同时,保持了整体一致性。

在这个过程中,我们使用了 getSelectionParentEle 函数,它使用 SlateEditor.nodes API 来查找所选内容的父级节点,并通过 this.editor.toDOMNode 将其转换为 DOM 元素之后拿到这个DOM的style对象,保存起来,最后把它加到需要格式刷的节点的父级节点上。

接下来我们看一下具体细节:

//这里是js的实现方式
import { SlateEditor, SlateText,SlateElement,SlateTransforms,Boot } from '@wangeditor/editor'
export default class MyPainter {
  constructor() {
    this.title = '格式刷' // 自定义菜单标题
    // 这里是设置格式刷的样式图片跟svg都可以,但是注意要图片大小要小一点,因为要应用到鼠标手势上
    this.iconSvg = ``
    this.tag = 'button';//注入的菜单类型
    this.savedMarks = null;//保存的样式
    this.domId = null;//这个可要可不要
    this.editor = null;//编辑器示例
    this.parentStyle = null;//储存父节点样式
  }
  //添加或者移除鼠标事件
  addorRemove = (type) => {
    const dom = document.body;
    if (type === 'add') {
      dom.addEventListener('mousedown', this.changeMouseDown);
      dom.addEventListener('mouseup', this.changeMouseup);
    } else {
      //赋值完需要做的清理工作
      this.savedMarks = undefined;
      dom.removeEventListener('mousedown', this.changeMouseDown);
      dom.removeEventListener('mouseup', this.changeMouseup);
      document.querySelector('#w-e-textarea-1').style.cursor = "auto"
    }
  }

    //处理重复键名值不同的情况
  handlerRepeatandNotStyle=(styles) => {
    const addStyles=styles[0];
    const notVal=[];
    for (const style of styles) {
      for (const key in style) {
        const value=style[key];
        if(!addStyles.hasOwnProperty(key)){
            addStyles[key]=value;
        }else{
          if(addStyles[key]!==value){
            notVal.push(key);
          }
        }
      }
    }
    for (const key of notVal) {
      delete addStyles[key];
    }
    return addStyles;
  }

   // 获取当前选中范围的父级节点
  getSelectionParentEle = (type,func) => {
      if(this.editor){
        const parentEntry = SlateEditor.nodes(this.editor, {
          match: node => SlateElement.isElement(node),
        });
       let styles=[];
        for (const [node] of parentEntry) {
         styles.push(this.editor.toDOMNode(node).style);//将node对应的DOM对应的style对象加入到数组
        }
        styles = styles.map((item) => {//处理不为空的style
          const newItem = {};
          for (const key in item) {
            const val = item[key];
             if(val!==''){
               newItem[key] = val;
             }
          }
          return newItem;
        });
        type==='get'?func(type,this.handlerRepeatandNotStyle(styles)):func(type);
      }
  }

  //获取或者设置父级样式
  getorSetparentStyle = (type,style) => {
    if(type === 'get'){
      this.parentStyle=style;//这里是个样式对象 例如{textAlign:'center'}
    }else{
      SlateTransforms.setNodes(this.editor, {...this.parentStyle}, {
        mode: 'highest' // 针对最高层级的节点
      })
    }
  }

  //这里是将svg转换为Base64格式
  addmouseStyle = () => {
    const icon = ``// 这里是给鼠标手势添加图标
    // 将字符串编码为Base64格式
    const base64String = btoa(icon);
    // 生成数据URI
    const dataUri = `data:image/svg+xml;base64,${base64String}`;
    // 将数据URI应用于鼠标图标
    document.querySelector('#w-e-textarea-1').style.cursor = `url('${dataUri}'), auto`;
  }
  changeMouseDown = () => { }//鼠标落下

  changeMouseup = () => {//鼠标抬起
    if (this.editor) {
      const editor = this.editor;
      const selectTxt = editor.getSelectionText();//获取文本是否为null
      if (this.savedMarks && selectTxt) {
        //先改变父节点样式
        this.getSelectionParentEle('set', this.getorSetparentStyle);
        // 获取所有 text node
        const nodeEntries = SlateEditor.nodes(editor, {//nodeEntries返回的是一个迭代器对象
          match: (n) => SlateText.isText(n),//这里是筛选一个节点是否是 text
          universal: true,//当universal为 true 时,Editor.nodes会遍历整个文档,包括根节点和所有子节点,以匹配满足条件的节点。当universal为 false 时,Editor.nodes只会在当前节点的直接子节点中进行匹配。
        });
        // 先清除选中节点的样式
        for (const node of nodeEntries) {
          const n = node[0];//{text:xxxx}
          const keys = Object.keys(n);
          keys.forEach((key) => {
            if (key === 'text') {// 保留 text 属性
              return;
            }
            // 其他属性,全部清除
            SlateEditor.removeMark(editor, key);
          });
        }
        // 再赋值新样式
        for (const key in this.savedMarks) {
          if (Object.hasOwnProperty.call(this.savedMarks, key)) {
            const value = this.savedMarks[key];
            editor.addMark(key, value);
          }
        }
        this.addorRemove('remove');
      }
    }

  }

  getValue(editor) {
    return 'MyPainter'; // 标识格式刷菜单
  }

  isActive(editor) {
    return false;
  }

  isDisabled(editor) {//是否禁用
    return false;
  }
  exec(editor) {//当菜单点击后触发
   this.editor = editor;
   this.domId = editor.id.split('-')[1] ? `w-e-textarea-${editor.id.split('-')[1]}` : undefined;
    if (this.isDisabled(editor)) return;
    if (editor.isEmpty() || editor.getHtml() == '<p><br></p>' || editor.getSelectionText() == '') return;//这里是对没有选中或者没内容做的处理
    this.savedMarks = SlateEditor.marks(editor);// 获取当前选中文本的样式
    this.getSelectionParentEle('get', this.getorSetparentStyle);//获取父节点样式并赋值
    this.addmouseStyle();//点击之后给鼠标添加样式
    this.addorRemove('add');//处理添加和移除事件函数
  }
}

ts实现也是大差不差:

import type { IButtonMenu, IDomEditor } from '@wangeditor/editor'
import { SlateEditor, SlateText, SlateTransforms, SlateElement } from '@wangeditor/editor'

export default class MyPainter implements IButtonMenu {
  title: string;
  tag: string;
  editor: null | IDomEditor;
  domId: string | undefined;
  savedMarks: Record<string, any> | undefined;
  iconSvg: string;
  parentStyle: Record<string, any> | null;

  constructor() {
    this.editor = null;
    this.title = '格式刷'; // 自定义菜单标题
    this.iconSvg = '<svg> ....</svg>'; // 可选 格式刷图标
    this.tag = 'button';
    this.parentStyle = null; // 储存父节点样式
  }

  addmouseStyle = () => {
    const icon = '<svg> ....</svg>';
    const base64String = btoa(icon);
    // 生成数据URI
    const dataUri = `data:image/svg+xml;base64,${base64String}`;
    // 将数据URI应用于鼠标图标
    document.body.style.cursor = `url('${dataUri}'), auto`;
  };

  addorRemove = (type: string) => {
    const dom: HTMLElement | undefined = document.getElementById('w-e-textarea-1') || undefined;
    if (type === 'add') {
      dom?.addEventListener('mousedown', this.changeMouseDown);
      dom?.addEventListener('mouseup', this.changeMouseup);
    } else {
      // 赋值完需要做的清理工作
      this.savedMarks = undefined;
      dom?.removeEventListener('mousedown', this.changeMouseDown);
      dom?.removeEventListener('mouseup', this.changeMouseup);
      document.body.style.cursor = 'auto';
    }
  };

  handlerRepeatandNotStyle = (styles: Record<string, any>[]) => {
    const addStyles = styles[0];
    const notVal: string[] = [];
    for (const style of styles) {
      for (const key in style) {
        const value = style[key];
        if (!addStyles.hasOwnProperty(key)) {
          addStyles[key] = value;
        } else {
          if (addStyles[key] !== value) {
            notVal.push(key);
          }
        }
      }
    }
    for (const key of notVal) {
      delete addStyles[key];
    }
    return addStyles;
  };

  getSelectionParentEle = (type: string, func: (type: string, style: Record<string, any>) => void) => {
    if (this.editor) {
      const parentEntry = SlateEditor.nodes(this.editor, {
        match: (node) => SlateElement.isElement(node),
      });
      let styles: Record<string, any>[] = [];
      for (const [node] of parentEntry) {
        styles.push(this.editor.toDOMNode(node).style); // 将node对应的DOM对应的style对象加入到数组
      }
      styles = styles.map((item) => {
        const newItem: Record<string, any> = {};
        for (const key in item) {
          const val = item[key];
          if (val !== '') {
            newItem[key] = val;
          }
        }
        return newItem;
      });
     func(type, this.handlerRepeatandNotStyle(styles));
    }
  };

  getorSetparentStyle = (type: string, style: Record<string, any>) => {
    if (this.editor) {
      if (type === 'get') {
        this.parentStyle = style; // 这里是个样式对象 例如{textAlign:'center'}
      } else {
        SlateTransforms.setNodes(this.editor, { ...this.parentStyle } as Record<string, any>, {
          mode: 'highest', // 针对最高层级的节点
        });
      }
    }
  };

  changeMouseDown = () => {};

  changeMouseup = () => {
    if (this.editor) {
      const editor = this.editor;
      const selectTxt = editor.getSelectionText(); // 获取文本是否为null
      if (this.savedMarks && selectTxt) {
        // 先改变父节点样式
        this.getSelectionParentEle('set', this.getorSetparentStyle);
        // 获取所有 text node
        const nodeEntries = SlateEditor.nodes(editor, {
          match: (n) => SlateText.isText(n),
          universal: true,
        });
        // 先清除选中节点的样式
        for (const node of nodeEntries) {
          const n = node[0]; // {text:xxxx}
          const keys = Object.keys(n);
          keys.forEach((key) => {
            if (key === 'text') {
              return;
            }
            SlateEditor.removeMark(editor, key);
          });
        }
        // 再赋值新样式
        Object.entries(this.savedMarks).forEach(([key, value]) => {
          if (key === 'text') {
            return;
          }
          editor.addMark(key, value);
        });
        this.addorRemove('remove');
      }
    }
  };

  getValue(editor: IDomEditor): string | boolean {
    return 'MyPainter';
  }

  isActive(editor: IDomEditor): boolean {
    return false;
  }

  isDisabled(editor: IDomEditor): boolean {
    return false;
  }

  exec(editor: IDomEditor, value: string | boolean) {
    if (this.isDisabled(editor)) return;
    this.domId = editor.id.split('-')[1] ? `w-e-textarea-${editor.id.split('-')[1]}` : undefined;
    if (editor.isEmpty() || editor.getHtml() === '<p><br></p>' || editor.getSelectionText() === '') return;
    this.editor = editor;
    this.savedMarks = SlateEditor.marks(editor) || undefined;
    this.getSelectionParentEle('get', this.getorSetparentStyle);
    this.addmouseStyle();
    this.addorRemove('add');
  }
}





总结:

​ 虽然这是我第一次撰写文章,但我深知文笔有待提升。我将不断努力学习,以便更好地传达技术内容和经验分享。在这个过程中,如果各位读者有任何反馈、建议或疑问,我将非常欢迎。你的意见对于我来说是极其宝贵的,将帮助我不断提高,为读者提供更有价值的内容。

如果你有兴趣了解更多关于这个自定义功能的内容,或者你也在开发类似的功能,我愿意随时交流和分享。我们可以在技术社区中共同成长,相互学习,不断推动技术进步。

最后,感谢你的阅读和支持。让我们共同在编程的道路上越走越远,迎接更多的挑战和机遇。

源码地址

如果有兴趣的的话可参考下wangeditor5和slatejs源码

wangeditor5

slateJS

参考文档

wangeditor文档

slateJS文档

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
### 回答1: Tinymce 是一款非常流行的富文本编辑器,它提供了许多丰富的功能,包括格式功能。 使用 Tinymce 实现格式功能的方法如下: 第一步,首先需要配置 Tinymce 编辑器。具体方法是,需要在 Tinymce 的初始化函数中设置一个 toolbar 的数组,用来定义工具栏中显示的按钮。在这个数组中,需要添加一个 button,用来表示格式功能。 第二步,添加格式功能的代码。这一步可以使用 Tinymce 提供的 API 来实现格式功能。具体方法是,使用 getFormat 方法获取选中文本的格式,然后将这个格式应用到其它文本中。在应用格式的过程中,还可以使用 Tinymce 提供的 filter 方法来过滤一些非法的标签和属性。 第三步,添加一个按钮绑定格式功能。这个按钮可以添加到工具栏上,或者在页面上以其它形式显示。当用户点击这个按钮时,就会触发格式功能。 总之,Tinymce 可以通过一些简单的配置和代码实现格式功能。这个功能可以大大提高用户的编辑效率,使得文本编辑变得更加方便和易用。 ### 回答2: tinymce是一种基于JavaScript的富文本编辑器,可以在网页上直接编辑网页中的文字内容,实现所见即所得。在tinymce中,有许多实用的功能,其中包括格式功能格式功能是一种非常实用的文字排版工具,可以将已设定好格式的文字样式应用到其他文本上,从而快速实现一致的排版效果。在tinymce中,实现格式功能也很简单,具体步骤如下: 1. 打开tinymce编辑器,选择需要应用格式的文字。 2. 点击“格式”按钮,这个按钮通常是一个子的图标,可以在tinymce的工具栏中找到。 3. 然后,将鼠标的光标移到需要应用格式的文本上,进行单击。 4. 这时,文字的格式就会被应用到选中的文本中。 除了以上这种常规的格式功能,还可以在tinymce中自定义格式,具体步骤如下: 1. 打开tinymce编辑器,选择需要自定义的文本。 2. 点击“格式”按钮,在下拉菜单中选择“自定义格式”。 3. 然后,在弹出的对话框中,设置需要自定义的文字格式,比如字体、大小、颜色等。 4. 最后,点击“确认”按钮,将自定义格式保存下来。 这样,在编辑文字时,就可以直接通过选择自定义格式,快速应用之前设置好的文字格式,从而提高工作效率。总之,tinymce编辑器提供的格式功能自定义格式功能,对于web前端工程师来说非常实用,帮助他们更加高效地进行网页排版设计,实现所见即所得的效果。 ### 回答3: tinymce是一个非常常用的富文本编辑器,它有着丰富的功能以及插件,其中也包括格式功能格式功能可以使用户在编辑文本时更快速、更方便地将文本的格式进行统一。下面就是如何在tinymce中实现格式功能的方法。 首先,需要在tinymce的配置项中添加一个按钮,该按钮即为格式按钮,用户点击该按钮可以启动格式功能。为此,可以在tinymce配置项的toolbar选项中添加“formatselect”属性,用以显示格式按钮。例如: tinymce.init({ selector: 'textarea', toolbar: 'formatselect' }); 在添加了格式按钮之后,需要自定义格式的样式,以满足用户的需求。可以在tinymce的配置项中的formats选项中定义格式样式,例如: tinymce.init({ selector: 'textarea', toolbar: 'formatselect', formats: { custom: { block: 'p', attributes: { class: 'custom-style' } } } }); 上述代码中,我们定义了一个名为custom的自定义样式,该样式的标签是<p>,并且有一个class属性为custom-style。 最后,需要在点击格式按钮的时候,将选中的文本应用该格式样式。为此,可以在tinymce的配置项中的setup选项中添加一个函数来实现功能。例如: tinymce.init({ selector: 'textarea', toolbar: 'formatselect', formats: { custom: { block: 'p', attributes: { class: 'custom-style' } } }, setup: function (editor) { editor.addButton('formatbrush', { icon: 'formatbrush', tooltip: "Format Brush", onclick: function () { editor.formatter.apply('custom'); } }); } }); 上述代码中,我们添加了一个名为formatbrush的按钮,可以在tinymce中看到其图标为一个子。在点击该按钮的时候,调用editor.formatter.apply('custom')方法,即可应用我们定义的custom样式。 总之,通过以上步骤,便可在tinymce中实现格式功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值