wangEditor icon indicating copy to clipboard operation
wangEditor copied to clipboard

V5 版本自定义表情扩展参考示例

Open AnyFork opened this issue 1 year ago • 2 comments

功能示例参考

V5 版本自定义表情扩展参考示例,供大家借鉴学习

项目框架

Nuxt3 最新版

wangEditor 版本

"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",

最终效果图

image

大致思路

1 参考文档自定义拓展功能,自定义表情菜单。 2 将表情放在项目public/emoji目录下,通过node加载public/emoji目录下面的所有图片文件,获取到文件名名称,拼装表情地址 3 支持emoji表情,例如:😃 ,自定义图片。

获取public/emoji目录下的node代码

import fs from 'fs'
import { fileURLToPath } from 'url'
import { dirname, resolve } from 'path'
const __filename = fileURLToPath(import.meta.url)

// 递归函数,用于获取目录中的所有文件
const getAllFiles = (dir: string) => {
    // 存放所有文件的数组
    const files: string[] = []
    try {
        // 同步地读取目录内容
        const dirents = fs.readdirSync(dir, { withFileTypes: true })
        for (let i = 0; i < dirents.length; i++) {
            if (dirents[i].isDirectory()) {
                // 如果当前项为子目录,则递归调用getAllFiles()函数
                const subDir = `${dir}/${dirents[i].name}`
                files.push(...getAllFiles(subDir))
            } else {
                // 否则将该文件添加到files数组中
                files.push(`${dir}/${dirents[i].name}`)
            }
        }
        return files
    } catch (err) {
        console.error(err)
        throw err
    }
}
/**
 * 获取表情文件夹下面所有的表情图片名称
 */
export default defineEventHandler(() => {
    try {
        // 表情存放路径
        const emojiPath = process.env.NODE_ENV === 'development' ? resolve(dirname(__filename), '../../public/emoji') : resolve(dirname(__filename), '../public/emoji')
        //递归获取所有图片地址
        const allFiles = getAllFiles(emojiPath)
        // 默认静态图标,unicode编码
        const defaultEmoji: emojiList = {
            groupName: '默认',
            groupType: 'emoji',
            groupArray: '😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 😛 😝 😜 🤓 😎 😏 😒 😞 😔 😟 😕 🙁 😣 😖 😫 😩 😢 😭 😤 😠 😡 😳 😱 😨 🤗 🤔 😶 😑 😬 🙄 😯 😴 😷 🤑 😈 🤡 👻 💀 👀 👣 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🖕 ✍️ 🙏'.split(
                ' '
            )
        }
        //QQ经典动态表情
        const defaults: emojiList = { groupName: 'QQ经典', groupType: 'image', groupArray: [] }
        //气泡熊动态表情
        const ppx: emojiList = { groupName: '气泡熊', groupType: 'image', groupArray: [] }
        //蓝色QQ动态表情
        const grapeman: emojiList = { groupName: '蓝色QQ表情', groupType: 'image', groupArray: [] }
        //嘻哈猴动态表情
        const coolmonkey: emojiList = { groupName: '嘻哈猴', groupType: 'image', groupArray: [] }
        //QQ动态表情
        const comcom: emojiList = { groupName: 'QQ动态', groupType: 'image', groupArray: [] }
        // 图片处理
        allFiles.forEach((element) => {
            const file = element.split('emoji')[1]
            const fileImgSrc = process.env.NODE_ENV === 'development' ? `/_nuxt/emoji${file}` : `/emoji${file}`
            // 因为有分组,图片存在不同的目录下
            if (fileImgSrc.includes('default')) {
                defaults.groupArray.push(fileImgSrc)
            }
            if (fileImgSrc.includes('ppx')) {
                ppx.groupArray.push(fileImgSrc)
            }
            if (fileImgSrc.includes('grapeman')) {
                grapeman.groupArray.push(fileImgSrc)
            }
            if (fileImgSrc.includes('coolmonkey')) {
                coolmonkey.groupArray.push(fileImgSrc)
            }
            if (fileImgSrc.includes('comcom')) {
                comcom.groupArray.push(fileImgSrc)
            }
        })
        // 图片名称
        return [defaultEmoji, defaults, ppx, grapeman, coolmonkey, comcom]
    } catch (err) {
        console.error(err)
    }
})

插件配置

import { Boot, type IDomEditor, type IDropPanelMenu } from '@wangeditor/editor'
import attachmentModule from '@wangeditor/plugin-upload-attachment'
import type { DOMElement } from '@wangeditor/editor/dist/editor/src/utils/dom'
import $ from 'jquery'

class EmojiDropPanelMenu implements IDropPanelMenu {
    showDropPanel: boolean
    title: string
    iconSvg?: string | undefined
    hotkey?: string | undefined
    alwaysEnable?: boolean | undefined
    tag: string
    width?: number | undefined
    emojiArray: emojiList[]

    constructor(emojiArray: emojiList[]) {
        this.title = '表情'
        this.iconSvg =
            '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 17.5c2.33 0 4.3-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5M8.5 11A1.5 1.5 0 0 0 10 9.5A1.5 1.5 0 0 0 8.5 8A1.5 1.5 0 0 0 7 9.5A1.5 1.5 0 0 0 8.5 11m7 0A1.5 1.5 0 0 0 17 9.5A1.5 1.5 0 0 0 15.5 8A1.5 1.5 0 0 0 14 9.5a1.5 1.5 0 0 0 1.5 1.5M12 20a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8m0-18C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg>'
        this.tag = 'button'
        this.showDropPanel = true
        this.emojiArray = emojiArray
    }

    // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
    isActive(editor: IDomEditor): boolean {
        return false
    }

    // 获取菜单执行时的 value ,用不到则返回空 字符串或 false
    getValue(editor: IDomEditor): string | boolean {
        return ''
    }

    // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
    isDisabled(editor: IDomEditor): boolean {
        return false
    }

    // 点击菜单时触发的函数
    exec(editor: IDomEditor, value: string | boolean) {
        // TS 语法
        // exec(editor, value) {                              // JS 语法
        // DropPanel menu ,这个函数不用写,空着即可
    }

    // 定义 DropPanel 内部的 DOM Element
    getPanelContentElem(editor: IDomEditor): DOMElement {
        const div = $(`<div class="emoji-box"></div`)
        const div2 = $(`<div class="tabs-box"></div`)
        div.append(div2)
        this.emojiArray.forEach((item) => {
            const { groupName, groupType, groupArray } = item
            const div3 = $(`<div class="tab-item">${groupName}</div>`)
            const div4 = $(`<div class="emoji-list"></div>`)
            groupArray.forEach((emoji: string) => {
                if (groupType === 'emoji') {
                    const div5 = $(`<div class="emoji-item">${emoji}</div>`)
                    div5.on('click', () => {
                        editor.insertText(emoji)
                        editor.insertText(' ')
                    })
                    div4.append(div5)
                }
                if (groupType === 'image') {
                    const div5 = $(`<div class="emoji-item"><img src='${emoji}'/></div>`)
                    div5.on('click', () => {
                        //新建一个imageNode
                        const image = {
                            type: 'image',
                            src: emoji,
                            href: emoji,
                            alt: 'emoji-image',
                            style: {},
                            // 【注意】void node 需要一个空 text 作为 children
                            children: [{ text: '' }]
                        }
                        editor.insertNode(image)
                    })
                    div4.append(div5)
                }
            })
            div2.append(div3)
            div.append(div4)
        })
        div2.find('div').each((index, it) => {
            $(it).on('click', () => {
                $(it).siblings().removeClass('active')
                $(it).addClass('active')
                div.find('div[class="emoji-list"]').each((ix, i) => {
                    if (index === ix) {
                        $(i).css({ display: 'flex' })
                    } else {
                        $(i).css({ display: 'none' })
                    }
                })
            })
        })
        $(div2.find('div')[0]).trigger('click')
        return div[0]
    }
}
//自定义编辑器插件,对获取html进行处理
const withGetHtml = <T extends IDomEditor>(editor: T): T => {
    // 获取当前 editor API
    const { getHtml } = editor
    const newEditor = editor
    // 重写 getHtml
    newEditor.getHtml = () => {
        if (getHtml() === '<p><br></p>') return ''
        return getHtml()
    }
    // 返回 newEditor ,重要!
    return newEditor
}
// 注册,要在创建编辑器之前注册,且只能注册一次,不可重复注册。
export default defineNuxtPlugin(async (nuxtApp) => {
    // 获取表情数据
    const emojiArray = await $fetch('/emoji-path')
    // 自定义表情菜单
    const emojiMenuConf = {
        key: 'emojiMenu',
        factory() {
            return new EmojiDropPanelMenu(emojiArray)
        }
    }
    //注册自定义插件,处理编辑器默认值为''时获取到的是为'<p><br></p>'
    Boot.registerPlugin(withGetHtml)
    //注册附件上传插件
    Boot.registerModule(attachmentModule)
    //注册自定义标签菜单
    Boot.registerMenu(emojiMenuConf)
})

编辑器设置

//工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {
     .......
    //插入自定义表情菜单配置
    insertKeys:  {
            // 自定义插入的位置
            index: 20,
            // 自定义表情
            keys: ['emojiMenu']
      },
   .......
}

好了,到此就结束了。

AnyFork avatar Feb 23 '24 09:02 AnyFork

这个方案插入图片作为表情的时候,图片和当前行文字没有垂直居中,是哪里的问题?

zhengnan0627 avatar Jul 30 '24 08:07 zhengnan0627

@zhengnan0627 作为普通文字时肯定没有垂直居中的,如果你只要一个大标题的话可以试试这个https://cycleccc.github.io/demo/like-qq-doc.html 标题样式独立出来自己维护,这样 wangeditor-next 就不会 norm 你的标题样式了 代码在这儿 https://github.com/end-cycle/wangEditor/blob/238c778f6c0c102933feff29b984f9b8e23fbe1e/packages/editor/demo/like-qq-doc.html#L78

cycleccc avatar Jul 30 '24 08:07 cycleccc