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

如何定义Quill内容布局?如何实现Quill编辑器内嵌套编辑器?

关于Quill编辑器布局定义与嵌套实现的解决方案

我来帮你搞定这两个Quill的问题,尤其是你提到的嵌套编辑器场景——之前踩过多实例的坑,太懂那种事件冲突、历史记录乱套的痛苦了,直接给你最稳妥的基于Blot系统的方案:

一、如何为Quill内容定义布局?

Quill的核心是Blot系统,自定义布局本质就是通过自定义Blot给内容块加上布局类,再用CSS控制样式。举个简单的flex布局例子:

首先注册一个自定义Block Blot:

const Block = Quill.import('blots/block');

class FlexLayoutBlock extends Block {
  static create(value) {
    const node = super.create(value);
    // 给Blot加布局类,也可以根据value动态传不同类名
    node.classList.add('flex-row-layout');
    return node;
  }
}

FlexLayoutBlock.blotName = 'flex-layout';
FlexLayoutBlock.tagName = 'div';

Quill.register(FlexLayoutBlock, true);

然后写CSS实现布局:

.flex-row-layout {
  display: flex;
  gap: 1.2rem;
  align-items: center;
  margin: 0.8rem 0;
}

之后你就可以通过quill.insertEmbed(index, 'flex-layout', {})插入带flex布局的内容块,里面的子元素会自动遵循flex规则排列。如果需要更复杂的布局(比如grid),只需要修改CSS类即可。

二、如何实现你描述的嵌套编辑器场景?

你的需求是:主文本中插入一个“媒体+可编辑文本”的复合块,嵌套文本复用主工具栏但禁用嵌入,删除复合块时要把嵌套文本合并到主编辑器。绝对不要用多个Quill实例,这会导致事件冲突、历史记录分裂等一堆bug,正确姿势是做一个复合自定义Blot。

步骤1:创建复合Blot组件

我们需要三个Blot:不可编辑的媒体容器、可编辑的嵌套文本容器、把两者包起来的复合容器。

const Container = Quill.import('blots/container');
const Block = Quill.import('blots/block');

// 1. 不可编辑的媒体容器
class MediaContainer extends Block {
  static create(value) {
    const node = super.create(value);
    node.contentEditable = 'false';
    node.classList.add('media-box');
    // 插入媒体元素(这里以图片为例,视频同理)
    const media = document.createElement('img');
    media.src = value.src;
    media.alt = '嵌入媒体';
    node.appendChild(media);
    return node;
  }
}
MediaContainer.blotName = 'media-container';
MediaContainer.tagName = 'div';

// 2. 嵌套文本容器:限制可用格式,禁用嵌入
class NestedTextContainer extends Container {
  static allowedChildren() {
    // 只保留常规文本格式Blot,排除嵌入类(比如image、custom-embed)
    const defaultAllowed = super.allowedChildren();
    return defaultAllowed.filter(blot => 
      blot.blotName !== 'custom-embed' && blot.blotName !== 'image'
    );
  }
}
NestedTextContainer.blotName = 'nested-text';
NestedTextContainer.tagName = 'div';
NestedTextContainer.className = 'nested-text-box';

// 3. 复合容器:把媒体和嵌套文本组合起来,处理删除逻辑
class CustomEmbed extends Container {
  static create(value) {
    const node = super.create(value);
    node.classList.add('custom-embed-wrapper');
    // 添加媒体容器
    const mediaBlot = Quill.find(this.scroll.create('media-container', { src: value.mediaSrc }));
    node.appendChild(mediaBlot.domNode);
    // 添加嵌套文本容器,默认插入空段落
    const textBlot = Quill.find(this.scroll.create('nested-text'));
    const emptyPara = Quill.find(this.scroll.create('block'));
    textBlot.appendChild(emptyPara);
    node.appendChild(textBlot.domNode);
    return node;
  }

  // 当复合Blot被删除时,把嵌套文本移到主编辑器
  remove() {
    const nestedTextBlot = this.children.head.next; // 第二个子元素是文本容器
    if (nestedTextBlot) {
      // 遍历嵌套文本里的所有内容,逐个插入到当前Blot的位置
      let child = nestedTextBlot.children.head;
      while (child) {
        const nextChild = child.next;
        this.parent.insertBefore(child, this);
        child = nextChild;
      }
    }
    super.remove();
  }
}
CustomEmbed.blotName = 'custom-embed';
CustomEmbed.tagName = 'div';
CustomEmbed.className = 'custom-embed';
CustomEmbed.allowedChildren = [MediaContainer, NestedTextContainer];

// 注册所有自定义Blot
Quill.register({
  'blots/media-container': MediaContainer,
  'blots/nested-text': NestedTextContainer,
  'blots/custom-embed': CustomEmbed
}, true);

步骤2:CSS实现媒体+文本的并排布局

.custom-embed-wrapper {
  display: flex;
  gap: 1.5rem;
  margin: 1rem 0;
  padding: 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
}

.media-box {
  flex-shrink: 0;
  width: 220px;
}

.media-box img {
  width: 100%;
  height: auto;
  border-radius: 4px;
}

.nested-text-box {
  flex: 1;
  min-width: 0; /* 防止flex容器溢出 */
}

步骤3:工具栏动态控制(禁用嵌套区域的嵌入功能)

当用户聚焦到嵌套文本时,禁用工具栏的嵌入按钮,聚焦回主文本时恢复:

const quill = new Quill('#editor', {
  modules: { toolbar: '#toolbar' },
  theme: 'snow'
});

quill.on('selection-change', (range) => {
  if (!range) return;
  // 检查当前选中的内容是否在嵌套文本容器内
  let currentBlot = quill.getLine(range.index)[0];
  let isInNestedText = false;
  while (currentBlot) {
    if (currentBlot.statics.blotName === 'nested-text') {
      isInNestedText = true;
      break;
    }
    currentBlot = currentBlot.parent;
  }
  // 控制工具栏的嵌入按钮(假设按钮类名是.ql-image)
  const embedBtn = document.querySelector('.ql-image');
  if (isInNestedText) {
    embedBtn.disabled = true;
    embedBtn.classList.add('ql-disabled');
  } else {
    embedBtn.disabled = false;
    embedBtn.classList.remove('ql-disabled');
  }
});

步骤4:添加插入复合Blot的入口

给工具栏加个自定义按钮,点击插入复合块:

document.getElementById('insert-custom-embed').addEventListener('click', () => {
  const mediaSrc = prompt('请输入媒体URL:'); // 实际项目可以换成文件上传组件
  if (mediaSrc) {
    const insertIndex = quill.getSelection()?.index || quill.getLength();
    quill.insertEmbed(insertIndex, 'custom-embed', { mediaSrc });
    // 插入后光标移到复合块后面
    quill.setSelection(insertIndex + 1);
  }
});

为什么不推荐多实例方案?

你之前踩的坑都是多实例的通病:

  • 事件冒泡导致主编辑器接收嵌套实例的按键、选择事件
  • 编辑历史(undo/redo)各自独立,用户操作起来混乱
  • Delta数据无法同步,导出内容时还要手动合并多个实例的Delta,非常麻烦

而基于Blot的方案完全复用主Quill实例的所有功能,历史记录、Delta都是统一的,只需要通过Blot的规则限制格式,稳定性和可维护性高太多。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:05:27