概念介绍
Parchment是Quill的文档模型。是一个和DOM树对应的平行树结构,给内容编辑器Quill提供有用的功能。
一个Parchment 树是由Blots构成。Blot是一个DOM节点的对应物。Blots可以提供结构,格式化,或内容。Attributor可以提供轻量级的格式化信息。
Parchment tree是DOM tree的对应,二者关系紧密。
官方示例
LinkBlot
import Parchment from 'parchment';
class LinkBlot extends Parchment.Inline {
static create(url) {
let node = super.create();
node.setAttribute('href', url);
node.setAttribute('target', '_blank');
node.setAttribute('title', node.textContent);
return node;
}
static formats(domNode) {
return domNode.getAttribute('href') || true;
}
format(name, value) {
if (name === 'link' && value) {
this.domNode.setAttribute('href', value);
} else {
super.format(name, value);
}
}
formats() {
let formats = super.formats();
formats['link'] = LinkBlot.formats(this.domNode);
return formats;
}
}
LinkBlot.blotName = 'link';
LinkBlot.tagName = 'A';
Parchment.register(LinkBlot);
常见问题
富文本编辑器为什么需要自己的一套文档模型?
为什么必要?
为了提供一致的编辑体验,你需要一致的数据和可预测的行为。但是DOM在这两方面都不完美。所以现代的编辑器通过管理自己的文档模型来表示内容。
它的价值?
提供一致的数据和可预测的行为
如何构造出一套文档模型?如何与DOM建立关系?
首先需要定义出一套基础抽象节点类型, 一套基础的Attributor
ParentBlot,
ContainerBlot,
LeafBlot,
EmbedBlot,
ScrollBlot,
BlockBlot,
InlineBlot,
TextBlot
Attributor
ClassAttributor
StyleAttributor
然后会依赖于这些基础节点类型,来构造出一些实际节点类型。Quill中定义了一些实际节点
BlockBlot => Block
EmbedBlot => BlockEmbed
EmbedBlot => Break
ContainerBlot => Container
EmbedBlot => Cursor
EmbedBlot => Embed
InlineBlot => Inline
ScrollBlot => Scroll
TextBlot => Text
如何与DOM建立关系?
新建Blot时会调用static create
方法创建dom节点,并设置blot.domNode = dom
。 即建立关系。
源码分析
基础设计
目录结构
- src
- attributor
- attributor.ts
- class.ts
- store.ts
- style.ts
- blot
- abstract
- blot.ts
- container.ts
- format.ts
- leaf.ts
- shadow.ts
- block.ts
- embed.ts
- inline.ts
- scroll.ts
- text.ts
- collection
- linked-list.ts
- linked.node.ts
- parchment.ts
- registry.ts
类图完整版
类图简易版
在parchment.ts中对外导出的有四类东西。
-
节点Blot
- ParentBlot 【父级节点】能对子节点进行增,删,改,移动,查
- ContainerBlot 【容器节点】
- LeafBlot 【叶节点】
- EmbedBlot 嵌入式节点 【可格式化的叶节点】
- ScrollBlot root【文档的根节点,不可格式化】
- BlockBlot 块级 【可格式化的父级节点】
- InlineBlot 内联 【可格式化的父级节点】
- TextBlot 文本【叶节点】
-
属性Attributor
- Attributor 【一种代表格式的方法】
- ClassAttributor 【使用classname模式来代表格式】
- StyleAttributor 【使用内联样式来代表格式】
- AttributorStore 【节点的attributes管理器】在BlockBlot InlineBlot中使用到了
-
注册中心
- Registry 【static blots = new WeakMap
, attributes,classes,tags,types 】
- Registry 【static blots = new WeakMap
-
类型常量Scope
- Scope
从一个例子看源码流程
let Inline = Quill.import('blots/inline');
class BoldBlot extends Inline { }
BoldBlot.blotName = 'bold';
BoldBlot.tagName = 'strong';
class ItalicBlot extends Inline { }
ItalicBlot.blotName = 'italic';
ItalicBlot.tagName = 'em';
Quill.register(BoldBlot);
Quill.register(ItalicBlot);
let quill = new Quill('#editor-container');
$('#bold-button').click(function() {
quill.format('bold', true);
});
$('#italic-button').click(function() {
quill.format('italic', true);
});
- 依赖于Quill提供的Inline类,构造了BoldBlot, ItalicBlot类,并注册到Quill, 会注册出formats/blod, formats/italic(是在Parchment导出的Registry中注册)
- 传入dom id, 构造出一个Quill实例quill。 这里会初始化大量属性,和它内部的模块,注册一些事件,#editor-container中会插入些个quill自己的dom结构,prev sibling会插入一个div.ql-toolbar。
3.给toolbar上的icon绑定事件,click触发时执行quill的格式化方法(里面会做一些判断,看是否有selection, 进行对应的格式化)。
quill.format分两种情况
A. 选中部分内容后,执行格式化。代码流程:
this.editor.formatText -> [this.scroll.formatAt, this.update(delta)] -> scrollBlot.formatAt -> parent.formatAt -> inline.formatAt -> inline.format(DOM修改)
quill.format('bold', true)
本质上会找到BoldBlot,然后执行它的format方法(格式化选中部分),同步更新delta, 真实的修改DOM,给selection添加strong标签。
delta同步和DOM同步是彼此独立的,delta同步相对简单一些(但会做一些组合优化)
const delta = new Delta().retain(index).retain(length, clone(formats));
return this.update(delta);`
B. 未选中内容,执行格式化。
未选中内容是对光标进行格式化。 this.selection.format -> this.cursor.format
。即会对光标后新写的内容对应格式化。
另一个例子
class DividerBlot extends BlockEmbed { }
DividerBlot.blotName = 'divider';
DividerBlot.tagName = 'hr';
$('#divider-button').click(function() {
let range = quill.getSelection(true);
quill.insertText(range.index, '\n', Quill.sources.USER);
quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);
quill.setSelection(range.index + 2, Quill.sources.SILENT);
});
insertEmbed基本流程:quill.insertEmbed -> this.editor.insertEmbed -> [this.scroll.insertAt, this.update(delta)] -> 创建DOM,插到指定位置
同样的,创建DOM,更新delta都会进行。