UNPKG

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