当前位置:   article > 正文

wangEditor5在vue中自定义菜单栏--格式刷,上传图片,视频功能_wangeditor自定义菜单

wangeditor自定义菜单

复制粘贴修改一下直接用,也写了相关的注释。

一、安装相关插件

  1. npm install @wangeditor/editor
  2. npm install @wangeditor/editor-for-vue

二、官方关键文档

  1. ButtonMenu:https://www.wangeditor.com/v5/development.html#buttonmenu
  2. 注册菜单到wangEditor:自定义扩展新功能 | wangEditor
  3. insertKeys自定义功能的keys:https://www.wangeditor.com/v5/toolbar-config.html#insertkeys
  4. 自定义上传图片视频功能:菜单配置 | wangEditor
  5. 源码地址:GitHub - wangeditor-team/wangEditor: wangEditor —— 开源 Web 富文本编辑器

三、初始化编辑器(wangEdit.vue) 

  1. <template>
  2. <div style="border: 1px solid #ccc">
  3. <Toolbar
  4. style="border-bottom: 1px solid #ccc"
  5. :editor="editor"
  6. :defaultConfig="toolbarConfig"
  7. :mode="mode"
  8. />
  9. <Editor
  10. style="height: 500px; overflow-y: hidden"
  11. v-model="html"
  12. :defaultConfig="editorConfig"
  13. :mode="mode"
  14. @onCreated="onCreated"
  15. @onChange="onChange"
  16. />
  17. </div>
  18. </template>
  19. <script>
  20. // import Location from "@/utils/location";
  21. import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
  22. import { Boot, IModuleConf, DomEditor } from "@wangeditor/editor";
  23. import { getToken } from "@/utils/auth";
  24. import MyPainter from "./geshi";
  25. const menu1Conf = {
  26. key: "geshi", // 定义 menu key :要保证唯一、不重复(重要)
  27. factory() {
  28. return new MyPainter(); // 把 `YourMenuClass` 替换为你菜单的 class
  29. },
  30. };
  31. const module = {
  32. // JS 语法
  33. menus: [menu1Conf],
  34. // 其他功能,下文讲解...
  35. };
  36. Boot.registerModule(module);
  37. export default {
  38. components: { Editor, Toolbar },
  39. props: {
  40. relationKey: {
  41. type: String,
  42. default: "",
  43. },
  44. },
  45. created() {
  46. console.log(this.editorConfig.MENU_CONF.uploadImage.meta.activityKey);
  47. },
  48. data() {
  49. return {
  50. // 富文本实例
  51. editor: null,
  52. // 富文本正文内容
  53. html: "",
  54. // 编辑器模式
  55. mode: "default", // or 'simple'
  56. // 工具栏配置
  57. toolbarConfig: {
  58. //新增菜单
  59. insertKeys: {
  60. index: 32,
  61. keys: ["geshi"],
  62. },
  63. //去掉网络上传图片和视频
  64. excludeKeys: ["insertImage", "insertVideo"],
  65. },
  66. // 编辑栏配置
  67. editorConfig: {
  68. placeholder: "请输入相关内容......",
  69. // 菜单配置
  70. MENU_CONF: {
  71. // ===================
  72. // 上传图片配置
  73. // ===================
  74. uploadImage: {
  75. // 文件名称
  76. fieldName: "contentAttachImage",
  77. server: Location.serverPath + "/editor-upload/upload-image",
  78. headers: {
  79. Authorization: "Bearer " + getToken(),
  80. },
  81. meta: {
  82. activityKey: this.relationKey,
  83. },
  84. // 单个文件的最大体积限制,默认为 20M
  85. maxFileSize: 20 * 1024 * 1024,
  86. // 最多可上传几个文件,默认为 100
  87. maxNumberOfFiles: 10,
  88. // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
  89. allowedFileTypes: ["image/*"],
  90. // 跨域是否传递 cookie ,默认为 false
  91. withCredentials: true,
  92. // 超时时间,默认为 10 秒
  93. timeout: 5 * 1000,
  94. // 自定义插入图片操作
  95. customInsert: (res, insertFn) => {
  96. if (res.errno == -1) {
  97. this.$message.error("上传失败!");
  98. return;
  99. }
  100. insertFn(Location.serverPath + res.data.url, "", "");
  101. this.$message.success("上传成功!");
  102. },
  103. },
  104. // =====================
  105. // 上传视频配置
  106. // =====================
  107. uploadVideo: {
  108. // 文件名称
  109. fieldName: "contentAttachVideo",
  110. server: Location.serverPath + "/editor-upload/upload-video",
  111. headers: {
  112. Authorization: "Bearer " + getToken(),
  113. },
  114. meta: {
  115. activityKey: this.relationKey,
  116. },
  117. // 单个文件的最大体积限制,默认为 60M
  118. maxFileSize: 60 * 1024 * 1024,
  119. // 最多可上传几个文件,默认为 100
  120. maxNumberOfFiles: 3,
  121. // 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
  122. allowedFileTypes: ["video/*"],
  123. // 跨域是否传递 cookie ,默认为 false
  124. withCredentials: true,
  125. // 超时时间,默认为 10 秒
  126. timeout: 15 * 1000,
  127. // 自定义插入图片操作
  128. customInsert: (res, insertFn) => {
  129. if (res.errno == -1) {
  130. this.$message.error("上传失败!");
  131. return;
  132. }
  133. insertFn(Location.serverPath + res.data.url, "", "");
  134. this.$message.success("上传成功!");
  135. },
  136. },
  137. },
  138. },
  139. // ===== data field end =====
  140. };
  141. },
  142. methods: {
  143. // =============== Editor 事件相关 ================
  144. // 1. 创建 Editor 实例对象
  145. onCreated(editor) {
  146. this.editor = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
  147. this.$nextTick(() => {
  148. const toolbar = DomEditor.getToolbar(this.editor);
  149. const curToolbarConfig = toolbar.getConfig();
  150. console.log("【 curToolbarConfig 】-39", curToolbarConfig);
  151. });
  152. },
  153. // 2. 失去焦点事件
  154. onChange(editor) {
  155. this.$emit("change", this.html);
  156. },
  157. // =============== Editor操作API相关 ==============
  158. insertText(insertContent) {
  159. const editor = this.editor; // 获取 editor 实例
  160. if (editor == null) {
  161. return;
  162. }
  163. // 执行Editor的API插入
  164. editor.insertText(insertContent);
  165. },
  166. // =============== 组件交互相关 ==================
  167. // closeEditorBeforeComponent() {
  168. // this.$emit("returnEditorContent", this.html);
  169. // },
  170. closeContent(){
  171. this.html=''
  172. },
  173. // ========== methods end ===============
  174. },
  175. mounted() {
  176. // ========== mounted end ===============
  177. },
  178. beforeDestroy() {
  179. const editor = this.editor;
  180. if (editor == null) {
  181. return;
  182. }
  183. editor.destroy();
  184. console.log("销毁编辑器!");
  185. },
  186. };
  187. </script>
  188. <style lang="scss" scoped>
  189. // 对默认的p标签进行穿透
  190. ::v-deep .editorStyle .w-e-text-container [data-slate-editor] p {
  191. margin: 0 !important;
  192. }
  193. </style>
  194. <style src="@wangeditor/editor/dist/css/style.css"></style>
  1. 自定义上传图片接口
  2. uploadImage: {
  3. // 文件名称
  4. fieldName: "contentAttachImage",
  5. // server: '/api/v1/public/uploadFile',
  6. headers: {
  7. Authorization: "Bearer " + getToken(),
  8. },
  9. meta: {
  10. activityKey: this.relationKey,
  11. },
  12. // 单个文件的最大体积限制,默认为 20M
  13. maxFileSize: 20 * 1024 * 1024,
  14. // 最多可上传几个文件,默认为 100
  15. maxNumberOfFiles: 10,
  16. // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
  17. allowedFileTypes: ["image/*"],
  18. // 跨域是否传递 cookie ,默认为 false
  19. withCredentials: true,
  20. // 超时时间,默认为 10 秒
  21. timeout: 5 * 1000,
  22. 这里设置
  23. customUpload: async (file, insertFn) => {
  24. console.log(file, "file");
  25. let formData = new FormData()
  26. const sub = "order";
  27. formData.append('file', file)
  28. formData.append("sub", sub);
  29. formData.append("type", "1");
  30. let res = await getUploadImg(formData)
  31. insertFn(res.data.full_path, '', '');
  32. },
  33. customInsert: (res, insertFn) => {
  34. if (res.errno == -1) {
  35. this.$message.error("上传失败!");
  36. return;
  37. }
  38. // insertFn(res.data.url, "", "");
  39. this.$message.success("上传成功!");
  40. },
  41. },

四、格式刷功能类js文件

  1. import {
  2. SlateEditor,
  3. SlateText,
  4. SlateElement,
  5. SlateTransforms,
  6. DomEditor,
  7. // Boot,
  8. } from "@wangeditor/editor";
  9. // Boot.registerMenu(menu1Conf);
  10. import { Editor } from "slate";
  11. export default class MyPainter {
  12. constructor() {
  13. this.title = "格式刷"; // 自定义菜单标题
  14. // 这里是设置格式刷的样式图片跟svg都可以,但是注意要图片大小要小一点,因为要应用到鼠标手势上
  15. this.iconSvg = ``;
  16. this.tag = "button"; //注入的菜单类型
  17. this.savedMarks = null; //保存的样式
  18. this.domId = null; //这个可要可不要
  19. this.editor = null; //编辑器示例
  20. this.parentStyle = null; //储存父节点样式
  21. this.mark = "";
  22. this.marksNeedToRemove = []; // 增加 mark 的同时,需要移除哪些 mark (互斥,不能共存的)
  23. }
  24. clickHandler(e) {
  25. console.log(e, "e"); //无效
  26. }
  27. //添加或者移除鼠标事件
  28. addorRemove = (type) => {
  29. const dom = document.body;
  30. if (type === "add") {
  31. dom.addEventListener("mousedown", this.changeMouseDown);
  32. dom.addEventListener("mouseup", this.changeMouseup);
  33. } else {
  34. //赋值完需要做的清理工作
  35. this.savedMarks = undefined;
  36. dom.removeEventListener("mousedown", this.changeMouseDown);
  37. dom.removeEventListener("mouseup", this.changeMouseup);
  38. document.querySelector("#w-e-textarea-1").style.cursor = "auto";
  39. }
  40. };
  41. //处理重复键名值不同的情况
  42. handlerRepeatandNotStyle = (styles) => {
  43. const addStyles = styles[0];
  44. const notVal = [];
  45. for (const style of styles) {
  46. for (const key in style) {
  47. const value = style[key];
  48. if (!addStyles.hasOwnProperty(key)) {
  49. addStyles[key] = value;
  50. } else {
  51. if (addStyles[key] !== value) {
  52. notVal.push(key);
  53. }
  54. }
  55. }
  56. }
  57. for (const key of notVal) {
  58. delete addStyles[key];
  59. }
  60. return addStyles;
  61. };
  62. // 获取当前选中范围的父级节点
  63. getSelectionParentEle = (type, func) => {
  64. if (this.editor) {
  65. const parentEntry = SlateEditor.nodes(this.editor, {
  66. match: (node) => SlateElement.isElement(node),
  67. });
  68. let styles = [];
  69. for (const [node] of parentEntry) {
  70. styles.push(this.editor.toDOMNode(node).style); //将node对应的DOM对应的style对象加入到数组
  71. }
  72. styles = styles.map((item) => {
  73. //处理不为空的style
  74. const newItem = {};
  75. for (const key in item) {
  76. const val = item[key];
  77. if (val !== "") {
  78. newItem[key] = val;
  79. }
  80. }
  81. return newItem;
  82. });
  83. type === "get"
  84. ? func(type, this.handlerRepeatandNotStyle(styles))
  85. : func(type);
  86. }
  87. };
  88. //获取或者设置父级样式
  89. getorSetparentStyle = (type, style) => {
  90. if (type === "get") {
  91. this.parentStyle = style; //这里是个样式对象 例如{textAlign:'center'}
  92. } else {
  93. SlateTransforms.setNodes(
  94. this.editor,
  95. { ...this.parentStyle },
  96. {
  97. mode: "highest", // 针对最高层级的节点
  98. }
  99. );
  100. }
  101. };
  102. //这里是将svg转换为Base64格式
  103. addmouseStyle = () => {
  104. const icon = ``; // 这里是给鼠标手势添加图标
  105. // 将字符串编码为Base64格式
  106. const base64String = btoa(icon);
  107. // 生成数据URI
  108. const dataUri = `data:image/svg+xml;base64,${base64String}`;
  109. // 将数据URI应用于鼠标图标
  110. document.querySelector(
  111. "#w-e-textarea-1"
  112. ).style.cursor = `url('${dataUri}'), auto`;
  113. };
  114. changeMouseDown = () => {}; //鼠标落下
  115. changeMouseup = () => {
  116. //鼠标抬起
  117. if (this.editor) {
  118. const editor = this.editor;
  119. const selectTxt = editor.getSelectionText(); //获取文本是否为null
  120. if (this.savedMarks && selectTxt) {
  121. //先改变父节点样式
  122. this.getSelectionParentEle("set", this.getorSetparentStyle);
  123. // 获取所有 text node
  124. const nodeEntries = SlateEditor.nodes(editor, {
  125. //nodeEntries返回的是一个迭代器对象
  126. match: (n) => SlateText.isText(n), //这里是筛选一个节点是否是 text
  127. universal: true, //当universal为 true 时,Editor.nodes会遍历整个文档,包括根节点和所有子节点,以匹配满足条件的节点。当universal为 false 时,Editor.nodes只会在当前节点的直接子节点中进行匹配。
  128. });
  129. // 先清除选中节点的样式
  130. for (const node of nodeEntries) {
  131. const n = node[0]; //{text:xxxx}
  132. const keys = Object.keys(n);
  133. keys.forEach((key) => {
  134. if (key === "text") {
  135. // 保留 text 属性
  136. return;
  137. }
  138. // 其他属性,全部清除
  139. SlateEditor.removeMark(editor, key);
  140. });
  141. }
  142. // 再赋值新样式
  143. for (const key in this.savedMarks) {
  144. if (Object.hasOwnProperty.call(this.savedMarks, key)) {
  145. const value = this.savedMarks[key];
  146. editor.addMark(key, value);
  147. }
  148. }
  149. this.addorRemove("remove");
  150. }
  151. }
  152. };
  153. getValue(editor) {
  154. // return "MyPainter"; // 标识格式刷菜单
  155. const mark = this.mark;
  156. console.log(mark, "mark");
  157. const curMarks = Editor.marks(editor);
  158. // 当 curMarks 存在时,说明用户手动设置,以 curMarks 为准
  159. if (curMarks) {
  160. return curMarks[mark];
  161. } else {
  162. const [match] = Editor.nodes(editor, {
  163. // @ts-ignore
  164. match: (n) => n[mark] === true,
  165. });
  166. return !!match;
  167. }
  168. }
  169. isActive(editor, val) {
  170. const isMark = this.getValue(editor);
  171. return !!isMark;
  172. // return !!DomEditor.getSelectedNodeByType(editor, "geshi");
  173. // return false;
  174. }
  175. isDisabled(editor) {
  176. //是否禁用
  177. return false;
  178. }
  179. exec(editor, value) {
  180. //当菜单点击后触发
  181. // console.log(!this.isActive());
  182. console.log(value, "value");
  183. this.editor = editor;
  184. this.domId = editor.id.split("-")[1]
  185. ? `w-e-textarea-${editor.id.split("-")[1]}`
  186. : undefined;
  187. if (this.isDisabled(editor)) return;
  188. const { mark, marksNeedToRemove } = this;
  189. if (value) {
  190. // 已,则取消
  191. editor.removeMark(mark);
  192. } else {
  193. // 没有,则执行
  194. editor.addMark(mark, true);
  195. this.savedMarks = SlateEditor.marks(editor); // 获取当前选中文本的样式
  196. this.getSelectionParentEle("get", this.getorSetparentStyle); //获取父节点样式并赋值
  197. // this.addmouseStyle(); //点击之后给鼠标添加样式
  198. this.addorRemove("add"); //处理添加和移除事件函数
  199. // 移除互斥、不能共存的 marks
  200. if (marksNeedToRemove) {
  201. marksNeedToRemove.forEach((m) => editor.removeMark(m));
  202. }
  203. }
  204. if (
  205. editor.isEmpty() ||
  206. editor.getHtml() == "<p><br></p>" ||
  207. editor.getSelectionText() == ""
  208. )
  209. return; //这里是对没有选中或者没内容做的处理
  210. }
  211. }

五、页面应用组件

  1. <el-form-item label="内容">
  2. <WangEdit v-model="form.content" ref="wangEdit" @change="change"></WangEdit>
  3. </el-form-item>
  4. // js
  5. const WangEdit = () => import("@/views/compoments/WangEdit.vue");
  6. export default {
  7. name: "Notice",
  8. components: {
  9. WangEdit,
  10. },
  11. data(){
  12. return{
  13. form:{
  14. }
  15. }
  16. },
  17. methods: {
  18. change(val) {
  19. console.log(val,'aa');
  20. this.form.content=val
  21. },
  22. // 取消按钮
  23. cancel() {
  24. this.open = false;
  25. this.form={};
  26. this.$refs.wangEdit.closeContent();
  27. },
  28. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/493039
推荐阅读
相关标签
  

闽ICP备14008679号