main
js 202 lines 5.77 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import getNodeForCharacterOffset from './getNodeForCharacterOffset';
9 import {TEXT_NODE} from './HTMLNodeType';
10
11 /**
12 * @param {DOMElement} outerNode
13 * @return {?object}
14 */
15 export function getOffsets(outerNode) {
16 const {ownerDocument} = outerNode;
17 const win = (ownerDocument && ownerDocument.defaultView) || window;
18 const selection = win.getSelection && win.getSelection();
19
20 if (!selection || selection.rangeCount === 0) {
21 return null;
22 }
23
24 const {anchorNode, anchorOffset, focusNode, focusOffset} = selection;
25
26 // In Firefox, anchorNode and focusNode can be "anonymous divs", e.g. the
27 // up/down buttons on an <input type="number">. Anonymous divs do not seem to
28 // expose properties, triggering a "Permission denied error" if any of its
29 // properties are accessed. The only seemingly possible way to avoid erroring
30 // is to access a property that typically works for non-anonymous divs and
31 // catch any error that may otherwise arise. See
32 // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
33 try {
34 /* eslint-disable ft-flow/no-unused-expressions */
35 anchorNode.nodeType;
36 focusNode.nodeType;
37 /* eslint-enable ft-flow/no-unused-expressions */
38 } catch (e) {
39 return null;
40 }
41
42 return getModernOffsetsFromPoints(
43 outerNode,
44 anchorNode,
45 anchorOffset,
46 focusNode,
47 focusOffset,
48 );
49 }
50
51 /**
52 * Returns {start, end} where `start` is the character/codepoint index of
53 * (anchorNode, anchorOffset) within the textContent of `outerNode`, and
54 * `end` is the index of (focusNode, focusOffset).
55 *
56 * Returns null if you pass in garbage input but we should probably just crash.
57 *
58 * Exported only for testing.
59 */
60 export function getModernOffsetsFromPoints(
61 outerNode,
62 anchorNode,
63 anchorOffset,
64 focusNode,
65 focusOffset,
66 ) {
67 let length = 0;
68 let start = -1;
69 let end = -1;
70 let indexWithinAnchor = 0;
71 let indexWithinFocus = 0;
72 let node = outerNode;
73 let parentNode = null;
74
75 outer: while (true) {
76 let next = null;
77
78 while (true) {
79 if (
80 node === anchorNode &&
81 (anchorOffset === 0 || node.nodeType === TEXT_NODE)
82 ) {
83 start = length + anchorOffset;
84 }
85 if (
86 node === focusNode &&
87 (focusOffset === 0 || node.nodeType === TEXT_NODE)
88 ) {
89 end = length + focusOffset;
90 }
91
92 if (node.nodeType === TEXT_NODE) {
93 length += node.nodeValue.length;
94 }
95
96 if ((next = node.firstChild) === null) {
97 break;
98 }
99 // Moving from `node` to its first child `next`.
100 parentNode = node;
101 node = next;
102 }
103
104 while (true) {
105 if (node === outerNode) {
106 // If `outerNode` has children, this is always the second time visiting
107 // it. If it has no children, this is still the first loop, and the only
108 // valid selection is anchorNode and focusNode both equal to this node
109 // and both offsets 0, in which case we will have handled above.
110 break outer;
111 }
112 if (parentNode === anchorNode && ++indexWithinAnchor === anchorOffset) {
113 start = length;
114 }
115 if (parentNode === focusNode && ++indexWithinFocus === focusOffset) {
116 end = length;
117 }
118 if ((next = node.nextSibling) !== null) {
119 break;
120 }
121 node = parentNode;
122 parentNode = node.parentNode;
123 }
124
125 // Moving from `node` to its next sibling `next`.
126 node = next;
127 }
128
129 if (start === -1 || end === -1) {
130 // This should never happen. (Would happen if the anchor/focus nodes aren't
131 // actually inside the passed-in node.)
132 return null;
133 }
134
135 return {
136 start: start,
137 end: end,
138 };
139 }
140
141 /**
142 * In modern non-IE browsers, we can support both forward and backward
143 * selections.
144 *
145 * Note: IE10+ supports the Selection object, but it does not support
146 * the `extend` method, which means that even in modern IE, it's not possible
147 * to programmatically create a backward selection. Thus, for all IE
148 * versions, we use the old IE API to create our selections.
149 *
150 * @param {DOMElement|DOMTextNode} node
151 * @param {object} offsets
152 */
153 export function setOffsets(node, offsets) {
154 const doc = node.ownerDocument || document;
155 const win = (doc && doc.defaultView) || window;
156
157 // Edge fails with "Object expected" in some scenarios.
158 // (For instance: TinyMCE editor used in a list component that supports pasting to add more,
159 // fails when pasting 100+ items)
160 if (!win.getSelection) {
161 return;
162 }
163
164 const selection = win.getSelection();
165 const length = node.textContent.length;
166 let start = Math.min(offsets.start, length);
167 let end = offsets.end === undefined ? start : Math.min(offsets.end, length);
168
169 // IE 11 uses modern selection, but doesn't support the extend method.
170 // Flip backward selections, so we can set with a single range.
171 if (!selection.extend && start > end) {
172 const temp = end;
173 end = start;
174 start = temp;
175 }
176
177 const startMarker = getNodeForCharacterOffset(node, start);
178 const endMarker = getNodeForCharacterOffset(node, end);
179
180 if (startMarker && endMarker) {
181 if (
182 selection.rangeCount === 1 &&
183 selection.anchorNode === startMarker.node &&
184 selection.anchorOffset === startMarker.offset &&
185 selection.focusNode === endMarker.node &&
186 selection.focusOffset === endMarker.offset
187 ) {
188 return;
189 }
190 const range = doc.createRange();
191 range.setStart(startMarker.node, startMarker.offset);
192 selection.removeAllRanges();
193
194 if (start > end) {
195 selection.addRange(range);
196 selection.extend(endMarker.node, endMarker.offset);
197 } else {
198 range.setEnd(endMarker.node, endMarker.offset);
199 selection.addRange(range);
200 }
201 }
202 }