2021SC@SDUSC
ReactDOM input中selection相关操作的源码分析
由于在React中对类似于<input>
、<textarea>
等组件的输入操作一般都是在受控状态下进行编辑、输入操作的,因此在光标定位、选择等方面需要进行适配。如何获取到当前光标所处位置?如何确保光标在输入操作后保持在输入内容的最后?ReactDOM对这类问题进行了完善的针对浏览器不同差异的抹平适配,确保在各端的输入selection行为保持一致。
相关源码在packages\react-dom\src\client\ReactDOMSelection.js
中定义。其中的getModernOffsetsFromPoints()
方法能够确保React获取到正确的光标偏移量,setOffsets()
方法可以设置光标在输入框中的定位偏移量。
getModernOffsetsFromPoints
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`.
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.
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;
}
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,
};
}
setOffsets
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)
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.
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);
}
}
}