React DOM selection
一、作用
二、获取偏移量
/**
* @param {DOMElement} outerNode
* @return {?object}
*/
export function getOffsets(outerNode) {
const { ownerDocument } = outerNode;
const win = (ownerDocument && ownerDocument.defaultView) || window;
const selection = win.getSelection && win.getSelection();
if (!selection || selection.rangeCount === 0) {
return null;
}
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
// In Firefox, anchorNode and focusNode can be "anonymous divs", e.g. the
// up/down buttons on an <input type="number">. Anonymous divs do not seem to
// expose properties, triggering a "Permission denied error" if any of its
// properties are accessed. The only seemingly possible way to avoid erroring
// is to access a property that typically works for non-anonymous divs and
// catch any error that may otherwise arise. See
// 在 Firefox 中,anchorNode 和 focusNode 可能是“匿名 div”,例如
// `<input type="number">` 上的上下箭头按钮。匿名 div 似乎不会暴露属性,如果访问其
// 任何属性,会触发“权限被拒绝错误”。唯一看起来可能避免错误的方法是访问通常对非匿名
// div 有效的属性,并捕捉可能发生的任何错误。参见
// https://bugzilla.mozilla.org/show_bug.cgi?id=208427
try {
anchorNode.nodeType;
focusNode.nodeType;
} catch (e) {
return null;
}
return getModernOffsetsFromPoints(
outerNode,
anchorNode,
anchorOffset,
focusNode,
focusOffset,
);
}
三、从点获取现代偏移量
备注
TEXT_NODE由 HTMLNodeType#TEXT_NODE 提供
/**
* Returns {start, end} where `start` is the character/codepoint index of
* (anchorNode, anchorOffset) within the textContent of `outerNode`, and
* `end` is the index of (focusNode, focusOffset).
*
* 返回 `{start, end}` ,其中 `start` 是 `(anchorNode, anchorOffset)` 在
* `outerNode` 的 textContent 中的字符/代码点索引,`end` 是
* `(focusNode, focusOffset)` 的索引。
*
* Returns null if you pass in garbage input but we should probably just crash.
* 如果你传入无效输入,会返回 null,但我们可能应该直接让程序崩溃。
*
* Exported only for testing.
* 仅用于测试导出。
*/
export function getModernOffsetsFromPoints(
outerNode,
anchorNode,
anchorOffset,
focusNode,
focusOffset,
) {
let length = 0;
let start = -1;
let end = -1;
let indexWithinAnchor = 0;
let indexWithinFocus = 0;
let node = outerNode;
let parentNode = null;
outer: while (true) {
let next = null;
while (true) {
if (
node === anchorNode &&
(anchorOffset === 0 || node.nodeType === TEXT_NODE)
) {
start = length + anchorOffset;
}
if (
node === focusNode &&
(focusOffset === 0 || node.nodeType === TEXT_NODE)
) {
end = length + focusOffset;
}
if (node.nodeType === TEXT_NODE) {
length += node.nodeValue.length;
}
if ((next = node.firstChild) === null) {
break;
}
// Moving from `node` to its first child `next`.
// 从 `node` 移动到它的第一个子节点 `next`。
parentNode = node;
node = next;
}
while (true) {
if (node === outerNode) {
// If `outerNode` has children, this is always the second time visiting
// it. If it has no children, this is still the first loop, and the only
// valid selection is anchorNode and focusNode both equal to this node
// and both offsets 0, in which case we will have handled above.
// 如果 `outerNode` 有子节点,这总是第二次访问它。如果它没有子节点,这仍然是第
// 一次循环,并且唯一有效的选择是 anchorNode 和 focusNode 都等于这个节点,且
// 两个偏移量都是 0,在这种情况下我们将会在上面已经处理过。
break outer;
}
if (parentNode === anchorNode && ++indexWithinAnchor === anchorOffset) {
start = length;
}
if (parentNode === focusNode && ++indexWithinFocus === focusOffset) {
end = length;
}
if ((next = node.nextSibling) !== null) {
break;
}
node = parentNode;
parentNode = node.parentNode;
}
// Moving from `node` to its next sibling `next`.
// 从 `node` 移动到它的下一个兄弟节点 `next`。
node = next;
}
if (start === -1 || end === -1) {
// This should never happen. (Would happen if the anchor/focus nodes aren't
// actually inside the passed-in node.)
// 这永远不应该发生。(如果锚点/焦点节点实际上不在传入的节点内,就会发生这种情况。)
return null;
}
return {
start: start,
end: end,
};
}
四、设置偏移
备注
getNodeForCharacterOffset()由 getNodeForCharacterOffset#getNodeForCharacterOffset 实现
/**
* In modern non-IE browsers, we can support both forward and backward
* selections.
* 在现代非IE浏览器中,我们可以支持前向和后向选择。
*
* Note: IE10+ supports the Selection object, but it does not support
* the `extend` method, which means that even in modern IE, it's not possible
* to programmatically create a backward selection. Thus, for all IE
* versions, we use the old IE API to create our selections.
*
* 注意:IE10 支持 Selection 对象,但它不支持 `extend` 方法,这意味着即使在现代 IE
* 中,也无法以编程方式创建向后选择。因此,对于所有 IE 版本,我们使用旧的 IE API 来创建
* 选择。
*
* @param {DOMElement|DOMTextNode} node
* @param {object} offsets
*/
export function setOffsets(node, offsets) {
const doc = node.ownerDocument || document;
const win = (doc && doc.defaultView) || window;
// Edge fails with "Object expected" in some scenarios.
// (For instance: TinyMCE editor used in a list component that supports pasting to add more,
// fails when pasting 100+ items)
// 在某些情况下,Edge 会出现“需要对象”的错误。(例如:在一个支持粘贴以添加更多内容的列
// 表组件中使用 TinyMCE 编辑器时,粘贴 100 个项目会失败)
if (!win.getSelection) {
return;
}
const selection = win.getSelection();
const length = node.textContent.length;
let start = Math.min(offsets.start, length);
let end = offsets.end === undefined ? start : Math.min(offsets.end, length);
// IE 11 uses modern selection, but doesn't support the extend method.
// Flip backward selections, so we can set with a single range.
// IE 11 使用现代选择,但不支持 extend 方法。翻转向后的选择,这样我们可以用单个范围
// 来设置。
if (!selection.extend && start > end) {
const temp = end;
end = start;
start = temp;
}
const startMarker = getNodeForCharacterOffset(node, start);
const endMarker = getNodeForCharacterOffset(node, end);
if (startMarker && endMarker) {
if (
selection.rangeCount === 1 &&
selection.anchorNode === startMarker.node &&
selection.anchorOffset === startMarker.offset &&
selection.focusNode === endMarker.node &&
selection.focusOffset === endMarker.offset
) {
return;
}
const range = doc.createRange();
range.setStart(startMarker.node, startMarker.offset);
selection.removeAllRanges();
if (start > end) {
selection.addRange(range);
selection.extend(endMarker.node, endMarker.offset);
} else {
range.setEnd(endMarker.node, endMarker.offset);
selection.addRange(range);
}
}
}