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

news2024/11/15 9:31:04

vue3+ts

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

wangeditor文档wangEditor开源 Web 富文本编辑器,开箱即用,配置简单icon-default.png?t=N7T8https://www.wangeditor.com/

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

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

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 

<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组件

<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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2075085.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将标准输入stdin转换成命令行参数——Unix中的xargs指令

xargs是Unix中的复合指令加工机&#xff0c;联合管道符“|”将制造更加强大的“复杂”指令组合。 (笔记模板由python脚本于2024年08月22日 18:13:51创建&#xff0c;本篇笔记适合喜欢Linux的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.…

【数据结构与算法】使用哈夫曼编码压缩文本

哈夫曼编码原理 哈夫曼编码属于一种基于字符出现频率的贪心算法&#xff0c;其通过构建哈夫曼树&#xff0c;为文本中的每一个字符赋予独一无二的二进制编码。频率较高的字符会被分配较短的编码&#xff0c;而频率较低的字符则会被分配较长的编码&#xff0c;以此达成压缩数据…

通过模板级知识蒸馏进行掩模不变人脸识别

Mask-invariant Face Recognition through Template-level Knowledge Distillation 创新点 1.提出了一种掩模不变人脸识别解决方案&#xff08;MaskInv&#xff09;&#xff0c;该解决方案在训练范式中利用模板级知识蒸馏&#xff0c;旨在生成与相同身份的非蒙面人脸相似的蒙面…

上半年大模型遍地开花,大模型发展中有哪些经验和教训?

前言 过去一年里&#xff0c;大模型遍地开花&#xff0c;我自己也在做大模型训练相关的工作&#xff0c;踩过了很多很多坑&#xff0c;这里分享一些教训&#xff1a;用成熟的分布式训练框架&#xff1a; 多用 DeepSpeed&#xff0c;少用 Pytorch 原生的 torchrun。在节点数量较…

Android settings命令讲解和实战

1&#xff0c;简介 在Android系统中&#xff0c;settings命令用于管理设备设置。这些命令可以与Settings提供者&#xff08;Settings provider&#xff09;交互&#xff0c;后者是一个用于存储和检索系统设置的系统服务。Settings provider在Android系统中可以被看作是一个特殊…

垃圾目标检测数据集

垃圾目标检测数据集。数据集已经标注好&#xff0c;txt标注&#xff0c;YOLO格式&#xff0c;适用于训练YOLO系列目标检测模型&#xff08;YOLO5 YOLO8 YOLO9 YOLO10等&#xff09;数据集已经划分好训练集&#xff08;4526张图&#xff09;验证集&#xff08;1046张图片&#x…

Git 分支操作全解析:创建、切换、合并、删除及冲突解决

“ 在现代软件开发中&#xff0c;高效的版本控制是确保项目成功的关键。Git 提供了强大的分支管理功能&#xff0c;使得开发者能够独立地进行功能开发、修复 bug 和进行紧急修补。本文将深入探讨 Git 分支的基本操作&#xff0c;包括创建、切换、合并和删除分支&#xff0c;同时…

使命同心,六西格玛同行,顶尖人才扎堆来!——张驰咨询

在当今竞争激烈的商业环境中&#xff0c;顶尖人才不仅是企业创新与突破的源泉&#xff0c;更是决定企业成败的关键因素。如何有效吸引并长期留住这些宝贵资源&#xff0c;成为企业家们亟待解决的难题。企业文化、领导风格以及先进的管理方法&#xff0c;如六西格玛&#xff0c;…

两个视频如何合成一个视频?推荐8个视频合并方法

两个视频如何合成一个视频&#xff1f;在数字时代&#xff0c;视频内容的创作和编辑变得越来越普及。无论是在个人项目还是专业制作中&#xff0c;将两个视频合并成一个已成为常见需求。这一操作不仅能提高视频内容的连贯性&#xff0c;还能增强观众的观看体验。下面&#xff0…

大模型火了一年半,AI应用如何实现商业变现?

自ChatGPT走红后&#xff0c;国内AI大模型建设潮起&#xff0c;如今经过一年半的快速发展后&#xff0c;大模型商业化显著提速。自生成式AI席卷各行业以来&#xff0c;市场对AI应用发展的预期高涨&#xff0c;但AI应用似乎陷入“增长难题”&#xff0c;进展不及预期&#xff0c…

Java筑基之路:数组的深入了解学习!

&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d; &#x1f947;博主昵称&#xff1a;小菜元 &#x1f35f;博客主页…

科技温柔拥抱梦乡!康姿百德柔压磁性枕舒适与科技的甜蜜邂逅

解锁未来睡眠新姿势&#xff01;康姿百德柔压磁性枕&#xff0c;科技护航每一夜好梦 在现代家居产品的设计中&#xff0c;科技与舒适性的结合越来越受到人们的关注。康姿百德柔压磁性枕正是一款将科技与舒适结合的产品&#xff0c;为现代生活注入了新的活力。 康姿百德柔压磁性…

【发邮件】 在邮件中添加 (mailto:) 链接的返回电子邮件

&#x1f433;打工人给导师发邮件&#xff0c;注意格式中学到的一个东西&#xff0c;记录一下 发送邮件想达到点击这个邮件就能到收件人的位置&#xff0c;不用跳转。 也就是你点击这个邮件 就能直接给你蹦到发送这个人&#xff0c;然后直接发送 只需要在邮件那个位置 加入超…

语言大模型的分布式训练与高效微调指南

最近语言大模型&#xff08;LLM&#xff09;异常火爆&#xff0c;一个非常特别的开源社区正在探索在消费级硬件上微调、提供服务和进行推理的最佳方式。为满足上述需求&#xff0c;出现了许多出色的开源代码库&#xff0c;以HuggingFace生态系统为中心&#xff0c;这些代码库还…

【干货】看看我司消息队列用啥,全网最接地气pulsar教程(含业务解耦demo源码)

前言 &#x1f34a;缘由 消息队列一出手&#xff0c;pulsar就知有没有 &#x1f423;闪亮主角 大家好&#xff0c;我是JavaDog程序狗 今天跟大家分享pulsar&#xff0c;一个分布式的消息发布/订阅传递平台。 本狗以身入局&#xff0c;将pulsar的使用场景&#xff0c;结合实…

【flask框架搭建服务器demo】Python 使用轻量级 Flask 框架搭建 Web 服务器可视化数据库数据demo

本文适合刚入门flask框架用来熟悉项目的开发人员&#xff0c;关于flask框架的组成概念一些用法请参考下面的文章 https://blog.csdn.net/qq_47452807/article/details/122289200 本文主要给出一个可视化sqlite数据库数据的demo&#xff0c;先展示一下效果&#xff1a; 主要的…

前端速通面经八股系列(二)—— HTML篇

HTML高频面经八股目录 1. src和href的区别2. 对HTML语义化的理解3. DOCTYPE(⽂档类型) 的作⽤4. script标签中defer和async的区别5. 常⽤的meta标签有哪些6. HTML5有哪些更新1. 语义化标签2. 媒体标签3. 表单4. 进度条、度量器5.DOM查询操作6. Web存储7. 其他 7. img的srcset属…

Python 图像处理进阶:特征提取与图像分类

特征提取 特征提取是计算机视觉中的一个重要环节&#xff0c;它可以从图像中提取出有助于后续处理的特征&#xff0c;比如用于识别和分类的关键点、纹理等。常见的特征提取方法包括SIFT、SURF和ORB等。 SIFT&#xff08;尺度不变特征变换&#xff09; SIFT是一种用于检测图像…

Web-ssrfme--redis 未授权访问攻击

目录 1、题目源码 2、测试ssrf 3、发现主机 4、发现服务 5、redis 未授权访问攻击 6&#xff0c;拿flag 1、题目源码 <?php highlight_file(__file__); function curl($url){ $ch curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_HEADER…