wangEditor官网 开源 Web 富文本编辑器,开箱即用,配置简单
要实现一个完整的富文本编辑器功能,你可能还需要以下功能:
- 内容处理 - 获取内容,设置内容,展示内容
- 工具栏配置 - 插入新菜单,屏蔽某个菜单等
- 编辑器配置 - 兼听各个生命周期,自定义粘贴
- 菜单配置 - 配置颜色、字体、字号、链接校验、上传图片、上传视频等
- 编辑器 API - 控制编辑器内容和选区
- 扩展新功能 - 扩展菜单、元素、插件等
Vue2
安装
npm install @wangeditor/editor --save
# 或者 yarn add @wangeditor/editor
npm install @wangeditor/editor-for-vue --save
# 或者 yarn add @wangeditor/editor-for-vue
使用
<template>
<div style="border: 1px solid #ccc;">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden;"
v-model="html"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
/>
</div>
</template>
<script>
import Vue from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default Vue.extend({
components: { Editor, Toolbar },
data() {
return {
editor: null,
html: '<p>hello</p>',
toolbarConfig: { },
editorConfig: { placeholder: '请输入内容...' },
mode: 'default', // or 'simple'
}
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
},
mounted() {
// 模拟 ajax 请求,异步渲染编辑器
setTimeout(() => {
this.html = '<p>模拟 Ajax 异步设置内容 HTML</p>'
}, 1500)
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
}
})
</script>
TIP:
赋值 this.editor 时要用 Object.seal()
组件销毁时,要及时销毁编辑器
wangEditor富文本编辑框中,有上传图片和上传视频的功能,但是没有上传音频的功能,所以就需要使用wangEditor的自动以扩展新功能来实现上传音频的功能
自定义扩展新功能
文件目录为:
1. 注册新菜单
import { IDomEditor, IDropPanelMenu } from "@wangeditor/editor";
// class MyDropPanelMenu implements IDropPanelMenu {
// TS 语法
export default class AudioMenu {
// JS 语法
constructor() {
this.title = "上传音频";
this.iconSvg =
'<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#FF7F7F" d="M849.408 6.656L411.648 140.8c-53.248 15.36-95.744 70.656-95.744 123.392v461.312S284.16 704 213.504 714.24C109.568 729.088 25.6 808.448 25.6 891.904s83.968 134.656 187.904 119.808c103.936-14.848 179.712-91.648 179.712-175.104v-445.44c0-36.864 44.544-52.736 44.544-52.736l387.072-121.344s43.008-14.336 43.008 25.088v367.616s-39.424-22.528-110.08-14.336c-103.936 12.8-187.904 90.624-187.904 174.08S653.824 905.728 757.76 893.44c103.936-12.8 187.904-90.624 187.904-174.08V74.752c-0.512-52.224-43.52-82.944-96.256-68.096z" /></svg>';
this.tag = "button";
this.showDropPanel = true;
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
// isActive(editor: IDomEditor): boolean {
// TS 语法
isActive(editor) {
// JS 语法
return false;
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
// getValue(editor: IDomEditor): string | boolean {
// TS 语法
getValue(editor) {
// JS 语法
return "";
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
// isDisabled(editor: IDomEditor): boolean {
// TS 语法
isDisabled(editor) {
// JS 语法
return false;
}
// 点击菜单时触发的函数
// exec(editor: IDomEditor, value: string | boolean) {
// TS 语法
exec(editor, value) {
// JS 语法
// DropPanel menu ,这个函数不用写,空着即可
if (this.isDisabled(editor)) {
return;
}
editor.emit("AudioMenuClick");
}
}
// export const menu1Conf = {
// key: "uploadAudio", // 定义 menu key :要保证唯一、不重复(重要)
// factory() {
// return new AudioMenu(); // 把 `YourMenuClass` 替换为你菜单的 class
// }
// };
2. 注册菜单到wangEditor,并插入菜单到工具栏 --- index.vue
onCreated(editor) {
this.editorRef = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
this.toolbarConfig.insertKeys = {
index: 24, // 插入的位置,基于当前的 toolbarKeys
keys: ["menu1"]
};
// 注册菜单
Boot.registerMenu(menu1Conf);
module();
// 事件监听
const initMediaMenuEvent = () => {
const editor = this.editorRef;
// 在点击事件中,根据具体菜单,可以触发响应的功能,这里可以为每个事件创建一个el-dialog弹窗。我们就可以完全按照自己的需求开发后续功能
editor.on("AudioMenuClick", () => {
// 你点击了音频菜单
console.log("123");
editor.insertNode({
type: "audio",
src: "http://music.163.com/song/media/outer/url?id=1908673805.mp3",
children: [{ text: "aaa" }]
});
});
};
initMediaMenuEvent(); // 注册自定义菜单点击事件
}
3. 定义节点数据结构 --- plugin.js
import { DomEditor, IDomEditor } from "@wangeditor/editor";
import { Transforms } from "slate";
function withAudio(editor) {
const { isVoid, normalizeNode } = editor;
const newEditor = editor;
// 重写 isVoid
// @ts-ignore
newEditor.isVoid = elem => {
const { type } = elem;
if (type === "audio") {
return true;
}
return isVoid(elem);
};
// 重写 normalizeNode
newEditor.normalizeNode = ([node, path]) => {
const type = DomEditor.getNodeType(node);
// ----------------- audio 后面必须跟一个 p header blockquote -----------------
if (type === "audio") {
// -------------- audio 是 editor 最后一个节点,需要后面插入 p --------------
const isLast = DomEditor.isLastNode(newEditor, node);
if (isLast) {
Transforms.insertNodes(newEditor, DomEditor.genEmptyParagraph(), {
at: [path[0] + 1]
});
}
}
// 执行默认的 normalizeNode ,重要!!!
return normalizeNode([node, path]);
};
// 返回 editor ,重要!
return newEditor;
}
export default withAudio;
4. 在编辑器中渲染新元素 --- render-elem.js
必须安装 snabbdom.js
yarn add snabbdom --peer
## 安装到 package.json 的 peerDependencies 中即可
import { DomEditor, IDomEditor, SlateElement } from "@wangeditor/editor";
import { h, VNode } from "snabbdom";
function renderAudioElement(elemNode, children, editor) {
const { src = "", width = "300", height = "54" } = elemNode;
const selected = DomEditor.isNodeSelected(editor, elemNode);
const audioVnode = h(
"audio", // html标签
{
props: {
src: src,
contentEditable: false,
controls: true
},
style: {
width: width + "px",
height: height + "px",
"max-width": "100%" // 这里之所以要写死,是为了实现宽度自适应的。如果直接设置width:100%,会触发报错。所以想要实现width:100%效果,需要先设置max-width,然后在给width设置一个离谱的值,比如说100000.
}
}
);
const vnode = h(
"div",
{
props: {
className: "w-e-textarea-video-container", // 这里直接复用video的效果
"data-selected": selected ? "true" : ""
}
},
audioVnode
);
const containerVnode = h(
"div",
{
props: {
contentEditable: false
},
on: {
mousedown: e => e.preventDefault()
}
},
vnode
);
return containerVnode;
}
const renderAudioConf = {
type: "audio", // 新元素 type ,重要!!!即custom-type中定义的type
renderElem: renderAudioElement
};
export { renderAudioConf };
5. 把新元素转换为 HTML --- elem-to-html.js
import { SlateElement } from "@wangeditor/editor";
function audioElemtToHtml(elem, childrenHtml) {
const { src, width = 300, height = 54 } = elem;
// 通过data-w-e开头的data数据,存放一些必要的信息,到时候通过setHtml将富文本信息还原回编辑器的时候,才能使编辑器正常识别
const html = `<div data-w-e-type="audio" data-w-e-is-void data-w-e-type="audio" data-w-e-width="${width}" data-w-e-height="${height}" data-src="${src}" data-width="${width}" data-height="${height}">
<audio poster="" controls style="width:${width};height:${height};max-width:100%" src="${src}"><source src="${src}" type="audio/mpeg"/></audio>
</div>`;
return html;
}
const audioToHtmlConf = {
type: "audio",
elemToHtml: audioElemtToHtml
};
export { audioToHtmlConf };
6. 解析新元素 HTML 到编辑器 --- parse-elem-html.js
import { IDomEditor, SlateDescendant, SlateElement } from "@wangeditor/editor";
function parseAudioElementHtml(domElem,children,editor) {
const src = domElem.getAttribute("data-src"); // 这些就是elem-html.ts自定义扩展存放的地方,可以根据需要自行扩展
const height = domElem.getAttribute("data-height");
const width = domElem.getAttribute("data-width");
const myAudio = {
// 这里的信息要和custom-types.ts一致
type: "audio",
src,
width,
height,
children: [{ text: "" }]
};
return myAudio;
}
const parseAudioHtmlConf = {
selector: 'div[data-w-e-type="audio"]', // 这个就是elem-html.ts中第一个div里包含的信息
parseElemHtml: parseAudioElementHtml
};
export { parseAudioHtmlConf };
7. 注册插件到 wangEditor --- index.js
import { IModuleConf } from "@wangeditor/editor";
import { Boot } from "@wangeditor/editor";
import { renderAudioConf } from "./render-elem";
import { audioToHtmlConf } from "./elem-to-html";
import { parseAudioHtmlConf } from "./parse-elem-html";
import withAudio from "./plugin";
function module() {
Boot.registerRenderElem(renderAudioConf);
Boot.registerElemToHtml(audioToHtmlConf);
Boot.registerParseElemHtml(parseAudioHtmlConf);
Boot.registerPlugin(withAudio);
}
export default module;
8. 在index.vue中导入并使用
import module from '../../../plugins/module'
onCreated(editor) {
module()
}
以上就是今天的全部内容啦。遇到问题就留言。