211 lines
6.6 KiB
JavaScript
211 lines
6.6 KiB
JavaScript
|
/**
|
||
|
* Copyright 2013-present, Facebook, Inc.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* This source code is licensed under the BSD-style license found in the
|
||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
|
||
|
|
||
|
var getNodeForCharacterOffset = require('./getNodeForCharacterOffset');
|
||
|
var getTextContentAccessor = require('./getTextContentAccessor');
|
||
|
|
||
|
/**
|
||
|
* While `isCollapsed` is available on the Selection object and `collapsed`
|
||
|
* is available on the Range object, IE11 sometimes gets them wrong.
|
||
|
* If the anchor/focus nodes and offsets are the same, the range is collapsed.
|
||
|
*/
|
||
|
function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
|
||
|
return anchorNode === focusNode && anchorOffset === focusOffset;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the appropriate anchor and focus node/offset pairs for IE.
|
||
|
*
|
||
|
* The catch here is that IE's selection API doesn't provide information
|
||
|
* about whether the selection is forward or backward, so we have to
|
||
|
* behave as though it's always forward.
|
||
|
*
|
||
|
* IE text differs from modern selection in that it behaves as though
|
||
|
* block elements end with a new line. This means character offsets will
|
||
|
* differ between the two APIs.
|
||
|
*
|
||
|
* @param {DOMElement} node
|
||
|
* @return {object}
|
||
|
*/
|
||
|
function getIEOffsets(node) {
|
||
|
var selection = document.selection;
|
||
|
var selectedRange = selection.createRange();
|
||
|
var selectedLength = selectedRange.text.length;
|
||
|
|
||
|
// Duplicate selection so we can move range without breaking user selection.
|
||
|
var fromStart = selectedRange.duplicate();
|
||
|
fromStart.moveToElementText(node);
|
||
|
fromStart.setEndPoint('EndToStart', selectedRange);
|
||
|
|
||
|
var startOffset = fromStart.text.length;
|
||
|
var endOffset = startOffset + selectedLength;
|
||
|
|
||
|
return {
|
||
|
start: startOffset,
|
||
|
end: endOffset
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {DOMElement} node
|
||
|
* @return {?object}
|
||
|
*/
|
||
|
function getModernOffsets(node) {
|
||
|
var selection = window.getSelection && window.getSelection();
|
||
|
|
||
|
if (!selection || selection.rangeCount === 0) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
var anchorNode = selection.anchorNode;
|
||
|
var anchorOffset = selection.anchorOffset;
|
||
|
var focusNode = selection.focusNode;
|
||
|
var focusOffset = selection.focusOffset;
|
||
|
|
||
|
var currentRange = selection.getRangeAt(0);
|
||
|
|
||
|
// In Firefox, range.startContainer and range.endContainer can be "anonymous
|
||
|
// divs", e.g. the up/down buttons on an <input type="number">. Anonymous
|
||
|
// divs do not seem to expose properties, triggering a "Permission denied
|
||
|
// error" if any of its properties are accessed. The only seemingly possible
|
||
|
// way to avoid erroring is to access a property that typically works for
|
||
|
// non-anonymous divs and catch any error that may otherwise arise. See
|
||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=208427
|
||
|
try {
|
||
|
/* eslint-disable no-unused-expressions */
|
||
|
currentRange.startContainer.nodeType;
|
||
|
currentRange.endContainer.nodeType;
|
||
|
/* eslint-enable no-unused-expressions */
|
||
|
} catch (e) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// If the node and offset values are the same, the selection is collapsed.
|
||
|
// `Selection.isCollapsed` is available natively, but IE sometimes gets
|
||
|
// this value wrong.
|
||
|
var isSelectionCollapsed = isCollapsed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
|
||
|
|
||
|
var rangeLength = isSelectionCollapsed ? 0 : currentRange.toString().length;
|
||
|
|
||
|
var tempRange = currentRange.cloneRange();
|
||
|
tempRange.selectNodeContents(node);
|
||
|
tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
|
||
|
|
||
|
var isTempRangeCollapsed = isCollapsed(tempRange.startContainer, tempRange.startOffset, tempRange.endContainer, tempRange.endOffset);
|
||
|
|
||
|
var start = isTempRangeCollapsed ? 0 : tempRange.toString().length;
|
||
|
var end = start + rangeLength;
|
||
|
|
||
|
// Detect whether the selection is backward.
|
||
|
var detectionRange = document.createRange();
|
||
|
detectionRange.setStart(anchorNode, anchorOffset);
|
||
|
detectionRange.setEnd(focusNode, focusOffset);
|
||
|
var isBackward = detectionRange.collapsed;
|
||
|
|
||
|
return {
|
||
|
start: isBackward ? end : start,
|
||
|
end: isBackward ? start : end
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {DOMElement|DOMTextNode} node
|
||
|
* @param {object} offsets
|
||
|
*/
|
||
|
function setIEOffsets(node, offsets) {
|
||
|
var range = document.selection.createRange().duplicate();
|
||
|
var start, end;
|
||
|
|
||
|
if (offsets.end === undefined) {
|
||
|
start = offsets.start;
|
||
|
end = start;
|
||
|
} else if (offsets.start > offsets.end) {
|
||
|
start = offsets.end;
|
||
|
end = offsets.start;
|
||
|
} else {
|
||
|
start = offsets.start;
|
||
|
end = offsets.end;
|
||
|
}
|
||
|
|
||
|
range.moveToElementText(node);
|
||
|
range.moveStart('character', start);
|
||
|
range.setEndPoint('EndToStart', range);
|
||
|
range.moveEnd('character', end - start);
|
||
|
range.select();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* In modern non-IE browsers, we can support both forward and backward
|
||
|
* selections.
|
||
|
*
|
||
|
* Note: IE10+ supports the Selection object, but it does not support
|
||
|
* the `extend` method, which means that even in modern IE, it's not possible
|
||
|
* to programmatically create a backward selection. Thus, for all IE
|
||
|
* versions, we use the old IE API to create our selections.
|
||
|
*
|
||
|
* @param {DOMElement|DOMTextNode} node
|
||
|
* @param {object} offsets
|
||
|
*/
|
||
|
function setModernOffsets(node, offsets) {
|
||
|
if (!window.getSelection) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var selection = window.getSelection();
|
||
|
var length = node[getTextContentAccessor()].length;
|
||
|
var start = Math.min(offsets.start, length);
|
||
|
var end = offsets.end === undefined ? start : Math.min(offsets.end, length);
|
||
|
|
||
|
// IE 11 uses modern selection, but doesn't support the extend method.
|
||
|
// Flip backward selections, so we can set with a single range.
|
||
|
if (!selection.extend && start > end) {
|
||
|
var temp = end;
|
||
|
end = start;
|
||
|
start = temp;
|
||
|
}
|
||
|
|
||
|
var startMarker = getNodeForCharacterOffset(node, start);
|
||
|
var endMarker = getNodeForCharacterOffset(node, end);
|
||
|
|
||
|
if (startMarker && endMarker) {
|
||
|
var range = document.createRange();
|
||
|
range.setStart(startMarker.node, startMarker.offset);
|
||
|
selection.removeAllRanges();
|
||
|
|
||
|
if (start > end) {
|
||
|
selection.addRange(range);
|
||
|
selection.extend(endMarker.node, endMarker.offset);
|
||
|
} else {
|
||
|
range.setEnd(endMarker.node, endMarker.offset);
|
||
|
selection.addRange(range);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && !('getSelection' in window);
|
||
|
|
||
|
var ReactDOMSelection = {
|
||
|
/**
|
||
|
* @param {DOMElement} node
|
||
|
*/
|
||
|
getOffsets: useIEOffsets ? getIEOffsets : getModernOffsets,
|
||
|
|
||
|
/**
|
||
|
* @param {DOMElement|DOMTextNode} node
|
||
|
* @param {object} offsets
|
||
|
*/
|
||
|
setOffsets: useIEOffsets ? setIEOffsets : setModernOffsets
|
||
|
};
|
||
|
|
||
|
module.exports = ReactDOMSelection;
|