共计 7307 个字符,预计需要花费 19 分钟才能阅读完成。
术语表
首先咱们须要晓得一些术语, 能力更好地了解, 如果您曾经理解, 能够跳过这一段
锚点 (anchor)
锚指的是一个选区的起始点(不同于 HTML 中的锚点链接)。当咱们应用鼠标框选一个区域的时候,锚点就是咱们鼠标按下霎时的那个点。在用户拖动鼠标时,锚点是不会变的。
焦点 (focus)
选区的焦点是该选区的起点,当您用鼠标框选一个选区的时候,焦点是你的鼠标松开霎时所记录的那个点。随着用户拖动鼠标,焦点的地位会随着扭转。
范畴 (range)
范畴指的是文档中间断的一部分。一个范畴包含整个节点,也能够蕴含节点的一部分,例如文本节点的一部分。用户通常下只能抉择一个范畴,然而有的时候用户也有可能抉择多个范畴(例如当用户按下 Control 按键并框选多个区域时,Chrome 中禁止了这个操作)。“范畴”会被作为 Range
对象返回。Range 对象也能通过 DOM 创立、减少、删减。
本术语表来源于 MDN
contenteditable
contenteditable 全局属性是一个枚举属性,示意该元素是否应该由用户编辑。如果是的话,浏览器就会批改其小部件以容许编辑。
简略的来说, 如果要让一个 div 变得可编辑, 咱们加上这个属性就能实现了
这就是富文本编辑器的最根底的结构了, 想要残缺的富文本, 首先咱们要管制他的光标
而浏览器提供了 selection 对象和 range 对象来操作光标。
Selection
Selection 对象示意用户抉择的文本范畴或插入符号的以后地位。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标通过文字而产生。
咱们能够通过 API window.getSelection()
来获取以后用户选中了哪些文本
这是调用后的返回后果:
局部属性阐明
anchorNode 只读
返回该选区终点所在的节点(Node)。
anchorOffset 只读
返回一个数字,其示意的是选区终点在
anchorNode
中的地位偏移量。
- 如果
anchorNode
是文本节点,那么返回的就是从该文字节点的第一个字开始,直到被选中的第一个字之间的字数(如果第一个字就被选中,那么偏移量为零)。- 如果
anchorNode
是一个元素,那么返回的就是在选区第一个节点之前的同级节点总数。(这些节点都是anchorNode
的子节点 )
isCollapsed 只读
返回一个布尔值,用于判断选区的起始点和起点是否在同一个地位。
rangeCount 只读
返回该选区所蕴含的间断范畴的数量。
办法
这里只论述几个重要的办法
getRangeAt
var selObj = window.getSelection();
range = sel.getRangeAt(index)
例子:
let ranges = [];
sel = window.getSelection();
for(var i = 0; i < sel.rangeCount; i++) {ranges[i] = sel.getRangeAt(i);
}
/* 在 ranges 数组的每一个元素都是一个 range 对象,* 对象的内容是以后选区中的一个。*/
在很多状况下, rangeCount
的数量都是 1
他的返回值是一个 Range
, 具体在本文的 Range
局部解说
addRange
向选区(Selection)中增加一个区域(Range)。
这里举一个小栗子就能疾速了解:
<strong id="foo"> 这是一段话巴拉巴拉 </strong>
<strong id="bar"> 这是另一段话 </strong>
var s = window.getSelection();
// 一开始咱们让他选中 foo 节点
var range = document.createRange();
range.selectNode(foo);
s.addRange(range);
// 在一秒钟后咱们勾销 foo 节点的选中, 抉择所有 body 节点
setTimeout(()=>{s.removeAllRanges();
var range2 = document.createRange();
range2.selectNode(document.body);
s.addRange(range2);
}, 1000)
成果展现:
遇到 contenteditable 元素时
如果 strong#foo
元素是一个 contenteditable
元素: <strong id="foo" contenteditable="true"> 这是一段话巴拉巴拉 </strong>
那么咱们不能间接用 range.selectNode(foo);
, 而是应该这样做:
var range = document.createRange();
range.setStart(foo, 0)
range.setEnd(foo, 1)
// 其中 0, 1 代表子节点数量
s.addRange(range);
其中 setStart
和 setEnd
第二个参数:
如果起始节点类型是 Text、Comment 或 CDATASection 之一,那么 startOffset 指的是从起始节点算起字符的偏移量。对于其余 Node 类型节点,startOffset 是指从起始结点开始算起子节点的偏移量。
或者应用 selectNodeContents
API:
var range = document.createRange();
range.selectNodeContents(foo)
s.addRange(range);
collapse
collapse 办法能够收起以后选区到一个点。文档不会产生扭转。如果选区的内容是可编辑的并且焦点落在下面,则光标会在该处闪动。
同样地, 这里也创立一个例子
<p id="foo"> 这是一段话巴拉巴拉 </p>
var s = window.getSelection();
var range = document.createRange();
range.selectNode(foo);
s.addRange(range);
setTimeout(()=>{s.collapse(foo, 0);
}, 1000)
成果是, 在 1 秒之后, 选区隐没了
咱们再在 p
标签上增加 contenteditable
尝试下:
<p contenteditable="true" id="foo"> 这是一段话巴拉巴拉 </p>
var s = window.getSelection();
var range = document.createRange();
range.selectNodeContents(foo)
s.addRange(range);
setTimeout(()=>{s.collapse(foo, 1);
}, 1000)
成果展现:
Range
Range 接口示意一个蕴含节点与文本节点的一部分的文档片段。
在上述的例子中, 咱们曾经尝试过应用 Document.createRange
办法创立 Range
也能够通过 Selection
对象的 getRangeAt()
办法或者 Document
对象的 caretRangeFromPoint()
办法获取 Range
对象。
Range
(通过 document.createRange();
创立 ) 领有这些属性:
{
collapsed:true // 示意 Range 的起始地位和终止地位是否雷同的布尔值
commonAncestorContainer:document // 返回残缺蕴含 startContainer 和 endContainer 的、最深一级的节点
endContainer:document // 蕴含 Range 起点的节点。endOffset:0 // 一个示意 Range 起点在 endContainer 中的地位的数字。startContainer:document // 蕴含 Range 开始的节点。startOffset:0 // 一个数字,示意 Range 在 startContainer 中的起始地位。}
collapse
Range.collapse() 办法向边界点折叠该 Range
语法:
range.collapse(toStart);
toStart 可选
一个布尔值:true
折叠到 Range 的 start 节点,false
折叠到 end 节点。如果省略,则默认为 false
在之前的 Selection
– collapse
例子中, 咱们也能够通过此 API 来操作, 达到雷同的成果:
<p contenteditable="true" id="foo"> 这是一段话巴拉巴拉 </p>
var s = window.getSelection();
var range = document.createRange();
range.selectNodeContents(foo)
s.addRange(range);
setTimeout(()=>{range.collapse()
// s.collapse(foo, 1);
}, 1000)
在前文中曾经尝试过应用 selectNode()
, selectNodeContents()
, setEnd()
, setStart()
等办法, 这里就不在多赘述
quill 中的 Selection
在 quill 中, 会基于原生 API 获取信息, 并包装出一个本人的对象:
getRange() {
const root = this.scroll.domNode;
// 省略空值判断
const normalized = this.getNativeRange(); // 咱们先看这个函数
if (normalized == null) return [null, null];
// 后续临时疏忽
}
getRange
函数就是 quill
中, 获取选区的办法, 而 normalized
是基于原生的 api, 并通过肯定的包装, 来获取数据:
getNativeRange() {const selection = document.getSelection();
if (selection == null || selection.rangeCount <= 0) return null;
const nativeRange = selection.getRangeAt(0);
if (nativeRange == null) return null;
// 下面四句都是通过原生 api, 来判断以后是否有选区
// 因为基本上 rangeCount 都是 1, 所以间接通过 getRangeAt(0) 即可获取选区
// 这里的 normalizeNative 才是对原生真正的操作
// nativeRange 是以后
const range = this.normalizeNative(nativeRange);
return range;
}
normalizeNative
normalizeNative(nativeRange) {
// 判断选区是否在以后的编辑器根元素中, 是否是选中状态
if (!contains(this.root, nativeRange.startContainer) ||
(!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))
) {return null;}
// 结构一个自定义对象, 存储原生数据
const range = {
start: {
node: nativeRange.startContainer,
offset: nativeRange.startOffset, // 开始元素的偏移, 然而并不代表是从视觉看上去的偏移, 具体看 nativeRange.startContainer.data
},
end: {node: nativeRange.endContainer, offset: nativeRange.endOffset},
native: nativeRange,
};
// 开始遍历 [range.start, range.end]
[range.start, range.end].forEach(position => {
// 从原生处取值: node = range.startContainer offset = range.startOffset
let {node, offset} = position;
// 当某一节点不是 text, 且有子节点时
// 因为在 quill, 中会有一些非凡的格局, 比方图片, 视频, emoji 等等
// 这些非凡格局在选区中的占位是不同的, 举个例子: 一张看着很大的图片, 但其实偏移量只有 1
// 同时, 如果咱们须要一些定制的性能的话, 这里的判断可能会影响选区 , 所以咱们须要对这里做出一些非凡的判断
while (!(node instanceof Text) && node.childNodes.length > 0) {if (node.childNodes.length > offset) { // 超出的状况判断
node = node.childNodes[offset];
offset = 0;
} else if (node.childNodes.length === offset) {
node = node.lastChild;
if (node instanceof Text) {offset = node.data.length;} else if (node.childNodes.length > 0) {
// Container case
offset = node.childNodes.length;
} else {
// Embed case
offset = node.childNodes.length + 1;
}
} else {break;}
}
position.node = node;
position.offset = offset; // 赋值
});
return range;
}
最初返回一个自定义 range 对象
normalizedToRange
而在自定义对象包装完结之后, 还会经验一次计算 normalizedToRange
办法
getRange() {
const root = this.scroll.domNode;
// 省略空值判断
const normalized = this.getNativeRange(); // 返回的自定义 range
if (normalized == null) return [null, null];
const range = this.normalizedToRange(normalized);
return [range, normalized];
}
normalizedToRange:
// range 的构造
// const range = {
// start: {
// node: nativeRange.startContainer,
// offset: nativeRange.startOffset, // 开始元素的偏移, 然而并不代表是从视觉看上去的偏移, 具体看 nativeRange.startContainer.data
// },
// end: {node: nativeRange.endContainer, offset: nativeRange.endOffset},
// native: nativeRange,
// };
//
normalizedToRange(range) {const positions = [[range.start.node, range.start.offset]];
// 如果不是闭合的即光标状态, 则新增 开端的数据到数组中
if (!range.native.collapsed) {positions.push([range.end.node, range.end.offset]);
}
// 遍历数据, 获取索引 (从编辑框的第 0 个开始算)
const indexes = positions.map(position => {
// 通过 normalizeNative 批改的取值, 和原生相比, node 和 offset 可能产生了批改
const [node, offset] = position;
// 搜寻到对应的 dom
const blot = this.scroll.find(node, true);
// 通过他的 api 来获取偏移量, 能够查看 https://github.com/quilljs/parchment
const index = blot.offset(this.scroll);
// 如果在某一个 dom 上的偏移量为 0, 那么以后索引就是 dom 的索引
if (offset === 0) {return index;}
// LeafBlot 属于非凡状况, 属于子节点, 属于 parchment 库
if (blot instanceof LeafBlot) {return index + blot.index(node, offset);
}
// 最初加上以后节点的长度
return index + blot.length();});
// 比拟以后的索引和, 编辑器的长度, 不让他超出
const end = Math.min(Math.max(...indexes), this.scroll.length() - 1);
// 比拟结尾和索引, 获取最小值, 作为开始值
const start = Math.min(end, ...indexes);
// 通过原生 api 从新 new 一个新的 Range 对象, 传参为 start 和 length
return new Range(start, end - start);
}
执行程序
查看 selection
通过官网 API, 咱们即可查看到之前计算的数据:
const editorRef = useRef<any>()
editorRef.current?.getEditor()?.selection
后果如图:
其中 lastRange
对应 normalizedToRange
的后果,
而 lastNative
则是 getNativeRange
的返回 (包装的原生数据)
总结
本文次要介绍了 原生 API: Selection 和 Range 的作用和他的属性、办法的阐明,
并通过这两 API 介绍在 quill 中, API 会有什么影响, 咱们又须要采纳哪些判断
总的来说, 这两 API 在除富文本性能中, 根本不会遇见, 所以大多数状况下, 只须要理解即可
援用
- https://developer.mozilla.org…
- https://github.com/quilljs/pa…
- https://github.com/quilljs/quill