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

一、技术点引入

上一篇文章主要讲解如何给所有的自定义元素加上一些统一的样式,元素或者js处理,这一篇主要讲解如何处理富文本套娃的情况下处理各种事件。

我们先新写一个组件RText.ce.vue:

js 复制代码
<script setup lang="ts">
</script>

<template>
    <div contenteditable="true">这是RText.ce.vue</div>
</template>
<script lang="ts">
export default {
    name: 'r-text',
    type: 'block'
}
</script>

他新建后的样子为:

看上去仿佛一切都好,但是我们将光标放在富文本内部,再尝试修改文本颜色:

可以看到颜色并不会生效。

这是什么原因呢?我们回过头看看我们是怎么做的,回到第一篇文章,我们可以看到修改样式的流程为先获取用户选中的文本,再执行样式覆盖,问题多半出现在获取用户文本的函数上了,也就是getCursorRange方法出了问题。我们尝试在获取到selection了后打印一下range各个节点的位置:

js 复制代码
  // 获取当前光标位置
  private getCursorRange() {
    const sel = getSelection()
    if (sel && sel.rangeCount) {
      const range = sel.getRangeAt(0)
      console.log(range.commonAncestorContainer);
      console.log(range.startContainer);
      console.log(range.endContainer);

      // 只允许rootElement的子节点参与修改,不允许本身参与修改
      if (this.rootElement.contains(range.commonAncestorContainer)) return range
    }
    return null
  }

输出的结果为:

可以看到range中的commonAncestorContainer,startContainer,endContainer节点都无法捕获到r-text元素的内部 。这也是一个坑点,那就是浏览器将web component视为一个不可再分的原子元素,这就导致每次获取的range都没在自定义元素内部。

要解决这个问题,有以下几个要点:

  1. document.activeElement可以获取到当前聚集态的element,详情请看mdn文档DocumentOrShadowRoot.activeElement - Web API 接口参考 | MDN (mozilla.org)
  2. document.selectionchange事件可以监听到document的Selection改变,详情请看Document:selectionchange 事件 - Web API 接口参考 | MDN (mozilla.org)
  3. 以上两点相结合,我们可以在每当selection变化时就获取一次document.activeElement,在richText类中用focusEl属性保存当前光标聚焦的元素,我们先在initRootNode方法中试试看能不能获取到正在编辑的自定义元素:
js 复制代码
  private initRootNode(parent: HTMLElement, mode: 'edit' | 'show' = 'edit') {
    this.rootElement = document.createElement('div')
    this.rootElement.contentEditable = mode === 'edit' ? 'true' : 'false'
    parent.appendChild(this.rootElement)
    document.addEventListener('selectionchange', () => {
      // 获取当前聚焦元素
      let activeEl = document.activeElement
      console.log(activeEl);
      
      this.focusEl = activeEl as HTMLElement
    })
  }

然后我们新建富文本元素试试:

可以看到能够很好的聚焦到我们的自定义元素上。那么事情就简单了,在selectionchange事件中获取当前聚焦的元素,然后如果是自定义元素的话则需要不断地往里面找最里面一层自定义元素,然后将focusEl属性设置为到的最内部的自定义元素。

二、代码实现

这是修改后的完整代码:

js 复制代码
  private initRootNode(parent: HTMLElement, mode: 'edit' | 'show' = 'edit') {
    this.rootElement = document.createElement('div')
    this.rootElement.contentEditable = mode === 'edit' ? 'true' : 'false'
    parent.appendChild(this.rootElement)
    document.addEventListener('selectionchange', () => {
      // 获取当前聚焦元素
      let activeEl = document.activeElement
      // shadow dom中的聚焦元素需要获取
      //不断地通过shadowroot.activeElement与自定义元素列表比对,观察是否焦点还在内部
      while (activeEl?.shadowRoot && getCustomComponents()[activeEl.nodeName.toLocaleLowerCase()]) {
        const childActiveEl = activeEl.shadowRoot.activeElement
        // 只有当childActiveEl存在且聚焦的为自定义元素才继续找,不然就锁定到这一层就好了,方便获取selection
        if (childActiveEl && getCustomComponents()[childActiveEl.nodeName.toLocaleLowerCase()]) {
          activeEl = childActiveEl
        } else {
          break
        }
      }
      this.focusEl = activeEl as HTMLElement
    })
  }

  // 获取当前光标位置
  private getCursorRange() {
    // 只有自定义元素需要特殊处理
    if (this.focusEl && getCustomComponents()[this.focusEl.nodeName.toLocaleLowerCase()]) {
      // 如果不支持嵌套增加组件,则直接返回
      if (!getCustomComponents()[this.focusEl.nodeName.toLocaleLowerCase()].nestable) {
        alert('当前组件内部不支持嵌套其他组件或进行操作!')
        return null
      }
      // 进入自定义元素的shadowdom了,还不支持嵌套shadow dom处理
      const shadowroot = this.focusEl.shadowRoot
      const selection = shadowroot?.getSelection()
      if (selection.rangeCount) {
        const range = selection.getRangeAt(0)
        return range
      }
    } else {
      const sel = getSelection()
      if (sel && sel.rangeCount) {
        const range = sel.getRangeAt(0)
        // 只允许rootElement的子节点参与修改,不允许本身参与修改
        if (this.rootElement.contains(range.commonAncestorContainer)) return range
      }
    }
    return null
  }

其中我们主要修改了initRootNode 方法和getCursorRange 方法,其中initRootNode 方法主要监听了selectionchange事件,每当有所改变则重新获取一次最里层的自定义元素,如果没有shadowroot则代表他是一个普通元素,则可以直接返回,然后将他赋值给this.focuseEl;getCursorRange方法则根据this.focusEl来判断是不是自定义元素,如果是的话还要根据nestable判断他支不支持嵌套组件,比如像CustonLink和CodeEditor就不太适合往里面新增组件和改变样式,支持的自定义元素必须要显式的声明nestable为true,拿RText.ce.vue举例:

js 复制代码
<script setup lang="ts">
</script>

<template>
    <div contenteditable="true">这是RText.ce.vue</div>
</template>
<script lang="ts">
export default {
    name: 'r-text',
    type: 'block',
    // 在这里显示声明
    nestable: true,
}
</script>

展示一下效果:

可以看到此时r-text元素能够支持套娃式嵌套组件了

三、总结

本文主要讲了当getSelection遇到自定义元素时会将自定义元素视为原子元素而导致range选中元素和我们的期望元素不一致,我们主要采用selectionchange事件结合activeElement获取到最底一层的自定义元素,再判断它能不能执行命令。

相关推荐
Sun_light8 分钟前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js
爱学习的茄子11 分钟前
深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南
前端·javascript·面试
莫空000011 分钟前
Vue组件通信方式详解
前端·面试
呆呆的心11 分钟前
揭秘 CSS 伪元素:不用加标签也能玩转出花的界面技巧 ✨
前端·css·html
susnm16 分钟前
Dioxus 与数据库协作
前端·rust
优雅永不过时_v20 分钟前
基于vite适用于 vue和 react 的Three.js低代码与Ai结合编辑器
前端·javascript
小皮侠22 分钟前
nginx的使用
java·运维·服务器·前端·git·nginx·github
WildBlue24 分钟前
🧊 HTML5 王者对象 Blob - 二进制世界的魔法沙漏
前端·javascript·html
啷咯哩咯啷28 分钟前
Vue3构建低代码表单设计器
前端·javascript·vue.js
用户261245834016129 分钟前
vue学习路线(10.监视属性-watch)
前端·vue.js