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

如何在Quill中解析Delta并替换内容,同时保留选区与光标位置?

在Quill中实现表情与链接自动替换并保留选区

Quill确实基于Delta作为核心数据模型,并且支持Operational Transformation(OT)来处理协同编辑,你的理解是对的。要实现无工具栏的自动替换,核心是监听文本变化事件,基于Delta做内容替换,同时妥善处理光标/选区位置,具体步骤如下:

1. 监听用户输入事件

只响应用户主动输入的内容变化,避免程序自身修改触发循环:

quill.on('text-change', (delta, oldDelta, source) => {
  if (source !== 'user') return; // 忽略非用户触发的修改
});

2. 保存当前选区位置

在处理替换前,先记录光标或选区的位置,后续用于恢复:

const range = quill.getSelection();
if (!range) return; // 编辑器失焦时跳过处理

3. 生成替换用的Delta操作

表情替换

遍历表情映射,从后往前查找匹配的触发文本,生成对应的删除+插入操作(从后往前处理可避免索引偏移):

const emojiMap = {
  '<3': '❤️',
  ':(': '😢',
  ':)': '😊'
};
const text = quill.getText();
const ops = [];

Object.entries(emojiMap).forEach(([trigger, emoji]) => {
  let index = text.lastIndexOf(trigger);
  while (index !== -1) {
    // 先保留前面的内容,再删除触发文本,插入表情
    ops.push({ retain: index });
    ops.push({ delete: trigger.length });
    ops.push({ insert: emoji });
    index = text.lastIndexOf(trigger, index - 1);
  }
});

链接替换

借助Autolinker识别文本中的链接,生成带link属性的插入操作:

Autolinker.link(text, {
  replaceFn: (match) => {
    if (match.getType() === 'url') {
      const start = match.getOffset();
      const length = match.getLength();
      const url = match.getUrl();
      // 同样从后往前添加操作
      ops.push({ retain: start });
      ops.push({ delete: length });
      ops.push({
        insert: match.getOriginalText(),
        attributes: { link: url }
      });
    }
    return true;
  }
});

4. 应用Delta并恢复选区

将生成的操作反转(因为是从后往前收集的,需转为从前往后执行),静默更新内容后调整并恢复选区:

if (ops.length > 0) {
  ops.reverse();
  const replacementDelta = new Quill.Constructor.Delta(ops);
  // 用silent模式避免再次触发text-change
  quill.updateContents(replacementDelta, 'silent');

  // 计算替换导致的光标偏移:表情替换会改变文本长度
  let offsetAdjustment = 0;
  Object.entries(emojiMap).forEach(([trigger, emoji]) => {
    const matchCount = (text.match(new RegExp(trigger, 'g')) || []).length;
    offsetAdjustment += matchCount * (emoji.length - trigger.length);
  });

  // 恢复并调整选区位置
  quill.setSelection({
    index: range.index + offsetAdjustment,
    length: range.length
  });
}

完整初始化示例

// 初始化Quill,禁用工具栏
const quill = new Quill('#editor', {
  theme: 'snow',
  modules: { toolbar: false }
});

// 执行上述替换逻辑的完整代码
const emojiMap = {
  '<3': '❤️',
  ':(': '😢',
  ':)': '😊'
};

quill.on('text-change', (delta, oldDelta, source) => {
  if (source !== 'user') return;

  const range = quill.getSelection();
  if (!range) return;

  const text = quill.getText();
  const ops = [];

  // 处理表情替换
  Object.entries(emojiMap).forEach(([trigger, emoji]) => {
    let index = text.lastIndexOf(trigger);
    while (index !== -1) {
      ops.push({ retain: index });
      ops.push({ delete: trigger.length });
      ops.push({ insert: emoji });
      index = text.lastIndexOf(trigger, index - 1);
    }
  });

  // 处理链接替换
  Autolinker.link(text, {
    replaceFn: (match) => {
      if (match.getType() === 'url') {
        const start = match.getOffset();
        const length = match.getLength();
        const url = match.getUrl();
        ops.push({ retain: start });
        ops.push({ delete: length });
        ops.push({
          insert: match.getOriginalText(),
          attributes: { link: url }
        });
      }
      return true;
    }
  });

  // 应用替换并恢复选区
  if (ops.length > 0) {
    ops.reverse();
    const replacementDelta = new Quill.Constructor.Delta(ops);
    quill.updateContents(replacementDelta, 'silent');

    let offsetAdjustment = 0;
    Object.entries(emojiMap).forEach(([trigger, emoji]) => {
      const matchCount = (text.match(new RegExp(trigger, 'g')) || []).length;
      offsetAdjustment += matchCount * (emoji.length - trigger.length);
    });

    quill.setSelection({
      index: range.index + offsetAdjustment,
      length: range.length
    });
  }
});

关键注意事项

  • 必须用Delta操作修改内容,不能直接操作innerHTML,否则会破坏Quill的内部模型一致性
  • 用silent模式更新内容,防止修改触发新一轮text-change事件,造成循环
  • 从后往前处理替换,避免前面的修改导致后续索引偏移
  • 表情替换会改变文本长度,需要计算偏移量调整选区;链接替换文本长度不变,无需额外调整

内容的提问来源于stack exchange,提问作者Benoît Lahoz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 02:03:22