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

Vue3项目中Quill编辑器offset初始化错误修复方案

Vue3集成Quill编辑器修改笔记时offset空值错误的修复

问题描述

创建笔记功能正常,但进入修改笔记页面后,点击或输入Quill编辑器时会触发如下空值错误:

quill.js?v=870ff25b:12570 Uncaught TypeError: Cannot read properties of null (reading 'offset')
    at quill.js?v=870ff25b:12570:26
    at Array.map (<anonymous>)
    at Selection.normalizedToRange (quill.js?v=870ff25b:12567:31)
    at Selection.getRange (quill.js?v=870ff25b:12556:25)
    at Selection.update (quill.js?v=870ff25b:12691:43)
    at quill.js?v=870ff25b:12452:12
    at quill.js?v=870ff25b:12338:9
    at Array.forEach (<anonymous>)
    at Emitter.handleDOM (quill.js?v=870ff25b:12332:43)
    at quill.js?v=870ff25b:12310:23

当前实现代码

<template>
  <section id="view-note-page" class="container mt-4">
    <div class="note-header-container mb-3 d-flex justify-content-between align-items-center">
      <div class="note-title-container">
        <label for="note-title" class="note-title-label">Title:</label>
        <input
            id="note-title"
            type="text"
            v-model="noteData.title"
            class="note-title-input"
            placeholder="Enter note title"
        />
      </div>
      <button class="btn btn-primary btn-save-changes" @click="saveNote">Save Changes</button>
    </div>

    <div id="post-editor" class="mb-3 flex-grow-1">
      <div id="quill-editor"></div>
    </div>
  </section>
</template>

<script>
import { nextTick } from "vue";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import { noteService } from "@/services/note.service";

export default {
  name: "ViewNotePage",
  props: {
    noteId: {
      type: [String, Number],
      required: true,
    },
  },
  data() {
    return {
      noteData: {
        title: "",
        content: "",
      },
      quillEditor: null,
    };
  },
  methods: {
    async fetchNote() {
      try {
        const note = await noteService.fetchNoteFromId(this.noteId);
        this.noteData = note;
        await this.updateQuillContent(note.content);
      } catch (error) {
        console.error("Failed to fetch the note:", error);
        toast.error("Failed to fetch the note.", { autoClose: 2000 });
      }
    },
    async initializeQuill() {
      try {
        await nextTick();
        const editorElement = document.getElementById("quill-editor");

        if (editorElement) {
          this.quillEditor = new Quill(editorElement, {
            theme: "snow",
            modules: {
              toolbar: [
                ["bold", "italic", "underline"],
                [{list: "ordered"}, {list: "bullet"}],
                [{header: [1, 2, 3, false]}],
                ["link"],
                ["clean"],
              ],
            },
          });

          this.quillEditor.on("text-change", () => {
            this.noteData.content = JSON.stringify(this.quillEditor.getContents());
          });
        }
      } catch (error) {
        console.error("Failed to initialize Quill editor:", error);
      }
    },
    async updateQuillContent(content) {
      try {
        if (this.quillEditor) {
          const delta = content ? JSON.parse(content) : {ops: []};
          this.quillEditor.setContents(delta);
        }
      } catch (error) {
        console.error("Failed to update Quill content:", error);
      }
    },
    async saveNote() {
      try {
        if (!this.noteData.title || !this.noteData.content) {
          toast.error("Please provide a title and content for the note.", {autoClose: 2000});
          return;
        }

        await noteService.updateNote(this.noteId, this.noteData);
        toast.success("Note updated successfully!", {autoClose: 2000});
        this.$emit("update", this.noteData);
      } catch (error) {
        console.error("Failed to save the note:", error);
        toast.error("Failed to update the note.", {autoClose: 2000});
      }
    },
  },
  async mounted() {
    try {
      await this.initializeQuill();
      await this.fetchNote();
    } catch (error) {
      console.error("Error during component setup:", error);
    }
  },
  watch: {
    noteId: {
      immediate: true,
      handler(newNoteId) {
        this.fetchNote();
      },
    },
  },
};
</script>

已尝试无效方案

曾尝试将Quill实例声明为组件外变量的方案,但问题未解决:

// ✅  works with quill2
let quill: Quill | null = null

onMounted(() => {
   quill = new Quill(...)
})

有效修复方法

修复思路

该错误核心原因是:noteId变化时直接更新编辑器内容,导致旧Quill实例与当前DOM绑定冲突,或者编辑器选区状态异常。同时异步初始化顺序也可能引发内容更新时机不匹配问题。

具体修复步骤

  1. 监听noteId变化时,销毁旧实例再重新初始化
    修改noteId的watch处理器,先清除旧的Quill实例和DOM残留,再重新初始化编辑器并加载新内容:

    watch: {
      noteId: {
        immediate: true,
        async handler(newNoteId) {
          // 销毁旧编辑器实例
          if (this.quillEditor) {
            this.quillEditor = null;
            const editorElement = document.getElementById("quill-editor");
            if (editorElement) {
              editorElement.innerHTML = '';
            }
          }
          // 重新初始化编辑器并加载新笔记
          await this.initializeQuill();
          await this.fetchNote();
        },
      },
    },
    
  2. 设置内容后手动重置编辑器选区
    在updateQuillContent方法中,添加选区重置逻辑,避免Quill自动计算选区时出现空值:

    async updateQuillContent(content) {
      try {
        if (this.quillEditor) {
          const delta = content ? JSON.parse(content) : {ops: []};
          this.quillEditor.setContents(delta);
          // 将光标定位到内容末尾,重置选区状态
          this.quillEditor.setSelection(this.quillEditor.getLength() - 1, 0);
        }
      } catch (error) {
        console.error("Failed to update Quill content:", error);
      }
    },
    
  3. 初始化编辑器前清空DOM元素
    优化initializeQuill方法,确保每次初始化时编辑器DOM元素是干净的:

    async initializeQuill() {
      try {
        await nextTick();
        const editorElement = document.getElementById("quill-editor");
        if (editorElement) {
          // 清空DOM残留内容
          editorElement.innerHTML = '';
          this.quillEditor = new Quill(editorElement, {
            theme: "snow",
            modules: {
              toolbar: [
                ["bold", "italic", "underline"],
                [{list: "ordered"}, {list: "bullet"}],
                [{header: [1, 2, 3, false]}],
                ["link"],
                ["clean"],
              ],
            },
          });
    
          this.quillEditor.on("text-change", () => {
            this.noteData.content = JSON.stringify(this.quillEditor.getContents());
          });
        }
      } catch (error) {
        console.error("Failed to initialize Quill editor:", error);
      }
    },
    

修复原理

  • 销毁旧实例并重新初始化:解决noteId切换时,旧实例与当前DOM绑定冲突导致的选区计算异常。
  • 手动重置选区:避免Quill自动计算选区时,因内容更新引发的空DOM节点引用。
  • 清空DOM元素:确保每次初始化编辑器时,没有残留的旧内容或编辑器生成的DOM结构干扰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 05:24:52