171 lines
No EOL
5.7 KiB
JavaScript
171 lines
No EOL
5.7 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import getNodeForCharacterOffset from './getNodeForCharacterOffset';
|
|
import { TEXT_NODE } from './HTMLNodeType';
|
|
|
|
/**
|
|
* @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
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=208427
|
|
try {
|
|
/* eslint-disable ft-flow/no-unused-expressions */
|
|
anchorNode.nodeType;
|
|
focusNode.nodeType;
|
|
/* eslint-enable ft-flow/no-unused-expressions */
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
return getModernOffsetsFromPoints(outerNode, anchorNode, anchorOffset, focusNode, focusOffset);
|
|
}
|
|
|
|
/**
|
|
* 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).
|
|
*
|
|
* Returns null if you pass in garbage input but we should probably just crash.
|
|
*
|
|
* 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`.
|
|
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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* In modern non-IE browsers, we can support both forward and backward
|
|
* selections.
|
|
*
|
|
* 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.
|
|
*
|
|
* @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)
|
|
if (!win.getSelection) {
|
|
return;
|
|
}
|
|
const selection = win.getSelection();
|
|
const length = node.textContent.length;
|
|
let start = offsets.start |> Math.min(%, length);
|
|
let end = offsets.end === undefined ? start : offsets.end |> Math.min(%, 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 = node |> getNodeForCharacterOffset(%, start);
|
|
const endMarker = node |> getNodeForCharacterOffset(%, 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();
|
|
startMarker.node |> range.setStart(%, startMarker.offset);
|
|
selection.removeAllRanges();
|
|
if (start > end) {
|
|
range |> selection.addRange(%);
|
|
endMarker.node |> selection.extend(%, endMarker.offset);
|
|
} else {
|
|
endMarker.node |> range.setEnd(%, endMarker.offset);
|
|
range |> selection.addRange(%);
|
|
}
|
|
}
|
|
} |