在vue3中实现富文本--利用defineCustomElement来快速实现富文本组件(三)

一、技术点引入

上篇文章主要介绍了如何实现将vue组件注册为自定义元素,以及在富文本使用自定义元素时遇到自定义元素不能直接插入富文本中的问题;这篇文章将继续这个话题,谈谈如何给这些元素加上一些删除按钮,其主要的核心思想为如何在元素挂载时给他们加上一些统一的元素、样式和js事件。

在上篇文章说过,defineCustomElement 方法它返回的是一个,这个类就是web component,我们可以尝试在定义后输出到控制台看一下:

js 复制代码
      const CustomElement = defineCustomElement(component)
      console.log(CustomElement);

输出的结果为:

好像是个叫 VueCustomElement的类,我们鼠标单击跳转一下,看到源代码为:

可以看到这个VueCustomElement的类继承自VueElement,并将我们传递的组件options定义为组建后挂载到def属性上,并将这个VueCustomElement这个类返回。 而VueCustomElement这个类是一个webcomponent,所以当我们想修改它时,我们只需要定义一个类继承他就好了:

js 复制代码
  const CustomElement = defineCustomElement(component)
  class InlineComponent extends CustomElement {
    constructor(initialProps) {
      super(initialProps)
    }
  }

这样我们就可以给所有的自定义元素统一的做一些操作了,这里以给所有自定义元素加上删除符号为例。

二、代码实现

有关webcomponent的生命周期函数我就不在此赘述了,有兴趣的话可以前往# 使用自定义元素下面详细查看生命周期,总之我们要了解的是每一个自定义元素的元素挂载完成和vue示例都要在connectedCallback这个webcomponent生命周期钩子中拿到,我们对dom元素的操作和对组件实例上的一些更改应该在这里进行。下面是buildComponent.ts文件的两个函数,他们分别负责给inline元素和block元素添加关闭按钮:

js 复制代码
export function buildInlineComponentClass(ParentClass: CustomElementConstructor) {
  class InlineComponent extends ParentClass {
    constructor(initialProps) {
      super(initialProps)
    }
    connectedCallback() {
      super.connectedCallback && super.connectedCallback()

      // 处理原生撤回时重复生成clone-btn的问题,要判断是否已经存在.v__close-btn'
      if (this.shadowRoot && !this.shadowRoot.querySelector('.v__close-btn')) {
        const closeBtn = document.createElement('a')
        closeBtn.style.marginLeft = '3px'
        closeBtn.innerText = '×'
        closeBtn.classList.add('v__close-btn')
        // 如果父级为container,则一起删除
        closeBtn.addEventListener('click', () => {
          const parent = this.parentElement
          if (parent && parent.getAttribute('data-element-container') !== null) {
            parent.parentElement?.removeChild(parent)
          } else {
            this.parentElement?.removeChild(this)
          }
        })
        this.shadowRoot.appendChild(closeBtn)
      }
    }
  }
  return InlineComponent
}

export function buildBlockComponentClass(ParentClass: CustomElementConstructor) {
  class BlockComponent extends ParentClass {
    constructor(initialProps) {
      super(initialProps)
    }
    connectedCallback() {
      super.connectedCallback && super.connectedCallback()

      if (this.shadowRoot && !this.shadowRoot.querySelector('.v__config-container')) {
        const configContainer = document.createElement('div')
        configContainer.classList.add('v__config-container')
        const closeBtn = document.createElement('a')
        closeBtn.textContent = '×'
        closeBtn.classList.add('close-btn')
        // 如果父级为container,则一起删除
        closeBtn.addEventListener('click', () => {
          const parent = this.parentElement
          if (parent && parent.getAttribute('data-element-container') !== null) {
            parent.parentElement?.removeChild(parent)
          } else {
            this.parentElement?.removeChild(this)
          }
        })
        configContainer.appendChild(closeBtn)
        this.shadowRoot.insertBefore(configContainer, this.shadowRoot.firstChild)
        const style = document.createElement('style')
        style.textContent = `
                .v__config-container {
                    width: 100%;
                    height: 24px;
                    background-color: black;
                    opacity: .5;
                    transition: .3s;
                }
                
                .close-btn{
                    cursor: pointer;
                    font-size: 18px;
                    color: white;
                    float: right;
                    line-height: 18px;
                    padding: 3px;
                    width: 24px;
                    box-sizing: border-box;
                    text-align: center;
                }
                `
        this.shadowRoot.appendChild(style)
      }
    }
  }
  return BlockComponent
}

可以看到,两个函数都在自定义元素的shadow dom加载完成后添加新的元素,给每个关闭按钮添加一个点击事件,点击后查看父元素是否设置了data-element-container,如果设置了代表父元素为自定义元素的container,要把父元素一起销毁,如果不是则销毁这个自定义元素。同时要注意添加这个自定义元素时要判断是不是已经存在这个按钮了,如果存在就不要重复添加了,这个是当删除这个元素在ctrl+z撤回时会遇到重复添加的问题。

同时,我们要回到RichText类,给两个自定义元素的container上打上标签data-element-container:

js 复制代码
  private insertInlineCustomElement(el: HTMLElement, range: Range) {
    const container = document.createElement('div')
    container.style.display = 'inline-block'
    container.setAttribute('contenteditable', 'false')
    el.setAttribute('containereditable', 'true')
    // container打上标签
    container.setAttribute('data-element-container','')
    container.appendChild(el)
    range.insertNode(container)
  }

这里以insertInlineCustomElement方法举例,insertBlockCustomElement也要做相同的操作。

最后,修改index.ts中的自定义元素注册方法,让他将这个函数返回的新的类进行注册和保存:

js 复制代码
const customComponents = {
  install() {
    const componentsName: CustomComponents = {}
    Components.forEach((component) => {
      const CustomElement = defineCustomElement(component)
      const type = component.type ?? 'inline'
      // 根据组件类型来获得添加完删除按钮的自定义元素
      const ComponentClass =
        type === 'inline'
          ? buildInlineComponentClass(CustomElement)
          : buildBlockComponentClass(CustomElement)

      customElements.define(component.name ?? component.__name, ComponentClass)
      const name = component.name ?? component.__name
      componentsName[name] = {
        name,
        type,
        nestable: component.nestable ?? false,
        Constructor: ComponentClass
      }
      if (component.props) {
        const props = {}
        Object.keys(component.props).forEach((prop) => {
          props[prop] = component.props[prop].name
        })
        componentsName[name].props = props
      }
    })
    const customComponents = customComponentsStore()
    customComponents.setComponents(componentsName)
  }
}

这样完整的流程就走完了,下面是CustomLink元素和CodeEditor元素现在的效果:

可以看到两种关闭按钮都很好的展示上去了,并且点击事件也是生效的。

三、总结

这篇文章主要以给自定义元素加上删除按钮为例,讲如何给自定义元素进行统一处理,主要采用继承vue的defineCustomElement定义后的类,然后在继承的类中进行统一处理,最后新建自定义元素时采用处理过后的类来使用。这里只展示了inline和block的类型,inline-block类型的自定义元素可以自行处理。

相关推荐
嘵奇11 分钟前
SpringBoot五:Web开发
java·前端·spring boot
年纪轻轻只想躺平11 分钟前
vue3响应式数据原理
前端·javascript·vue.js
drebander15 分钟前
Docker 与 Nginx:容器化 Web 服务器
前端·nginx·docker
咔咔库奇2 小时前
【react】基础教程
前端·react.js·前端框架
前端小王hs2 小时前
MySQL后端返回给前端的时间变了(时区问题)
前端·数据库·mysql
千篇不一律2 小时前
工作项目速刷手册
服务器·前端·数据库
阿丽塔~4 小时前
vue3 下载文件 responseType-blob 或者 a标签
前端·vue·excel
七灵微5 小时前
【前端】Axios & AJAX & Fetch
前端·javascript·ajax
究极无敌暴龙战神X5 小时前
一篇文章学懂Vuex
前端·javascript·vue.js
shaoin_25 小时前
Vue3中ref与reactive的区别
前端·vue.js