UNPKG

10.6 kBJavaScriptView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its 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 * @format
8 *
9 * @emails oncall+draft_js
10 */
11'use strict';
12
13var DraftEffects = require("./DraftEffects");
14
15var DraftJsDebugLogging = require("./DraftJsDebugLogging");
16
17var UserAgent = require("fbjs/lib/UserAgent");
18
19var containsNode = require("fbjs/lib/containsNode");
20
21var getActiveElement = require("fbjs/lib/getActiveElement");
22
23var getCorrectDocumentFromNode = require("./getCorrectDocumentFromNode");
24
25var invariant = require("fbjs/lib/invariant");
26
27var isElement = require("./isElement");
28
29var isIE = UserAgent.isBrowser('IE');
30
31function getAnonymizedDOM(node, getNodeLabels) {
32 if (!node) {
33 return '[empty]';
34 }
35
36 var anonymized = anonymizeTextWithin(node, getNodeLabels);
37
38 if (anonymized.nodeType === Node.TEXT_NODE) {
39 return anonymized.textContent;
40 }
41
42 !isElement(anonymized) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Node must be an Element if it is not a text node.') : invariant(false) : void 0;
43 var castedElement = anonymized;
44 return castedElement.outerHTML;
45}
46
47function anonymizeTextWithin(node, getNodeLabels) {
48 var labels = getNodeLabels !== undefined ? getNodeLabels(node) : [];
49
50 if (node.nodeType === Node.TEXT_NODE) {
51 var length = node.textContent.length;
52 return getCorrectDocumentFromNode(node).createTextNode('[text ' + length + (labels.length ? ' | ' + labels.join(', ') : '') + ']');
53 }
54
55 var clone = node.cloneNode();
56
57 if (clone.nodeType === 1 && labels.length) {
58 clone.setAttribute('data-labels', labels.join(', '));
59 }
60
61 var childNodes = node.childNodes;
62
63 for (var ii = 0; ii < childNodes.length; ii++) {
64 clone.appendChild(anonymizeTextWithin(childNodes[ii], getNodeLabels));
65 }
66
67 return clone;
68}
69
70function getAnonymizedEditorDOM(node, getNodeLabels) {
71 // grabbing the DOM content of the Draft editor
72 var currentNode = node; // this should only be used after checking with isElement
73
74 var castedNode = currentNode;
75
76 while (currentNode) {
77 if (isElement(currentNode) && castedNode.hasAttribute('contenteditable')) {
78 // found the Draft editor container
79 return getAnonymizedDOM(currentNode, getNodeLabels);
80 } else {
81 currentNode = currentNode.parentNode;
82 castedNode = currentNode;
83 }
84 }
85
86 return 'Could not find contentEditable parent of node';
87}
88
89function getNodeLength(node) {
90 return node.nodeValue === null ? node.childNodes.length : node.nodeValue.length;
91}
92/**
93 * In modern non-IE browsers, we can support both forward and backward
94 * selections.
95 *
96 * Note: IE10+ supports the Selection object, but it does not support
97 * the `extend` method, which means that even in modern IE, it's not possible
98 * to programatically create a backward selection. Thus, for all IE
99 * versions, we use the old IE API to create our selections.
100 */
101
102
103function setDraftEditorSelection(selectionState, node, blockKey, nodeStart, nodeEnd) {
104 // It's possible that the editor has been removed from the DOM but
105 // our selection code doesn't know it yet. Forcing selection in
106 // this case may lead to errors, so just bail now.
107 var documentObject = getCorrectDocumentFromNode(node);
108
109 if (!containsNode(documentObject.documentElement, node)) {
110 return;
111 }
112
113 var selection = documentObject.defaultView.getSelection();
114 var anchorKey = selectionState.getAnchorKey();
115 var anchorOffset = selectionState.getAnchorOffset();
116 var focusKey = selectionState.getFocusKey();
117 var focusOffset = selectionState.getFocusOffset();
118 var isBackward = selectionState.getIsBackward(); // IE doesn't support backward selection. Swap key/offset pairs.
119
120 if (!selection.extend && isBackward) {
121 var tempKey = anchorKey;
122 var tempOffset = anchorOffset;
123 anchorKey = focusKey;
124 anchorOffset = focusOffset;
125 focusKey = tempKey;
126 focusOffset = tempOffset;
127 isBackward = false;
128 }
129
130 var hasAnchor = anchorKey === blockKey && nodeStart <= anchorOffset && nodeEnd >= anchorOffset;
131 var hasFocus = focusKey === blockKey && nodeStart <= focusOffset && nodeEnd >= focusOffset; // If the selection is entirely bound within this node, set the selection
132 // and be done.
133
134 if (hasAnchor && hasFocus) {
135 selection.removeAllRanges();
136 addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
137 addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
138 return;
139 }
140
141 if (!isBackward) {
142 // If the anchor is within this node, set the range start.
143 if (hasAnchor) {
144 selection.removeAllRanges();
145 addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
146 } // If the focus is within this node, we can assume that we have
147 // already set the appropriate start range on the selection, and
148 // can simply extend the selection.
149
150
151 if (hasFocus) {
152 addFocusToSelection(selection, node, focusOffset - nodeStart, selectionState);
153 }
154 } else {
155 // If this node has the focus, set the selection range to be a
156 // collapsed range beginning here. Later, when we encounter the anchor,
157 // we'll use this information to extend the selection.
158 if (hasFocus) {
159 selection.removeAllRanges();
160 addPointToSelection(selection, node, focusOffset - nodeStart, selectionState);
161 } // If this node has the anchor, we may assume that the correct
162 // focus information is already stored on the selection object.
163 // We keep track of it, reset the selection range, and extend it
164 // back to the focus point.
165
166
167 if (hasAnchor) {
168 var storedFocusNode = selection.focusNode;
169 var storedFocusOffset = selection.focusOffset;
170 selection.removeAllRanges();
171 addPointToSelection(selection, node, anchorOffset - nodeStart, selectionState);
172 addFocusToSelection(selection, storedFocusNode, storedFocusOffset, selectionState);
173 }
174 }
175}
176/**
177 * Extend selection towards focus point.
178 */
179
180
181function addFocusToSelection(selection, node, offset, selectionState) {
182 var activeElement = getActiveElement();
183 var extend = selection.extend; // containsNode returns false if node is null.
184 // Let's refine the type of this value out here so flow knows.
185
186 if (extend && node != null && containsNode(activeElement, node)) {
187 // If `extend` is called while another element has focus, an error is
188 // thrown. We therefore disable `extend` if the active element is somewhere
189 // other than the node we are selecting. This should only occur in Firefox,
190 // since it is the only browser to support multiple selections.
191 // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444.
192 // logging to catch bug that is being reported in t16250795
193 if (offset > getNodeLength(node)) {
194 // the call to 'selection.extend' is about to throw
195 DraftJsDebugLogging.logSelectionStateFailure({
196 anonymizedDom: getAnonymizedEditorDOM(node),
197 extraParams: JSON.stringify({
198 offset: offset
199 }),
200 selectionState: JSON.stringify(selectionState.toJS())
201 });
202 } // logging to catch bug that is being reported in t18110632
203
204
205 var nodeWasFocus = node === selection.focusNode;
206
207 try {
208 // Fixes some reports of "InvalidStateError: Failed to execute 'extend' on
209 // 'Selection': This Selection object doesn't have any Ranges."
210 // Note: selection.extend does not exist in IE.
211 if (selection.rangeCount > 0 && selection.extend) {
212 selection.extend(node, offset);
213 }
214 } catch (e) {
215 DraftJsDebugLogging.logSelectionStateFailure({
216 anonymizedDom: getAnonymizedEditorDOM(node, function (n) {
217 var labels = [];
218
219 if (n === activeElement) {
220 labels.push('active element');
221 }
222
223 if (n === selection.anchorNode) {
224 labels.push('selection anchor node');
225 }
226
227 if (n === selection.focusNode) {
228 labels.push('selection focus node');
229 }
230
231 return labels;
232 }),
233 extraParams: JSON.stringify({
234 activeElementName: activeElement ? activeElement.nodeName : null,
235 nodeIsFocus: node === selection.focusNode,
236 nodeWasFocus: nodeWasFocus,
237 selectionRangeCount: selection.rangeCount,
238 selectionAnchorNodeName: selection.anchorNode ? selection.anchorNode.nodeName : null,
239 selectionAnchorOffset: selection.anchorOffset,
240 selectionFocusNodeName: selection.focusNode ? selection.focusNode.nodeName : null,
241 selectionFocusOffset: selection.focusOffset,
242 message: e ? '' + e : null,
243 offset: offset
244 }, null, 2),
245 selectionState: JSON.stringify(selectionState.toJS(), null, 2)
246 }); // allow the error to be thrown -
247 // better than continuing in a broken state
248
249 throw e;
250 }
251 } else {
252 // IE doesn't support extend. This will mean no backward selection.
253 // Extract the existing selection range and add focus to it.
254 // Additionally, clone the selection range. IE11 throws an
255 // InvalidStateError when attempting to access selection properties
256 // after the range is detached.
257 if (node && selection.rangeCount > 0) {
258 var range = selection.getRangeAt(0);
259 range.setEnd(node, offset);
260 selection.addRange(range.cloneRange());
261 }
262 }
263}
264
265function addPointToSelection(selection, node, offset, selectionState) {
266 var range = getCorrectDocumentFromNode(node).createRange(); // logging to catch bug that is being reported in t16250795
267
268 if (offset > getNodeLength(node)) {
269 // in this case we know that the call to 'range.setStart' is about to throw
270 DraftJsDebugLogging.logSelectionStateFailure({
271 anonymizedDom: getAnonymizedEditorDOM(node),
272 extraParams: JSON.stringify({
273 offset: offset
274 }),
275 selectionState: JSON.stringify(selectionState.toJS())
276 });
277 DraftEffects.handleExtensionCausedError();
278 }
279
280 range.setStart(node, offset); // IE sometimes throws Unspecified Error when trying to addRange
281
282 if (isIE) {
283 try {
284 selection.addRange(range);
285 } catch (e) {
286 if (process.env.NODE_ENV !== "production") {
287 /* eslint-disable-next-line no-console */
288 console.warn('Call to selection.addRange() threw exception: ', e);
289 }
290 }
291 } else {
292 selection.addRange(range);
293 }
294}
295
296module.exports = {
297 setDraftEditorSelection: setDraftEditorSelection,
298 addFocusToSelection: addFocusToSelection
299};
\No newline at end of file