You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

VueJS集成Quill编辑器删除文本时的异常问题求助

Vue3集成Quill编辑器的删除异常问题

删除2-3个以上字符后光标跳转到第一行,控制台偶尔抛出TypeError: leaf.position is not a function错误,排查发现问题出在setContents或setText调用环节。以下是我的代码:

<template>
  <div class="app-text-editor">
    <div id="toolbar">
      <div class="ql-formats">
        <select class="ql-size">
          <option value="small" />
          <option selected />
          <option value="large" />
          <option value="huge" />
        </select>
      </div>
      <div class="ql-formats">
        <select class="ql-color">
          <option selected />
          <option value="red" />
          <option value="orange" />
          <option value="yellow" />
          <option value="green" />
          <option value="blue" />
          <option value="purple" />
        </select>
        <select class="ql-background">
          <option selected />
          <option value="red" />
          <option value="orange" />
          <option value="yellow" />
          <option value="green" />
          <option value="blue" />
          <option value="purple" />
        </select>
      </div>
      <div class="ql-formats">
        <button class="ql-bold" />
        <button class="ql-italic" />
        <button class="ql-underline" />
        <button class="ql-strike" />
      </div>
      <div class="ql-formats">
        <button class="ql-list" value="ordered" />
        <button class="ql-list" value="bullet" />
        <select class="ql-align">
          <option selected />
          <option value="center" />
          <option value="right" />
          <option value="justify" />
        </select>
      </div>
    </div>
    <div id="editor" />
  </div>
</template>

<script setup>
  import { onMounted, ref } from 'vue'
  import { useI18n } from 'vue-i18n'
  import Quill from 'quill'
  import 'quill/dist/quill.core.css'
  import 'quill/dist/quill.snow.css'

  const props = defineProps({
    modelValue: {
      type: String,
      default: null
    }
  })

  const emits = defineEmits(['update:modelValue'])

  const i18n = useI18n()
  const quill = ref(null)

  onMounted(() => {
    var options = {
      modules: {
        toolbar: '#toolbar'
      },
      theme: 'snow',
      placeholder: i18n.t('app-text-editor.type-text')
    }
    quill.value = new Quill('#editor', options)
    setText()
    watchText()
  })

  const watchText = () => quill.value.on('text-change', () => emits('update:modelValue', quill.value?.root?.innerHTML))

  const setText = () => {
    const text = props.modelValue
    const delta = quill.value.clipboard.convert(text)

    quill.value.setContents(delta, 'silent')
  }
</script>

<style lang="scss" scoped>
  .app-text-editor {
    height: 100%;
    display: flex;
    flex-direction: column;
    #toolbar,
    #editor {
      background-color: var(--color-light-grey);
      border: none;
    }
    #toolbar {
      border-radius: 1.25rem 1.25rem 0 0;
    }
    #editor {
      border-radius: 0 0 1.25rem 1.25rem;
    }
  }
</style>

问题根源及修复方案

核心问题

  1. 双向绑定循环触发:text-change事件直接触发modelValue更新,若父组件回传内容未做判断,会再次调用setText重写编辑器内容,导致内部状态混乱,光标跳转。
  2. clipboard.convert误用:该方法用于处理剪贴板内容,直接转换HTML字符串会生成不符合Quill Delta规范的结构,引发内部方法调用错误。
  3. 缺少modelValue变化监听:仅在初始化时设置内容,父组件更新modelValue时编辑器无法同步,同时未阻断自触发更新循环。

修改后的完整代码

<template>
  <div class="app-text-editor">
    <div id="toolbar">
      <div class="ql-formats">
        <select class="ql-size">
          <option value="small" />
          <option selected />
          <option value="large" />
          <option value="huge" />
        </select>
      </div>
      <div class="ql-formats">
        <select class="ql-color">
          <option selected />
          <option value="red" />
          <option value="orange" />
          <option value="yellow" />
          <option value="green" />
          <option value="blue" />
          <option value="purple" />
        </select>
        <select class="ql-background">
          <option selected />
          <option value="red" />
          <option value="orange" />
          <option value="yellow" />
          <option value="green" />
          <option value="blue" />
          <option value="purple" />
        </select>
      </div>
      <div class="ql-formats">
        <button class="ql-bold" />
        <button class="ql-italic" />
        <button class="ql-underline" />
        <button class="ql-strike" />
      </div>
      <div class="ql-formats">
        <button class="ql-list" value="ordered" />
        <button class="ql-list" value="bullet" />
        <select class="ql-align">
          <option selected />
          <option value="center" />
          <option value="right" />
          <option value="justify" />
        </select>
      </div>
    </div>
    <div id="editor" />
  </div>
</template>

<script setup>
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Quill from 'quill'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'

const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})

const emits = defineEmits(['update:modelValue'])

const i18n = useI18n()
const quill = ref(null)
let isUpdating = false // 标记内部更新,阻断循环

onMounted(() => {
  const options = {
    modules: {
      toolbar: '#toolbar'
    },
    theme: 'snow',
    placeholder: i18n.t('app-text-editor.type-text')
  }
  quill.value = new Quill('#editor', options)

  // 初始化内容
  if (props.modelValue) {
    quill.value.root.innerHTML = props.modelValue
  }

  // 监听编辑器内容变化
  quill.value.on('text-change', () => {
    if (!isUpdating) {
      const html = quill.value.root.innerHTML
      emits('update:modelValue', html)
    }
  })
})

// 监听父组件传入的modelValue变化,同步到编辑器
watch(() => props.modelValue, (newVal) => {
  if (quill.value && newVal !== quill.value.root.innerHTML) {
    isUpdating = true
    // 直接设置innerHTML更适配HTML内容同步,避免clipboard.convert的兼容性问题
    quill.value.root.innerHTML = newVal
    isUpdating = false
  }
})
</script>

<style lang="scss" scoped>
.app-text-editor {
  height: 100%;
  display: flex;
  flex-direction: column;
  #toolbar,
  #editor {
    background-color: var(--color-light-grey);
    border: none;
  }
  #toolbar {
    border-radius: 1.25rem 1.25rem 0 0;
  }
  #editor {
    border-radius: 0 0 1.25rem 1.25rem;
  }
}
</style>

关键修改说明

  • 增加isUpdating标记,阻断编辑器自身触发的modelValue更新引发的重绘,避免光标异常跳转。
  • 替换clipboard.convert为直接设置root.innerHTML,更适配HTML内容的同步,避免Delta结构不兼容问题。
  • 添加watch监听modelValue变化,实现父组件到编辑器的双向同步,同时对比内容避免无效更新。
  • 初始化内容时直接赋值innerHTML,减少setContents带来的内部状态冲突。

内容的提问来源于stack exchange,提问作者Charles

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 11:14:57