wangEditor5在Vue3中的自定义图片+视频+音频菜单

news2025/1/14 4:16:29

本文适用于wangEditor5用在Vue3中自定义扩展音频、视频、图片菜单;并扩展音频元素节点,保证音频节点的插入、读取、回写功能正常;支持动态修改尺寸。适用于初学者。

1、官网关键文档。

  • ButtonMenu:自定义扩展新功能 | wangEditor
  • ModalMenu:自定义扩展新功能 | wangEditor
  • 注册菜单到wangEditor:定义新元素:自定义扩展新功能 | wangEditor
  • insertKeys:工具栏配置 | wangEditor
  • hoverbarKeys:编辑器配置 | wangEditor
  • 定义新元素:自定义扩展新功能 | wangEditor
  • 各种节点数据结构:节点数据结构 | wangEditor

2、安装

关键包:

npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue
npm install snabbdom

3、初始化编辑器(Editor.vue)

<template>
  <div style="border:1px solid #ccc;height: calc(100% - 30px)">
    <toolbar
        style="border-bottom: 1px solid #ccc"
        :editor="editorRef"
        :defaultConfig="toolbarConfig"
        :mode="mode"
    ></toolbar>
    <editor
        style="height: calc(100% - 81px);overflow-y: hidden"
        v-model="editorValue"
        :defaultConfig="editorConfig"
        :mode="mode"
        @onCreated="handleCreated"></editor>
  </div>
</template>
<script lang="ts" setup>
import '@wangeditor/editor/dist/css/style.css'
import {Toolbar, Editor} from "@wangeditor/editor-for-vue";
import {onBeforeUnmount, onMounted, reactive, ref, shallowRef} from "vue";
import {IToolbarConfig} from "@wangeditor/editor";
const mode = ref('default');
const editorRef = shallowRef();
const editorValue = ref();

onMounted(() => {
   editorValue.value = ''  // 这里可以设置编辑器内容。但是无法设置富文本信息。需要用setHtml
})

onBeforeUnmount(() => { // 当离开后,销毁编辑器实例
  const editor = editorRef.value;
  if (editor == null) {
    return;
  }
  editor.destroy();
})

const toolbarConfig: Partial<IToolbarConfig> = { // 工具栏配置

}
const editorConfig = { // 编辑器配置
  placeholder: '请输入内容...'
}
// 当编辑器创建的时候
const handleCreated = (editor: any) => {
  editorRef.value = editor;// 记录编辑器实例
}
</script>
<style scoped lang="scss">
</style>
初始化后效果

 4、在Editor.vue中隐藏自带的图片、视频上传。

        在toolbarConfig中添加excludeKeys字段。将图片和视频的key填入。

const toolbarConfig: Partial<IToolbarConfig> = {
  excludeKeys: [
    "group-image", // 图片上传:本地上传+网络图片
    "group-video", // 视频上传:本地上传+网络视频
    "fullScreen"
  ]
}

5、创建自定义图片、视频、音频菜单

        这里使用到了ButtonMenu。可以到官网查看具体文档。

        5.1、图片菜单配置文件。

                创建ImageMenu.ts文件。然后在文件中添加以下代码。                

import {IButtonMenu, IDomEditor} from "@wangeditor/editor";

class ImageMenu implements IButtonMenu {
    tag: string;
    title: string;
    iconSvg: string;
    constructor() {
        this.title = '插入图片'; // 鼠标悬浮显示
        this.tag = 'button'
        this.iconSvg = '<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>'; // 菜单图标
    }

    isActive(editor: IDomEditor):boolean{ // 保持默认
        return false
    }
    getValue(editor: IDomEditor): string|boolean{ // 保持默认
        return ''
    }
    isDisabled(editor: IDomEditor): boolean{ // 保持默认
        return false;
    }
    exec(editor: IDomEditor, value: string | boolean){ // 菜单点击事件,这里将点击事件暴露出去
        if(this.isDisabled(editor)){
            return;
        }
        editor.emit('ImageMenuClick');
    }

}

export default ImageMenu

        5.2、视频菜单配置文件。

                创建VideoMenu.ts文件。然后在文件中添加以下代码。                

import {IButtonMenu, IDomEditor} from "@wangeditor/editor";

class VideoMenu implements IButtonMenu {
    tag: string;
    title: string;
    iconSvg: string;

    constructor() {
        this.title = '插入视频';
        this.tag = 'button'
        this.iconSvg = '<svg viewBox="0 0 1024 1024"><path 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"></path></svg>';
    }

    isActive(editor: IDomEditor): boolean {
        return false
    }

    getValue(editor: IDomEditor): string | boolean {
        return ''
    }

    isDisabled(editor: IDomEditor): boolean {
        return false;
    }

    exec(editor: IDomEditor, value: string | boolean) {
        if (this.isDisabled(editor)) {
            return;
        }
        editor.emit('VideoMenuClick');
    }

}

export default VideoMenu

        5.3、音频菜单配置文件。

                创建AudioMenu.ts文件。然后在文件中添加以下代码。

import {IButtonMenu, IDomEditor} from "@wangeditor/editor";

class AudioMenu implements IButtonMenu {
    tag: string;
    title: string;
    iconSvg: string;
    constructor() {
        this.title = '插入音频';
        this.tag = 'button'
        this.iconSvg = '<svg t="1637634971457" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7981" width="16" height="16"><path d="M983.792981 0H40.211115A40.5504 40.5504 0 0 0 0.002048 40.96v942.08c0 22.664533 17.954133 40.96 40.209067 40.96h943.581866a40.5504 40.5504 0 0 0 40.209067-40.96V40.96c0-22.664533-17.954133-40.96-40.209067-40.96z m-235.383466 207.530667v118.784H581.702315v326.8608c0 81.92-62.190933 148.548267-138.8544 148.548266-76.663467 0-138.8544-63.351467-138.8544-141.448533 0-78.097067 62.122667-141.448533 138.8544-141.448533 31.607467 0 60.074667-2.730667 83.3536 16.110933v-327.68l222.208 0.273067z" fill="#999999" p-id="7982"></path></svg>';
    }

    isActive(editor: IDomEditor):boolean{
        return false
    }
    getValue(editor: IDomEditor): string|boolean{
        return ''
    }
    isDisabled(editor: IDomEditor): boolean{
        return false;
    }
    exec(editor: IDomEditor, value: string | boolean){
        if(this.isDisabled(editor)){
            return;
        }
        editor.emit('AudioMenuClick');
    }

}

export default AudioMenu

        5.4、创建初始化文件。index.ts               

import ImageMenu from "@/share/Editor/menus/ImageMenu";
import VideoMenu from "@/share/Editor/menus/VideoMenu";
import AudioMenu from "@/share/Editor/menus/AudioMenu";
import {IDomEditor, IToolbarConfig, Boot, IModuleConf} from "@wangeditor/editor";

const MenusList = [
    {
        key: 'ImageMenu',
        class: ImageMenu,
        index: 22 // 菜单要在工具栏显示的位置
    },
    {
        key: 'VideoMenu',
        class: VideoMenu,
        index: 23 // 菜单要在工具栏显示的位置
    },
    {
        key: 'AudioMenu',
        class: AudioMenu,
        index: 24 // 菜单要在工具栏显示的位置
    }
]
const registerMenu = function (editor: IDomEditor, toolbarConfig: Partial<IToolbarConfig>){
    const allRegisterMenu = editor.getAllMenuKeys(); // 获取所有已注册的菜单
    let keys = [];
    for(let item of MenusList){
        if(allRegisterMenu.indexOf(item.key) < 0){ // 如果未注册,则注册
            const menuObj = {
                key: item.key,
                factory() {
                    return new item.class()
                }
            }
            Boot.registerMenu(menuObj);
        }
        keys.push(item.key)
    }
    toolbarConfig.insertKeys = {
        index: MenusList[0].index,
        keys: keys
    }
}

export default registerMenu

        5.5、在Editor.vue中初始化自定义菜单并注册自定义菜单点击事件。

// 在handleCreated中,获取编辑器实例后注册相关菜单功能。
const handleCreated = (editor: any) => {
  editorRef.value = editor;// 记录编辑器实例
  registerMenu(editorRef.value, toolbarConfig); // 注册自定义菜单。这个是5.4那边声明的
  initMediaMenuEvent(); // 注册自定义菜单点击事件
}

// 事件监听
const initMediaMenuEvent = () => {
  const editor = editorRef.value;
    // 在点击事件中,根据具体菜单,可以触发响应的功能,这里可以为每个事件创建一个el-dialog弹窗。我们就可以完全按照自己的需求开发后续功能
  editor.on('AudioMenuClick', () => {
    // 你点击了音频菜单
  });
  editor.on('ImageMenuClick', () => {
    // 你点击了图片菜单
  });
  editor.on('VideoMenuClick', () => {
   // 你点击了视频菜单
  });
}
点击事件效果展示

6、插入图片到富文本中(具体数据结构可查看1段落中的数据结构文档地址)

editorRef.value.insertNode({
      type: 'image',
      src: url,
      children: [{ // 该字段必须要
        text: ''
      }]
})

7、插入视频到富文本中

editorRef.value.insertNode({
      type: 'video',
      src: url,
      poster:"", // 如果有则填入
      children: [{
        text: ''
      }]
})

8、插入音频到富文本中

 由于wangEditor富文本并不兼容音频插入,所以这里有两种插入方式。按需选择

        8.1、懒人开发。

                其实在HTML中,音频文件也可以直接使用video标签播放,所以对于不想折腾的读者,可以直接使用video插入视频的方式来实现播放音频。

        8.2、费脑子开发。

                这里要注意,这边会涉及到wangEditor中ModalMenu、插件、新元素等方面的内容,具体可以参考官方文档。这边所涉及的源代码是在wangeditor的video源码的上做更改的。涉及多个文件。记得安装snabbdom.js这个包。下面的代码里会用到

                 8.2.1、创建custom-types.ts文件定义Audio节点数据结构。

type EmptyText = {
    text: ''
};
export type AudioElement = {
    type: 'audio'
    src: string
    width?: string
    height?:string
    children: EmptyText[]
}

                  8.2.2、创建render-elem.ts文件,并编写   renderElem函数

                                该函数是为了将insertNode传入的信息在富文本容器中渲染成html代码。

import {DomEditor, IDomEditor, SlateElement} from "@wangeditor/editor";
import {h, VNode} from "snabbdom";
import {AudioElement} from "@/share/Editor/Node/custom-types";

function renderAudioElement(elemNode: SlateElement,children:VNode[] | null, editor: IDomEditor):VNode{
    const {src='',width='300',height='54'} = elemNode as AudioElement;
    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}

                    8.2.3、创建elem-html.ts文件并编写elemToHtml函数。    

                                    8.2.2中所提到的代码已经能够通过editor.insertNode() 将Audio插入到富文本中,但是如果真行editor.getHtml会发现,获取到的代码中并没有audio这类信息,所以还需要编写以下代码。

import {SlateElement} from "@wangeditor/editor";
import {AudioElement} from "@/share/Editor/Node/custom-types";


function audioElemtToHtml(elem: SlateElement, childrenHtml: string): string{
    const {src,width=300,height=54} = elem as AudioElement
// 通过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 controls src="${src}" style="width: ${width};height:${height};max-width: 100%"/>
                     </div>`
    return html
}

const audioToHtmlConf = {
    type: 'audio',
    elemToHtml: audioElemtToHtml
}

export {audioToHtmlConf}

                        8.2.4、创建parse-elem-html.ts文件并编写parseElemHtml 函数。

                                        到8.2.3为止,已经可以将audio元素添加到编辑器中,并从编辑器中获取到富文本内容。但如果现在通过setHtml将富文本内容还原回编辑器,你会发现audio信息丢失或异常。所以还需要编写以下的代码。

import {IDomEditor, SlateDescendant, SlateElement} from "@wangeditor/editor";

function parseAudioElementHtml(domElem:Element, children:SlateDescendant[], editor: IDomEditor): SlateElement {
    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}

                        8.2.5、创建plugin.ts并编写相关内容

                                        到8.2.4为止,已经能够实现audio自定义元素的插入,获取,读取的功能,但是当你插入之后会发现,插入audio元素后,audio元素之后的富文本区域变成不可编辑,哪怕你通过insertBreak()往后面添加换行也没用,所以还需要通过编写plugin.ts来避免这个问题。这里的代码基本复用了video模块的代码,只是将里面的video信息改成了audio信息。

import {DomEditor, IDomEditor} from "@wangeditor/editor";
import {AudioElement} from "@/share/Editor/Node/custom-types";
import { Transforms } from 'slate'

function withAudio<T extends IDomEditor>(editor: T): T {
    const { isVoid, normalizeNode } = editor
    const newEditor = editor

    // 重写 isVoid

    // @ts-ignore
    newEditor.isVoid = (elem: AudioElement) => {
        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

                        8.2.6、注册这些功能。

                                到此,已经完成了audio新元素所有的基础功能,也能保证编辑器的正常使用。为了保证audio新元素的正常使用,上面所述的所有功能模块需要在编辑器初始化前进行初始化。所以为上面这些功能模块编写index.ts作为初始化入口。

import {Boot} from "@wangeditor/editor";
import {renderAudioConf} from "@/share/Editor/Node/render-elem";
import {audioToHtmlConf} from "@/share/Editor/Node/elem-html";
import {parseAudioHtmlConf} from "@/share/Editor/Node/parse-elem-html";
import withAudio from "@/share/Editor/Node/plugin";



function initAudioNode() {
    Boot.registerRenderElem(renderAudioConf);
    Boot.registerElemToHtml(audioToHtmlConf)
    Boot.registerParseElemHtml(parseAudioHtmlConf)
    Boot.registerPlugin(withAudio);
}

export default initAudioNode

                                并在Editor.vue中,handleCreated函数之前引入该模块并调用。initAudioNode();

9、动态修改audio元素尺寸。

        如果你插入过video元素,你点击该元素会发现,该元素支持动态修改尺寸的功能。

        但是你去点击audio元素,却无法触发该功能,所以还需要一下的代码来实现。

        9.1、从wangeditor中复制两份代码出来。

//创建dom.ts文件,将以下代码复制进去
/**
 * @description DOM 操作
 * @author wangfupeng
 */

import $, { append, on, focus, attr, val, html, parent, hasClass, Dom7Array, empty } from 'dom7'
export { Dom7Array } from 'dom7'

if (append) $.fn.append = append
if (on) $.fn.on = on
if (focus) $.fn.focus = focus
if (attr) $.fn.attr = attr
if (val) $.fn.val = val
if (html) $.fn.html = html
if (parent) $.fn.parent = parent
if (hasClass) $.fn.hasClass = hasClass
if (empty) $.fn.empty = empty

export default $

/**
 * 获取 tagName lower-case
 * @param $elem $elem
 */
export function getTagName($elem: Dom7Array): string {
    if ($elem.length) return $elem[0].tagName.toLowerCase()
    return ''
}

/**
 * 生成带 size 样式的 iframe html
 * @param iframeHtml iframe html string
 * @param width width
 * @param height height
 * @returns iframe html string with size style
 */
export function genSizeStyledIframeHtml(
    iframeHtml: string,
    width: string = 'auto',
    height: string = 'auto'
): string {
    const $iframe = $(iframeHtml)
    $iframe.attr('width', width)
    $iframe.attr('height', height)
    return $iframe[0].outerHTML
}

// COMPAT: This is required to prevent TypeScript aliases from doing some very
// weird things for Slate's types with the same name as globals. (2019/11/27)
// https://github.com/microsoft/TypeScript/issues/35002
import DOMNode = globalThis.Node
import DOMComment = globalThis.Comment
import DOMElement = globalThis.Element
import DOMText = globalThis.Text
import DOMRange = globalThis.Range
import DOMSelection = globalThis.Selection
import DOMStaticRange = globalThis.StaticRange
export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }
//创建util.ts文件,并将以下代码复制进去
/**
 * @description 工具函数
 * @author wangfupeng
 */

import { nanoid } from 'nanoid'

/**
 * 获取随机数字符串
 * @param prefix 前缀
 * @returns 随机数字符串
 */
export function genRandomStr(prefix: string = 'r'): string {
    return `${prefix}-${nanoid()}`
}

export function replaceSymbols(str: string) {
    return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

        9.2、根据ModalMenu编写EditAudioSizeMenu.ts文件

                       该文件是根据video模块的EditAudioSizeMenu.ts修改的,具体逻辑自行分析。


import { Node as SlateNode, Transforms } from 'slate'
import {
    IModalMenu,
    IDomEditor,
    DomEditor,
    genModalInputElems,
    genModalButtonElems,
    t,
} from '@wangeditor/core'
import $, { Dom7Array, DOMElement } from '../dom'
import { genRandomStr } from '../util'
import {AudioElement} from "@/share/Editor/Node/custom-types";

/**
 * 生成唯一的 DOM ID
 */
function genDomID(): string {
    return genRandomStr('w-e-insert-audio')
}

class EditorAudioSizeMenu implements IModalMenu {
    readonly title = t('修改尺寸')
    readonly tag = 'button'
    readonly showModal = true // 点击 button 时显示 modal
    readonly modalWidth = 320
    private $content: Dom7Array | null = null
    private readonly widthInputId = genDomID()
    private readonly heightInputId = genDomID()
    private readonly buttonId = genDomID()

    private getSelectedAudioNode(editor: IDomEditor): SlateNode | null {
        return DomEditor.getSelectedNodeByType(editor, 'audio')
    }

    getValue(editor: IDomEditor): string | boolean {
        // 插入菜单,不需要 value
        return ''
    }

    isActive(editor: IDomEditor): boolean {
        // 任何时候,都不用激活 menu
        return false
    }

    exec(editor: IDomEditor, value: string | boolean) {
        // 点击菜单时,弹出 modal 之前,不需要执行其他代码
        // 此处空着即可
    }

    isDisabled(editor: IDomEditor): boolean {
        if (editor.selection == null) return true

        const audioNode = this.getSelectedAudioNode(editor)
        if (audioNode == null) {
            return true
        }
        return false
    }

    getModalPositionNode(editor: IDomEditor): SlateNode | null {
        return this.getSelectedAudioNode(editor)
    }

    getModalContentElem(editor: IDomEditor): DOMElement {

        const { widthInputId, heightInputId, buttonId } = this


        const [widthContainerElem, inputWidthElem] = genModalInputElems(
            t('宽度'),
            widthInputId,
            '300'
        )
        const $inputWidth = $(inputWidthElem)
        const [heightContainerElem, inputHeightElem] = genModalInputElems(
            t('高度'),
            heightInputId,
            '54'
        )
        const $inputHeight = $(inputHeightElem)
        const [buttonContainerElem] = genModalButtonElems(buttonId, t('确定'))

        if (this.$content == null) {
            // 第一次渲染
            const $content = $('<div></div>')

            // 绑定事件(第一次渲染时绑定,不要重复绑定)
            $content.on('click', `#${buttonId}`, e => {
                e.preventDefault()
                const rawWidth = $content.find(`#${widthInputId}`).val().trim()
                const rawHeight = $content.find(`#${heightInputId}`).val().trim()
                const numberWidth = parseInt(rawWidth)
                const numberHeight = parseInt(rawHeight)
                const width = numberWidth ? numberWidth.toString() : '300'
                const height = numberHeight ? numberHeight.toString() : '54'

                editor.restoreSelection()
                // 修改尺寸
                Transforms.setNodes(
                    editor,
                    // @ts-ignore
                    { width, height },
                    {
                        match: n => DomEditor.checkNodeType(n, 'audio'),
                    }
                )

                editor.hidePanelOrModal() // 隐藏 modal
            })

            this.$content = $content
        }

        const $content = this.$content

        // 先清空,再重新添加 DOM 内容
        $content.empty()
        $content.append(widthContainerElem)
        $content.append(heightContainerElem)
        $content.append(buttonContainerElem)

        const audioNode = this.getSelectedAudioNode(editor) as AudioElement
        if (audioNode == null) return $content[0]

        // 初始化 input 值
        const { width = '300', height = '54' } = audioNode
        $inputWidth.val(width)
        $inputHeight.val(height)
        setTimeout(() => {
            $inputWidth.focus()
        })

        return $content[0]
    }
}

export default EditorAudioSizeMenu

        9.3、修改5.4中提到的index.ts文件部分逻辑

                        这里其实并没有修改多少代码,只是对audio元素做针对性处理。

import ImageMenu from "@/share/Editor/menus/ImageMenu";
import VideoMenu from "@/share/Editor/menus/VideoMenu";
import AudioMenu from "@/share/Editor/menus/AudioMenu";
import {IDomEditor, IToolbarConfig, Boot, IModuleConf} from "@wangeditor/editor";
import EditAudioSizeMenu from "@/share/Editor/menus/EditAudioSizeMenu";

const MenusList = [
    {
        key: 'ImageMenu',
        class: ImageMenu,
        index: 22
    },
    {
        key: 'VideoMenu',
        class: VideoMenu,
        index: 23
    },
    {
        key: 'AudioMenu',
        class: AudioMenu,
        index: 24
    }
]
const registerMenu = function (editor: IDomEditor, toolbarConfig: Partial<IToolbarConfig>){
    const allRegisterMenu = editor.getAllMenuKeys(); // 获取所有已注册的菜单
    let keys = [];
    for(let item of MenusList){
        if(allRegisterMenu.indexOf(item.key) < 0){ // 如果未注册,则注册
            const menuObj = {
                key: item.key,
                factory() {
                    return new item.class()
                }
            }
            if(item.key === 'AudioMenu'){ // 如果是音频菜单
                const editorAudioSizeMenuConf = { // 先注册修改尺寸的modalMenu菜单
                    key: 'editAudioSize', // 任意命名,但需要跟9.4中用到的保存一致
                    factory() {
                        return new EditAudioSizeMenu() // 这个就是9.2中提到的方法
                    },
                }
                const module: Partial<IModuleConf> = {
                    menus: [menuObj, editorAudioSizeMenuConf],
                }
                 Boot.registerModule(module);// 这里的注册方法跟普通的不太一样,注意
            } else {
                Boot.registerMenu(menuObj);
            }
        }
        keys.push(item.key)
    }
    toolbarConfig.insertKeys = {
        index: MenusList[0].index,
        keys: keys
    }
}

export default registerMenu

        9.4、将modalmenu注册到编辑器的hoverbarKeys里。

// 在editorConfig中注册hoverbarKeys
const editorConfig = {
  placeholder: '请输入内容...',
  hoverbarKeys: {
    audio: {
      menuKeys: ['editAudioSize'] // 这个key就是9.3里声明的
    }
  }
}

到此,自定义媒体资源菜单就结束了。下面为最终效果的演示视频。

wangEditor模拟操作

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

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

相关文章

所有Gcc版本对C和C++的支持情况(超详细版本)

在最近接触的新的项目&#xff0c;由于技术使用为C98风格实现&#xff0c;遇到一个问题需要加锁解决&#xff0c;本能反应用lock_guradmutex解决&#xff0c;但是没设置CFLAGS为C11标准&#xff0c;不确定当前gcc编译器默认支持的C和C标准是什么&#xff0c;索性就一把都研究透…

G - 李华和图案

第一周刷题&#xff1a; 【题目描述】 李华有大小的格局nn&#xff0c;每个单元格为蓝色或红色。他可以表演完全k操作。在每次操作中&#xff0c;他选择一个单元格并将其颜色从红色更改为蓝色或从蓝色更改为红色。每个单元格都可以根据需要多次选择。是否有可能使图案与其旋转…

Vue-高德地图-立体多边形绘制

前言 在前端开发中&#xff0c;地图展示是常见的需求之一。而高德地图作为国内知名的地图服务商&#xff0c;其提供的 API 功能丰富&#xff0c;使用也相对简单。 本篇文章将介绍如何在 Vue2 中使用 amap/amap-jsapi-loader 安装高德地图并展示地图。 准备工作 首先需要在高…

02 【Sass语法介绍-变量】

sass有两种语法格式Sass(早期的缩进格式&#xff1a;Indented Sass)和SCSS(Sassy CSS) 目前最常用的是SCSS&#xff0c;任何css文件将后缀改为scss&#xff0c;都可以直接使用Sassy CSS语法编写。 所有有效的 CSS 也同样都是有效的 SCSS。 Sass语法介绍-变量 1.前言 Sass …

华为面试题:1+4=5,2+5=12,3+6=21,问8+11=?网友:幼儿园级别

面试&#xff0c;一直都是职场人士绕不过去的坎&#xff0c;对于有的人来说&#xff0c;或许更擅长日常的工作&#xff0c;在面试环节可谓是自己的薄弱环节&#xff0c;但对于有的人来说&#xff0c;相比于工作&#xff0c;更擅长应付面试&#xff01; 最近&#xff0c;有一位…

为了了解国外AI最新动态,分享我经常逛的6 个 YouTube AI频道

AI 正在迅速发展&#xff0c;每周都会有一篇关于该领域新发展的新论文&#xff0c;一种可以提高您工作效率的 AI 工具&#xff0c;或者一个改变一切的公告。 这就是为什么在本文中&#xff0c;我想与您分享最好的 YouTube 频道&#xff0c;以便及时了解 AI 的最新动态。这些 Y…

JUC多并发编程 原子类

基本类型原子类 AtomicIntegerAtomicBooleanAtomicLong 方法说明public final int get()获取当前的值public final int getAndSet(int newValue)获得当前的值,并设置新的值public final int getAndIncrement()获得当前的值&#xff0c;并自增public final int getAndDecremen…

C++内存管理之拷贝memcpy、分配malloc 与释放free

1.内存拷贝 memcpy C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。 #声明&#xff1a; void *memcpy(void *str1, const void *str2, size_t n)#********************** str1 -- 指向用于存储复制内容的目标数组…

【数据结构】二叉树-OJ

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 一、相同的树 二、翻转二叉树 三、单值二叉树 四、 二叉树的最大深度 一、相同的树 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。如果两个…

第六章 3D地形搭建(上)

Unity 提供了多种工具来创建环境特征&#xff0c;例如地形和植被。要在场景中添加地形 (Terrain) 游戏对象&#xff0c;请从菜单中选择 GameObject > 3D Object > Terrain。此过程也会在 Project 视图中添加相应的地形资源。默认情况&#xff0c;场景中出现一个大型平坦的…

Spring AOP 代码加案例详解

Spring AOP目录 Spring AOP主要内容代理模式静态代理动态代理JDK动态代理CGLIB 动态代理JDK代理与CGLIB代理的区别 Spring AOP当前项目存在问题和解决Spring AOP的介绍AOP基本概念实现AOP的两种方式 Spring AOP —— Schema-based方式前置通知 - 入门案例思考后置通知异常通知环…

高压功率放大器在脉冲X射线源技术及火星X射线通信中的应用

实验名称&#xff1a;高速调制脉冲X射线源技术及火星X射线通信应用研究 研究方向&#xff1a;通信技术 测试目的&#xff1a; 火星是深空探测的热点区域&#xff0c;随着对火星探测的深入&#xff0c;未来火星探测器将面临传统通信方式难以应对的恶劣情况&#xff0c;例如火…

ROS学习5:ROS常用组件

【Autolabor初级教程】ROS机器人入门 1. TF 坐标变换 背景 现有一移动式机器人底盘&#xff0c;在底盘上安装了一雷达&#xff0c;雷达相对于底盘的偏移量已知&#xff0c;现雷达检测到一障碍物信息&#xff0c;获取到坐标分别为(x,y,z)&#xff0c;该坐标是以雷达为参考系的…

测试岗35k*16薪,性能测试面试题问什么?测试老鸟总结分析(附答案)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 软件测试面试题&am…

【Python/机器学习】不使用Conda安装Pytorch和Torchvision(Windows系统)

这篇文章介绍如何不使用conda等包管理系统在Windows系统上直接使用pip安装Pytorch和Torchvision。首先你需要有Python 3.6以上的64位环境&#xff08;32位是不可以的哟&#xff01;&#xff09;&#xff0c;并且假设你有NVIDIA显卡且已安装CUDA。 文章目录 1. 查看CUDA版本2. 找…

简记二分算法模板与代码案例:整数二分

本文以 Java 语言实现&#xff0c;整理的代码模板适用于编程竞赛。对代码模板原理的讲解不多&#xff0c;主要记录一下如何使用。 目录 一、算法模板&#xff1a;整数二分 二、例题 一、算法模板&#xff1a;整数二分 整数二分有两套算法模板&#xff0c;这两套算法模板几乎…

03 【Sass语法介绍-嵌套】

1.前言 在企业的实际项目开发中&#xff0c;Sass 的嵌套可以说是非常非常有用的&#xff0c;它可以让你的 CSS 代码易于管理和维护&#xff0c;看起来也比较清晰&#xff0c;这也是 Sass 中很基础的一个知识点&#xff0c;首先掌握这个至关重要&#xff01;在此章节我们将学习…

Golang题目总结

1. slice底层数据结构和扩容原理 数据结构 Go 的 slice 底层数据结构是由一个 array 指针指向底层数组&#xff0c;len 表示切片长度&#xff0c;cap 表示切片容量。扩容原理 &#xff08;1&#xff09;扩容思路&#xff1a;对于 append 向 slice 添加元素时&#xff0c;若 sl…

STM32-HAL-SPI-读写W25Q128FV-JEDEC ID(1)

文章目录 一、SPI串行通信协议1.1 SPI通信协议简介1.2 SPI工作原理1.3 SPI特性 二、W25Q128FV芯片介绍2.1 芯片基本参数介绍2.2 芯片管脚介绍2.3 技术手册等更多信息 三、开发板的板载Flash的连接电路四、测试准备五、初始化片上外设SPI15.1 初始化SPI15.2 设置片选引脚PB145.3…

【网页小功能 最简单入门!!!表白墙】【html+javaScript+css实现 简易 网页版 表白墙】

网页小功能 最简单入门&#xff01;&#xff01;&#xff01; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"…