前言
这篇文章更适合对quill.js已经有一定了解的读者,如果您是正打算调研/学习quill.js,可以通过官方文档或我之前的一篇文章《深入理解quilljs》对Quill编辑器有一定了解后,再来阅读这篇文章。
为了更清晰的介绍Container的用法,我选择使用个人基于quill.js实现的表格模块 quill-better-table 作为示例来讲解,因为quill.js官方内置的模块都相对简单,难以覆盖各个方面。
quill-better-table项目地址:github.com/soccerloway…
Container类
Parchemnt是quill.js用于描述/管理编辑器内容结构及对应DOM树的底层依赖, quill.js中内置的Blot类均继承自Parchment的基础类,这些基础类继承自Parchemnt的抽象类。关于Container类的继承关系如下:
其中,ShadowBlot是所有blot(Inline、Block、Embed、Container等)的父类。实际上,在Quill/blots的源码中,Container类(quill.js)仅仅是继承了Parchment中的Container类,没有任何逻辑代码。故我们真正需要了解的是Parchment中的Container类。
如何定义嵌套结构及原理
接下来,我们来看看表格模块中单元格行(TableCellLine)、单元格(tableCell)的定义(为避免代码过多,影响阅读体验,format及表格业务逻辑相关的代码会省略掉,完整代码可到这里查看):
// TableCellLine
class TableCellLine extends Block { ...... // create/formats/optimize等}
TableCellLine.blotName = "table-cell-line"
TableCellLine.ClassName = "qlbt-cell-line"
TableCellLine.tagName = "DIV"
// TableCell
class TableCell extends Container {
......
checkMerge() {
if (super.checkMerge() && this.next.children.head != null) {
const thisHead = this.children.head.formats()["table-cell-line"]
const thisTail = this.children.tail.formats()["table-cell-line"]
const nextHead = this.next.children.head.formats()["table-cell-line"]
const nextTail = this.next.children.tail.formats()["table-cell-line"]
return (
thisHead.cell === thisTail.cell &&
thisHead.cell === nextHead.cell &&
thisHead.cell === nextTail.cell
)
}
return false
}
......
}
TableCell.blotName = "table"
TableCell.tagName = "TD"
TableCell.allowedChildren = [TableCellLine]
TableCellLine.requiredContainer = TableCell
经过上面的定义,最终生成的HTML结构如下:
<td>
<div class="qlbt-cell-line"></div>
<div class="qlbt-cell-line"></div>
</td>
实际上,控制嵌套结构关系的重点就是:
- 定义容器Blot类,且实现
checkMerge
接口方法; - 将被包裹Blot类的
requiredContainer
设置为容器Blot类。
Blot.requiredContainer
定义该Blot需要被哪一个容器Blot包裹。当该Blot被创建完成后,会执行到ShadowBlot类中的optimize
方法,其主要逻辑就是:检查该Blot的requiredContainer
是否被设置,并且该Blot的父Blot不是requiredContainer
设置的Blot类的实例,调用wrap
方法(wrap的作用就是创建容器Blot实例,插入的该Blot的父级中,然后将该Blot插入到容器中)。
以最终生成前面的HTML结构为例,经过wrap的过程后,HTML结构的变化:
checkMerge
该方法就是用于检查是否需要将这个容器Blot实例和它的下一个兄弟Blot实例合并为同一个容器Blot实例。返回值为true
则合并。checkMerge
在Container类的optimize
方法中调用,紧接shadowBlot.optimize
过程。当checkMerge
返回值为true
时,下一个兄弟Blot实例的children
会被插入到这个容器实例中,得到最终HTML结构。
<td>
<div class="qlbt-cell-line"></div>
<div class="qlbt-cell-line"></div>
</td>
利用checkMerge阻止并列(兄弟关系)的Blot合并
通过上面的定义,我们已经能够得到一个支持多行的表格单元格结构了。但在实际的表格中,同一表格行中有若干个单元格,结构如下:
<tr>
<td>
<div class="qlbt-cell-line">1</div>
</td>
<td>
<div class="qlbt-cell-line">2</div>
</td>
<td>
<div class="qlbt-cell-line">3</div>
</td>
</tr>
如何让checkMerge合适的时候返回false,阻止单元格被合并呢?让我们来看看quill-better-table中,TableCell类的checkMerge
。
checkMerge() {
if (super.checkMerge() && this.next.children.head != null) {
const thisHead = this.children.head.formats()["table-cell-line"]
const thisTail = this.children.tail.formats()["table-cell-line"]
const nextHead = this.next.children.head.formats()["table-cell-line"]
const nextTail = this.next.children.tail.formats()["table-cell-line"]
return (
thisHead.cell === thisTail.cell &&
thisHead.cell === nextHead.cell &&
thisHead.cell === nextTail.cell
)
}
return false
}
示例代码中,checkMerge
方法主要是通过检查当前tableCell实例和下一个tableCell实例的children
的formats中属性cell是否相等,相等则合并两个单元格的内容,否则不合并。
实际上,quill-better-table在定义TableCellLine
的时候,为它定义了表示单元格和行的唯一标识符,设置到domNode的属性上了,且通过formats方法能够得到这些信息,TableCellLine的DOM结构为:
<div class="qlbt-cell-line" data-row="row-xaes" data-cell="cell-hsop">
checkMerge
方法既是通过这个唯一标识符,也是data-cell
的值来区别td
是否需要被合并。依照这样的方式,我们就能够一层一层的把表格结构相关的Blot全部定义出来。最终实现在Quill编辑器中插入表格。
注意:像表格这种多层嵌套的内容结构,需要在最内层把唯一标识符都设计好,quill.js中嵌套结构的基础在最内层,然后一层一层wrap和merge,在wrap的时候把所需要的唯一标识符往外传递和使用。
结语
表格相关Blot的定义,以及表格编辑常用功能的实现涉及到的具体细节太多,在这里不赘述,有兴趣的同学可以到我的quill-better-table开源项目的源代码中查看具体实现细节,相信对将要在Quill中使用Container的同学大有帮助。