JSTQL-JS-Transform/output_testing/411ReactDOMSelection.js

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(%);
}
}
}