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

TinaAsura 2024-08-23 12:07:02 阅读 79

vue3+ts

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

wangeditor文档wangEditor开源 Web 富文本编辑器,开箱即用,配置简单

icon-default.png?t=N7T8

https://www.wangeditor.com/

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

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

<code>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>';code>

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>';code>

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">code>

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"code>

>

<Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />code>

<Editor

id="editor-container"code>

v-model="localeditvalue"code>

:defaultConfig="editorConfig"code>

:mode="mode"code>

style="height: 500px; overflow-y: hidden; border: 1px solid #ccc"code>

@onCreated="handleCreated"code>

@onChange="onChange"code>

/>

<el-upload

v-show="false"code>

ref="sendeluploads"code>

action="#"code>

:show-file-list="false"code>

accept=".docx"code>

:on-success="handleSuccess"code>

:before-upload="beforeUpload"code>

:http-request="handleUpload"code>

/>

<el-dialog

v-model="textReplaceShow"code>

title="文本替换"code>

width="30%"code>

class="replacedialog"code>

>

<el-form

v-model="txtplace"code>

label-width="auto"code>

>

<el-form-item label="查找文本">code>

<el-input v-model="txtplace.findContent" />code>

</el-form-item>

<el-form-item label="替换文本">code>

<el-input v-model="txtplace.replaceContent" />code>

</el-form-item>

<el-form-item>

<el-button type="primary" @click="handleSubmit">替换</el-button>code>

</el-form-item>

</el-form>

</el-dialog>

</div>

</template>

<style scoped lang="scss">code>

.replacedialog {

.el-form {

.el-form-item {

margin-bottom: 20px;

label {

font-weight: bold;

color: #333;

}

.el-input {

input {

color: #333;

}

}

}

}

}

</style>

<style lang="scss">code>

.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">code>

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%">code>

<!-- 这里组件写ref标识 保证每次组件打开都能更新 -->

<EdtiorUpload

ref="childeditRef"code>

:editvalue="editorcontent"code>

@changevalue="editorChange"code>

/>

</div>

</div>

</template>

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

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。