在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获取到最底一层的自定义元素,再判断它能不能执行命令。

相关推荐
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐4 小时前
前端图像处理(一)
前端