在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类型的自定义元素可以自行处理。

相关推荐
Martin -Tang1 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发1 小时前
解锁微前端的优秀库
前端
王解2 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁2 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂2 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐3 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成5 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽5 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新6 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_7 小时前
【Linux】多线程(概念,控制)
linux·运维·前端