main
js 199 lines 5.92 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 getActiveElement from './getActiveElement';
9
10 import {getOffsets, setOffsets} from './ReactDOMSelection';
11 import {ELEMENT_NODE, TEXT_NODE} from './HTMLNodeType';
12
13 function isTextNode(node) {
14 return node && node.nodeType === TEXT_NODE;
15 }
16
17 function containsNode(outerNode, innerNode) {
18 if (!outerNode || !innerNode) {
19 return false;
20 } else if (outerNode === innerNode) {
21 return true;
22 } else if (isTextNode(outerNode)) {
23 return false;
24 } else if (isTextNode(innerNode)) {
25 return containsNode(outerNode, innerNode.parentNode);
26 } else if ('contains' in outerNode) {
27 return outerNode.contains(innerNode);
28 } else if (outerNode.compareDocumentPosition) {
29 return !!(outerNode.compareDocumentPosition(innerNode) & 16);
30 } else {
31 return false;
32 }
33 }
34
35 function isInDocument(node) {
36 return (
37 node &&
38 node.ownerDocument &&
39 containsNode(node.ownerDocument.documentElement, node)
40 );
41 }
42
43 function isSameOriginFrame(iframe) {
44 try {
45 // Accessing the contentDocument of a HTMLIframeElement can cause the browser
46 // to throw, e.g. if it has a cross-origin src attribute.
47 // Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g:
48 // iframe.contentDocument.defaultView;
49 // A safety way is to access one of the cross origin properties: Window or Location
50 // Which might result in "SecurityError" DOM Exception and it is compatible to Safari.
51 // https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl
52
53 return typeof iframe.contentWindow.location.href === 'string';
54 } catch (err) {
55 return false;
56 }
57 }
58
59 function getActiveElementDeep(containerInfo) {
60 let win =
61 containerInfo != null &&
62 containerInfo.ownerDocument != null &&
63 containerInfo.ownerDocument.defaultView != null
64 ? containerInfo.ownerDocument.defaultView
65 : window;
66 let element = getActiveElement(win.document);
67 while (element instanceof win.HTMLIFrameElement) {
68 if (isSameOriginFrame(element)) {
69 win = element.contentWindow;
70 } else {
71 return element;
72 }
73 element = getActiveElement(win.document);
74 }
75 return element;
76 }
77
78 /**
79 * @ReactInputSelection: React input selection module. Based on Selection.js,
80 * but modified to be suitable for react and has a couple of bug fixes (doesn't
81 * assume buttons have range selections allowed).
82 * Input selection module for React.
83 */
84
85 /**
86 * @hasSelectionCapabilities: we get the element types that support selection
87 * from https://html.spec.whatwg.org/#do-not-apply, looking at `selectionStart`
88 * and `selectionEnd` rows.
89 */
90 export function hasSelectionCapabilities(elem) {
91 const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
92 return (
93 nodeName &&
94 ((nodeName === 'input' &&
95 (elem.type === 'text' ||
96 elem.type === 'search' ||
97 elem.type === 'tel' ||
98 elem.type === 'url' ||
99 elem.type === 'password')) ||
100 nodeName === 'textarea' ||
101 elem.contentEditable === 'true')
102 );
103 }
104
105 export function getSelectionInformation(containerInfo) {
106 const focusedElem = getActiveElementDeep(containerInfo);
107 return {
108 focusedElem: focusedElem,
109 selectionRange: hasSelectionCapabilities(focusedElem)
110 ? getSelection(focusedElem)
111 : null,
112 };
113 }
114
115 /**
116 * @restoreSelection: If any selection information was potentially lost,
117 * restore it. This is useful when performing operations that could remove dom
118 * nodes and place them back in, resulting in focus being lost.
119 */
120 export function restoreSelection(priorSelectionInformation, containerInfo) {
121 const curFocusedElem = getActiveElementDeep(containerInfo);
122 const priorFocusedElem = priorSelectionInformation.focusedElem;
123 const priorSelectionRange = priorSelectionInformation.selectionRange;
124 if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
125 if (
126 priorSelectionRange !== null &&
127 hasSelectionCapabilities(priorFocusedElem)
128 ) {
129 setSelection(priorFocusedElem, priorSelectionRange);
130 }
131
132 // Focusing a node can change the scroll position, which is undesirable
133 const ancestors = [];
134 let ancestor = priorFocusedElem;
135 while ((ancestor = ancestor.parentNode)) {
136 if (ancestor.nodeType === ELEMENT_NODE) {
137 ancestors.push({
138 element: ancestor,
139 left: ancestor.scrollLeft,
140 top: ancestor.scrollTop,
141 });
142 }
143 }
144
145 if (typeof priorFocusedElem.focus === 'function') {
146 priorFocusedElem.focus();
147 }
148
149 for (let i = 0; i < ancestors.length; i++) {
150 const info = ancestors[i];
151 info.element.scrollLeft = info.left;
152 info.element.scrollTop = info.top;
153 }
154 }
155 }
156
157 /**
158 * @getSelection: Gets the selection bounds of a focused textarea, input or
159 * contentEditable node.
160 * -@input: Look up selection bounds of this input
161 * -@return {start: selectionStart, end: selectionEnd}
162 */
163 export function getSelection(input) {
164 let selection;
165
166 if ('selectionStart' in input) {
167 // Modern browser with input or textarea.
168 selection = {
169 start: input.selectionStart,
170 end: input.selectionEnd,
171 };
172 } else {
173 // Content editable or old IE textarea.
174 selection = getOffsets(input);
175 }
176
177 return selection || {start: 0, end: 0};
178 }
179
180 /**
181 * @setSelection: Sets the selection bounds of a textarea or input and focuses
182 * the input.
183 * -@input Set selection bounds of this input or textarea
184 * -@offsets Object of same form that is returned from get*
185 */
186 export function setSelection(input, offsets) {
187 const start = offsets.start;
188 let end = offsets.end;
189 if (end === undefined) {
190 end = start;
191 }
192
193 if ('selectionStart' in input) {
194 input.selectionStart = start;
195 input.selectionEnd = Math.min(end, input.value.length);
196 } else {
197 setOffsets(input, offsets);
198 }
199 }