wangeditor编辑器自定义按钮和节点,上传word转换html,文本替换

vue3+ts

需求:在编辑器插入图片和视频时下方会有一个输入框填写描述,上传word功能

wangeditor文档wangEditor开源 Web 富文本编辑器,开箱即用,配置简单https://www.wangeditor.com/

安装:npm install @wangeditor/editor --save

1、自定义按钮部分 index.ts,参考了文档

TypeScript 复制代码
import type { IButtonMenu, IDomEditor } from "@wangeditor/editor-for-vue";
import { Range } from "slate";
import { DomEditor } from "@wangeditor/editor";

class VideoMenu implements IButtonMenu {
  title: string;
  tag: string;
  iconSvg: string;
  constructor() {
    this.title = "上传视频";
    this.iconSvg =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="black" 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"/></svg>';
    this.tag = "button";
  }
  getValue() {
    return " ";
  }
  isActive() {
    return false;
  }
  isDisabled(editor: IDomEditor): boolean {
    //这部分参考的源码写的
    const { selection } = editor;
    if (selection == null) return true;
    if (!Range.isCollapsed(selection)) return true; // 选区非折叠,禁用
    const selectedElems = DomEditor.getSelectedElems(editor);
    const hasVoidOrPre = selectedElems.some(elem => {
      const type = DomEditor.getNodeType(elem);
      if (type === "pre") return true;
      if (type === "list-item") return true;
      if (editor.isVoid(elem)) return true;
      return false;
    });
    if (hasVoidOrPre) return true; // void 或 pre ,禁用

    return false;
  }
  exec(editor: IDomEditor) {
    if (this.isDisabled(editor)) return;
    //点击打开上传视频的弹框
    editor.emit("uploadvideo");
  }
}
class TextReplace implements IButtonMenu {
  title: string;
  iconSvg: string;
  tag: string;
  constructor() {
    this.title = "文本替换";
    this.iconSvg =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path fill="black" d="M11 6c1.38 0 2.63.56 3.54 1.46L12 10h6V4l-2.05 2.05A6.976 6.976 0 0 0 11 4c-3.53 0-6.43 2.61-6.92 6H6.1A5 5 0 0 1 11 6m5.64 9.14A6.89 6.89 0 0 0 17.92 12H15.9a5 5 0 0 1-4.9 4c-1.38 0-2.63-.56-3.54-1.46L10 12H4v6l2.05-2.05A6.976 6.976 0 0 0 11 18c1.55 0 2.98-.51 4.14-1.36L20 21.49L21.49 20z"/></svg>';
    this.tag = "button";
  }
  getValue() {
    return false;
  }
  isActive() {
    return false;
  }
  isDisabled(editor: IDomEditor): boolean {
    const { selection } = editor;
    if (selection == null) return true;
    return false;
  }
  exec(editor: IDomEditor) {
    if (this.isDisabled(editor)) return;
    editor.emit("toggleModal", "textReplace", true);
  }
}

class sendwordMenu implements IButtonMenu {
  title: string;
  tag: string;
  constructor() {
    this.title = "上传word";
    this.tag = "button";
  }
  getValue() {
    return " ";
  }
  isActive() {
    return false;
  }
  isDisabled(editor: IDomEditor): boolean {
    const { selection } = editor;
    if (selection == null) return true;
    if (!Range.isCollapsed(selection)) return true; // 选区非折叠,禁用
    const selectedElems = DomEditor.getSelectedElems(editor);
    const hasVoidOrPre = selectedElems.some(elem => {
      const type = DomEditor.getNodeType(elem);
      if (type === "pre") return true;
      if (type === "list-item") return true;
      if (editor.isVoid(elem)) return true;
      return false;
    });
    if (hasVoidOrPre) return true; // void 或 pre ,禁用
  }
  exec(editor: IDomEditor) {
    if (this.isDisabled(editor)) return;
    //这里写点击按钮后的操作,我这里是调自定义事件
    editor.emit("uploadword");
  }
}
export const menu1Conf = {
  key: "videomenu", // 定义 menu key :要保证唯一、不重复(重要)
  factory() {
    return new VideoMenu();
  }
};

export const menu2Conf = {
  key: "wordmenu",
  factory() {
    return new sendwordMenu();
  }
};
export const menu3Conf = {
  key: "textReplace",
  factory() {
    return new TextReplace();
  }
};

2、editorComponents.vue代码,在editor组件中引入index.ts和renderviedoEle/index和renderimgEle/index

TypeScript 复制代码
<script setup lang="ts">
import {
  onBeforeUnmount,
  ref,
  reactive,
  shallowRef,
  defineEmits,
  defineProps,
} from "vue";
import "@wangeditor/editor/dist/css/style.css";
import {
  Editor,
  Toolbar,
  IDomEditor,
} from "@wangeditor/editor-for-vue";
import {
  Boot,
  DomEditor,
} from "@wangeditor/editor";
import type { UploadInstance } from "element-plus";
import mammoth from "mammoth";
import customvideo from "@/utils/renderviedoEle/index";
import customimage from "@/utils/renderimgEle/index";
import {
  menu1Conf,
  menu2Conf,
  menu3Conf,
} from "@/utils/menus/index";
defineOptions({
  name: "editUpload"
});
const emit = defineEmits([
  "changevalue",
]);

const mode = "default";
const props = defineProps({
  editvalue: {
    type: String,
    default: ""
  },
});
const localeditvalue = ref(props.editvalue);
const txtplace = reactive({
  findContent: "",
  replaceContent: ""
});
const textReplaceShow = ref(false);

const replaceTextInHTML = function (html, searchText, replaceText) {
  // 定义全局匹配的正则表达式,匹配除了HTML标签之外的所有内容
  const regex = />([^<]*)</g;
  // 使用replace方法替换匹配到的文本内容
  const replacedHtml = html.replace(regex, (match, text) => {
    // 判断文本内容是否包含需要替换的搜索文本
    if (text.includes(searchText)) {
      // 替换文本内容
      const replacedText = text.replace(
        new RegExp(searchText, "g"),
        replaceText
      );
      return `>${replacedText}<`;
    } else {
      // 不需要替换,返回原内容
      return match;
    }
  });

  return replacedHtml;
};
const handleSubmit = () => {//替换文本提交
  const html = editorRef.value.getHtml();
  const newHtml = replaceTextInHTML(
    html,
    txtplace.findContent,
    txtplace.replaceContent
  );
  editorRef.value.setHtml(newHtml);
};

const insertVideo = val => {//插入视频
  editorRef.value.restoreSelection();// 恢复选区
  setTimeout(() => {
    editorRef.value.insertNode({
      type: "customvideo",
      src: val.videoUrl,
      poster: val.coverUrl,
      videoId: val.videoID,
      altDes: "",
      children: [
        {
          text: ""
        }
      ]
    });
  }, 500);
} 
const sendeluploads = ref<UploadInstance>();
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
const toolbarConfig: any = {//这里把不想要的菜单排除掉
  excludeKeys: [
    "insertImage",
    "insertVideo",
    "uploadVideo",
    "editvideomenu",
    "group-video"
  ]
};
const editorConfig = {
  placeholder: "请输入内容...",
  MENU_CONF: {}
};
// 在工具栏插入自定义的按钮
toolbarConfig.insertKeys = {
  index: 19, // 插入的位置,基于当前的 toolbarKeys
  keys: [
    "videomenu",
    "wordmenu",
    "textReplace"
  ]
};

//注意:这个要再外面注入,不然会报错
Boot.registerModule(customvideo); 
Boot.registerModule(customimage);
const handleCreated = (editor: IDomEditor) => {
  editorRef.value = editor;
  // 判断已插入过就不要重复插入按钮
  if (
    !editor
      .getAllMenuKeys()
      ?.includes(
        "videomenu",
        "wordmenu",
        "textReplace"
      )
  ) {
    Boot.registerMenu(menu1Conf);
    Boot.registerMenu(menu2Conf);
    Boot.registerMenu(menu3Conf);
  }
  editor.on("uploadvideo", val => {
      // 处理上传视频的逻辑,上传完直接插入视频 insertVideo()
      // ........
  });
  editor.on("uploadword", () => {
    // 点击上传word按钮模拟上传事件clik
    sendeluploads.value.$.vnode.el.querySelector("input").click();
  });
  editor.on("toggleModal", (modalName, show) => {
    // 显示替换的弹框
    textReplaceShow.value = show;
  });

};
const onChange = editor => {//编辑器的值改变
  emit("changevalue", editor.getHtml());
};
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value;
  if (editor == null) return;
  editor.destroy();
});

// 图片上传阿里云服务器
editorConfig.MENU_CONF["uploadImage"] = {
  // 自定义上传
  async customUpload(file: File, insertFn) {
   aliyunApi(file).then((res: any) => {
      // 上传到服务器后插入自定义图片节点
      editorRef.value.insertNode({
        type: "customimage",
        src: res.url,
        alt: res.name,
        href: res.url,
        children: [
          {
            text: ""
          }
        ]
      });
    });
  }
};

const handleSuccess = val => {};
const beforeUpload = val => {};
const handleUpload = val => {//上传完word文档后的处理,此处用到了mammoth.js,查看地址:https://github.com/mwilliamson/mammoth.js
  // word文档转换插入到富文本
  const file = val.file;
  var reader = new FileReader();
  reader.onload = function (loadEvent) {
    var arrayBuffer = loadEvent.target?.result;
    mammoth
      .convertToHtml(
        { arrayBuffer: arrayBuffer as ArrayBuffer },
        { convertImage: convertImage }//将base64图片转换上传到阿里云服务器
      )
      .then(
        function (result) {
          // 没能修改插入图片的源码,这里自己做了下修改,加了customimage的div,让图片渲染走自己定义的节点
          // 如果没有这一步,会默认插入原先img的那个节点
          const parser = new DOMParser();
          const doc = parser.parseFromString(result.value, "text/html");
          const images = doc.getElementsByTagName("img");
          for (let i = images.length - 1; i >= 0; i--) {
            const img = images[i];
            const div = doc.createElement("div");
            div.setAttribute("data-w-e-type", "customimage");
            div.setAttribute("data-w-e-is-void", "");
            div.setAttribute("data-w-e-is-inline", "");
            if (img.parentNode) {
              img.parentNode.replaceChild(div, img);
            }
            div.appendChild(img);
          }
          const processedHtml = doc.body.innerHTML;
          editorRef.value.dangerouslyInsertHtml(processedHtml);
        },
        function (error) {
          console.error(error);
        }
      );
  };
  reader.readAsArrayBuffer(file);
};

// word图片转换
const convertImage = mammoth.images.imgElement(image => {
  return image.read("base64").then(async imageBuffer => {
    const result = await uploadBase64Image(imageBuffer, image.contentType);
    return { src: result };
  });
});

const uploadBase64Image = async (base64Image, mime) => {
  const _file = base64ToBlob(base64Image, mime);
  let data: any = await aliyunApi(_file);
  return data.url;
};
const base64ToBlob = (base64, mime) => {
  mime = mime || "";
  const sliceSize = 1024;
  const byteChars = window.atob(base64);
  const byteArrays = [];
  for (
    let offset = 0, len = byteChars.length;
    offset < len;
    offset += sliceSize
  ) {
    const slice = byteChars.slice(offset, offset + sliceSize);
    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }
    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }
  return new Blob(byteArrays, { type: mime });
};

</script>

<template>
  <div
    class="wangeditor"
  >
    <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
    <Editor
      id="editor-container"
      v-model="localeditvalue"
      :defaultConfig="editorConfig"
      :mode="mode"
      style="height: 500px; overflow-y: hidden; border: 1px solid #ccc"
      @onCreated="handleCreated"
      @onChange="onChange"
    />
    <el-upload
      v-show="false"
      ref="sendeluploads"
      action="#"
      :show-file-list="false"
      accept=".docx"
      :on-success="handleSuccess"
      :before-upload="beforeUpload"
      :http-request="handleUpload"
    />
    <el-dialog
      v-model="textReplaceShow"
      title="文本替换"
      width="30%"
      class="replacedialog"
    >
      <el-form
        v-model="txtplace"
        label-width="auto"
      >
        <el-form-item label="查找文本">
          <el-input v-model="txtplace.findContent" />
        </el-form-item>
        <el-form-item label="替换文本">
          <el-input v-model="txtplace.replaceContent" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSubmit">替换</el-button>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>
<style scoped lang="scss">
.replacedialog {
  .el-form {
    .el-form-item {
      margin-bottom: 20px;

      label {
        font-weight: bold;
        color: #333;
      }

      .el-input {
        input {
          color: #333;
        }
      }
    }
  }
}
</style>
<style lang="scss">
.w-e-image-container {
  border: 2px solid transparent;
}

.w-e-text-container [data-slate-editor] .w-e-selected-image-container {
  border: 2px solid rgb(180 213 255);
}

.w-e-text-container [data-slate-editor] img {
  display: block !important;
  margin: 0 auto;
}

.w-e-text-container [data-slate-editor] .w-e-image-container {
  display: block;
}

.w-e-text-container [data-slate-editor] .w-e-image-container:hover {
  box-shadow: none;
}

.txt-input {
  .el-textarea__inner {
    height: 300px;
  }
}

.w-e-text-container [data-slate-editor] p {
  margin: 5px 0;
}

.w-e-textarea-video-container video {
  width: 30%;
}

.w-e-textarea-video-container {
  background: none;
}

.w-e-text-container
  [data-slate-editor]
  .w-e-selected-image-container
  .left-top {
  display: none;
}

.w-e-text-container
  [data-slate-editor]
  .w-e-selected-image-container
  .right-top {
  display: none;
}

.w-e-text-container
  [data-slate-editor]
  .w-e-selected-image-container
  .left-bottom {
  display: none;
}

.w-e-text-container
  [data-slate-editor]
  .w-e-selected-image-container
  .right-bottom {
  display: none;
}
</style>

3、在页面中引用editor组件

TypeScript 复制代码
<script setup lang="ts">
import { ref, reactive } from "vue";
import { EdtiorUpload } from "@/components/editor";
const editorcontent = ref("");
const childeditRef = ref(null);
const editorChange = val => {
  // 编辑器值改变了...
};
</script>

<template>
  <div>
    <div style="width: 100%">
      <!-- 这里组件写ref标识 保证每次组件打开都能更新 -->
      <EdtiorUpload
        ref="childeditRef"
        :editvalue="editorcontent"
        @changevalue="editorChange"
      />
    </div>
  </div>
</template>

4.自定义节点的部分renderviedoEle/index,renderimgEle/index 放在了githubhttps://github.com/srttina/wangeditor-customsalte/tree/master

相关推荐
潇-xiao41 分钟前
vim的相关命令 + 三种模式(10)
linux·编辑器·vim
程序猿小D1 小时前
第28节 Node.js 文件系统
服务器·前端·javascript·vscode·node.js·编辑器·vim
kooboo china.4 小时前
什么是JSON ?从核心语法到编辑器
javascript·编辑器·json
waterHBO14 小时前
Cursor 编辑器, 使用技巧,简单记录一下
windows·编辑器
程序猿小D17 小时前
第24节 Node.js 连接 MongoDB
数据库·mongodb·npm·node.js·编辑器·vim·express
FL162386312919 小时前
VScode打开后一直显示正在重新激活终端 问题的解决方法
ide·vscode·编辑器
程序猿小D21 小时前
第26节 Node.js 事件
服务器·前端·javascript·node.js·编辑器·ecmascript·vim
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 编辑区(含工具条、自定义右键快捷菜单、快捷键编辑、拖拽打开文件等)
javascript·electron·编辑器
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)
javascript·electron·编辑器
晨曦backend2 天前
Vim 匹配跳转与搜索命令完整学习笔记
linux·编辑器·vim