Quill
社区优秀实现 quill-awesome
基础
安装:npm install quill -S
使用:
import Quill from "quill";
const quillEditor = new Quill(".editor", {
placeholder: "此处输入文字",
});
主题:
import Quill from "quill";
import "quill/dist/quill.snow.css";
const quillEditor = new Quill(".editor", {
theme: "snow",
placeholder: "此处输入文字",
});
核心
Format
quill.format
对用户选中文本格式化,如果无选区内容(光标),格式将作用于后续输入字符
format(name: String, value: any, source: String = 'api'): Delta
quill.formatText
对编辑器指定范围内容格式化
formatText(
index: Number,
length: Number,
formats: { [String]: any },
source: String = 'api'
): Delta
Embed
quill.insertEmbed
插入嵌入内容
insertEmbed(index: Number, type: String, value: any, source: String = 'api'): Delta
Delta
quill
通过 delta
(json)描述内容和变化
delta操作内容
import Delta from "quill-delta";
const index = quill.getSelection(true).index;
const delta = new Delta().retain(index);
delta.insert("\n");
delta.insert(
{ image: dataString },
{ width: 100 }
);
delta.insert("\n");
quill.updateContents(delta);
quill.setSelection(index + 3, Quill.sources.USER);
链式方法
插入操作:insert(String | Object, attributes)
删除操作:delete(length)
保留操作:retain(length, attribute)
,可实现跳过n项、批量文字处理
delta更新内容
const delta = quill.getContents();
delta.ops = delta.ops.filter... // 过来部分operation
delta.ops = delta.ops.map... // 修改部分operation
quill.setContents(delta);
配置
配置自定义编辑器
new Quill(".editor", Configurations)
modules.toolbar
工具栏选项
{
modules: {
toolbar: ['bold', 'italic', 'underline', 'strike']
}
}
工具栏布局
{
modules: {
toolbar: [['bold', 'italic'], ['link', 'image']]
}
}
工具关键字值
{
modules: {
{ 'header': '3' }
{ size: [ 'small', false, 'large', 'huge' ]},
[{ 'color': [] }, { 'background': [] }], // [] 即主题默认值
}
}
工具处理方法
{
modules: {
handler: {
'link': customLinkHandler'
'bold': customBoldHandler
}
}
}
定制工具栏
HTML 中手动创建工具栏,Quill 会将适当的处理程序附加到类名的形式为 ql-${format}
元素上
<div id="toolbar">
<button class="edit-btn ql-bold">加粗</button>
<button class="edit-btn ql-italic">斜体</button>
<!-- ql-formats表示分组 -->
<span class="ql-formats">
<button class="edit-btn ql-underline">下划线</button>
<button class="edit-btn ql-strike">删除线</button>
<span>
</div>
<div id="editor"></div>
<script>
import "quill/dist/quill.core.css";
new Quill('#editor', {
modules: {
toolbar: '#toolbar'
}
});
</script>
对于下拉选择颜色、字号等工具的定制,通过 format
API + Attributor 实现
// 字号白名单(支持的字号)
const SizeAttributor = Quill.import("attributors/style/size");
SizeAttributor.whitelist = [
"12px",
"14px",
"15px",
"16px",
"17px",
"18px",
"20px",
"24px",
];
Quill.register(SizeAttributor, true);
// 菜单选择调用
editor.format("size", `${size}px`);
editor.container.style.fontSize = `${size}px`; // 设置editor默认字号
主题与CSS
模块(Module)
History
历史记录支持
const quill = new Quill(".editor", {
modules: {
toolbar: "#toolbar",
history: {},
},
});
// 撤销
quill.history.undo()
// 重做
quill.history.redo()
自定义模块
Parchment
支持基于纯文本自定义 Quill 可识别的内容和格式,或添加全新的内容和格式
基于 Parchment API构造 Blot
声明内容/格式接口,最终通过 format
或 embed
调用;
blots(继承自parchment): block(block/embed)、break、container、embed、inline、text…(详见quill/blots目录)
format: Italic、Strike、Bold…(详见quill/formats目录)
embed: Image、Video…(详见quill/formats目录)
自定义blots
format
import Quill from 'quill';
// format id
let Inline = Quill.import('blots/inline');
class IdBlot extends Inline {
static create(value) {
let node = super.create();
node.setAttribute('quill-id', value);
return node;
}
static formats(node) {
return node.getAttribute('quill-id');
}
}
IdBlot.blotName = 'id';
IdBlot.tagName = 'span';
Quill.register(IdBlot);
// 使用
quill.format(0, 10, { id: '0x12345' })
embed
import Quill from 'quill';
let Embed = Quill.import('blots/embed');
class IdMarker extends Embed {
static create(value) {
let node = super.create();
node.setAttribute('quill-id', value);
return node;
}
static value(node) {
return node.getAttribute('quill-id');
}
}
IdMarker.blotName = 'idMarker';
IdMarker.className = 'id-marker'; // 必须声明类名称!!!
IdMarker.tagName = 'span';
Quill.register(IdMarker);
// 使用
quill.insertEmbed(10, 'idMarker', '0x12345' })
其它
操作行为
在editor的 disabled
生效下,需要为每个操作注明 source: api
,否则将视为 user 操作被 blocked
功能实现
文章批注修改
批注操作数据,包含操作类型(增删改),操作位置范围
删除和修改操作在正确位置直接执行即可;插入操作,需预留插入占位符,如下:
let baseIndex = 0; // 基础偏移量,即插入占位符数量
for (let item of checkList) {
if (operationMap.get(item.operation) === '插入') {
// 预留插入占位符
quillEditor.insertEmbed(
item.posStart + baseIndex,
'idMarker',
item.id,
);
baseIndex++;
}
// 删改操作高亮显示
else {
quillEditor.formatText(
item.posStart + baseIndex,
item.len,
{
color: '#f56c6c',
id: item.id,
},
);
}
}
表情emoji
当文章内容存在emoji字符时,由于表情实际占位字符长大于1,导致后续文字实际位置偏移(从人的理解上),导致读取或操作的范围位置不正确
通过一个文字位置映射解决
const indexDictionary = []; // 包含内容每个文字对应的正确index
[...articleInfo.value.content].reduce((total, chat) => {
indexDictionary.push(total);
return total + chat.length;
}, 0);
string.charAt(indexDictionary[index]) // string的index处字符,读取表情需额外处理