如何定义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
相关产品推荐
相关产品推荐

