UNPKG

6.65 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2013-present, Facebook, Inc.
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
9'use strict';
10
11var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
12
13var getNodeForCharacterOffset = require('./getNodeForCharacterOffset');
14var getTextContentAccessor = require('./getTextContentAccessor');
15
16/**
17 * While `isCollapsed` is available on the Selection object and `collapsed`
18 * is available on the Range object, IE11 sometimes gets them wrong.
19 * If the anchor/focus nodes and offsets are the same, the range is collapsed.
20 */
21function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
22 return anchorNode === focusNode && anchorOffset === focusOffset;
23}
24
25/**
26 * Get the appropriate anchor and focus node/offset pairs for IE.
27 *
28 * The catch here is that IE's selection API doesn't provide information
29 * about whether the selection is forward or backward, so we have to
30 * behave as though it's always forward.
31 *
32 * IE text differs from modern selection in that it behaves as though
33 * block elements end with a new line. This means character offsets will
34 * differ between the two APIs.
35 *
36 * @param {DOMElement} node
37 * @return {object}
38 */
39function getIEOffsets(node) {
40 var selection = document.selection;
41 var selectedRange = selection.createRange();
42 var selectedLength = selectedRange.text.length;
43
44 // Duplicate selection so we can move range without breaking user selection.
45 var fromStart = selectedRange.duplicate();
46 fromStart.moveToElementText(node);
47 fromStart.setEndPoint('EndToStart', selectedRange);
48
49 var startOffset = fromStart.text.length;
50 var endOffset = startOffset + selectedLength;
51
52 return {
53 start: startOffset,
54 end: endOffset
55 };
56}
57
58/**
59 * @param {DOMElement} node
60 * @return {?object}
61 */
62function getModernOffsets(node) {
63 var selection = window.getSelection && window.getSelection();
64
65 if (!selection || selection.rangeCount === 0) {
66 return null;
67 }
68
69 var anchorNode = selection.anchorNode;
70 var anchorOffset = selection.anchorOffset;
71 var focusNode = selection.focusNode;
72 var focusOffset = selection.focusOffset;
73
74 var currentRange = selection.getRangeAt(0);
75
76 // In Firefox, range.startContainer and range.endContainer can be "anonymous
77 // divs", e.g. the up/down buttons on an <input type="number">. Anonymous
78 // divs do not seem to expose properties, triggering a "Permission denied
79 // error" if any of its properties are accessed. The only seemingly possible
80 // way to avoid erroring is to access a property that typically works for
81 // non-anonymous divs and catch any error that may otherwise arise. See
82 // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
83 try {
84 /* eslint-disable no-unused-expressions */
85 currentRange.startContainer.nodeType;
86 currentRange.endContainer.nodeType;
87 /* eslint-enable no-unused-expressions */
88 } catch (e) {
89 return null;
90 }
91
92 // If the node and offset values are the same, the selection is collapsed.
93 // `Selection.isCollapsed` is available natively, but IE sometimes gets
94 // this value wrong.
95 var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
96
97 var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
98
99 var tempRange = currentRange.cloneRange();
100 tempRange.selectNodeContents(node);
101 tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
102
103 var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
104
105 var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
106 var end = start + rangeLength;
107
108 // Detect whether the selection is backward.
109 var detectionRange = document.createRange();
110 detectionRange.setStart(anchorNode, anchorOffset);
111 detectionRange.setEnd(focusNode, focusOffset);
112 var isBackward = detectionRange.collapsed;
113
114 return {
115 start: isBackward ? end : start,
116 end: isBackward ? start : end
117 };
118}
119
120/**
121 * @param {DOMElement|DOMTextNode} node
122 * @param {object} offsets
123 */
124function setIEOffsets(node, offsets) {
125 var range = document.selection.createRange().duplicate();
126 var start, end;
127
128 if (offsets.end === undefined) {
129 start = offsets.start;
130 end = start;
131 } else if (offsets.start > offsets.end) {
132 start = offsets.end;
133 end = offsets.start;
134 } else {
135 start = offsets.start;
136 end = offsets.end;
137 }
138
139 range.moveToElementText(node);
140 range.moveStart('character', start);
141 range.setEndPoint('EndToStart', range);
142 range.moveEnd('character', end - start);
143 range.select();
144}
145
146/**
147 * In modern non-IE browsers, we can support both forward and backward
148 * selections.
149 *
150 * Note: IE10+ supports the Selection object, but it does not support
151 * the `extend` method, which means that even in modern IE, it's not possible
152 * to programmatically create a backward selection. Thus, for all IE
153 * versions, we use the old IE API to create our selections.
154 *
155 * @param {DOMElement|DOMTextNode} node
156 * @param {object} offsets
157 */
158function setModernOffsets(node, offsets) {
159 if (!window.getSelection) {
160 return;
161 }
162
163 var selection = window.getSelection();
164 var length = node[getTextContentAccessor()].length;
165 var start = Math.min(offsets.start, length);
166 var end = offsets.end === undefined ? start : Math.min(offsets.end, length);
167
168 // IE 11 uses modern selection, but doesn't support the extend method.
169 // Flip backward selections, so we can set with a single range.
170 if (!selection.extend && start > end) {
171 var temp = end;
172 end = start;
173 start = temp;
174 }
175
176 var startMarker = getNodeForCharacterOffset(node, start);
177 var endMarker = getNodeForCharacterOffset(node, end);
178
179 if (startMarker && endMarker) {
180 var range = document.createRange();
181 range.setStart(startMarker.node, startMarker.offset);
182 selection.removeAllRanges();
183
184 if (start > end) {
185 selection.addRange(range);
186 selection.extend(endMarker.node, endMarker.offset);
187 } else {
188 range.setEnd(endMarker.node, endMarker.offset);
189 selection.addRange(range);
190 }
191 }
192}
193
194var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
195
196var ReactDOMSelection = {
197 /**
198 * @param {DOMElement} node
199 */
200 getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
201
202 /**
203 * @param {DOMElement|DOMTextNode} node
204 * @param {object} offsets
205 */
206 setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
207};
208
209module.exports = ReactDOMSelection;
\No newline at end of file