mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add ReactDOMSelection module
Add a DOM based selection module. This can both be used on its own and in ReactInputSelection where our current implementation is lacking. There is still some inconsistency with how IE and modern browsers handle block nodes. Should be OK if we are just getting and setting and not trying to set selection based on character offsets.
This commit is contained in:
committed by
Paul O’Shannessy
parent
d27746ee0b
commit
8f15eea910
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactDOMSelection
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var getNodeForCharacterOffset = require('getNodeForCharacterOffset');
|
||||
var getTextContentAccessor = require('getTextContentAccessor');
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function getIESelection(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
|
||||
*/
|
||||
function getModernSelection(node) {
|
||||
var selection = global.getSelection();
|
||||
var anchorNode = selection.anchorNode;
|
||||
var anchorOffset = selection.anchorOffset;
|
||||
var focusNode = selection.focusNode;
|
||||
var focusOffset = selection.focusOffset;
|
||||
|
||||
var currentRange = selection.getRangeAt(0);
|
||||
var rangeLength = currentRange.toString().length;
|
||||
|
||||
var tempRange = currentRange.cloneRange();
|
||||
tempRange.selectNodeContents(node);
|
||||
tempRange.setEnd(currentRange.startContainer, currentRange.startOffset);
|
||||
|
||||
var start = 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;
|
||||
detectionRange.detach();
|
||||
|
||||
return {
|
||||
start: isBackward ? end : start,
|
||||
end: isBackward ? start : end
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement|DOMTextNode} node
|
||||
* @param {object} offsets
|
||||
*/
|
||||
function setIESelection(node, offsets) {
|
||||
var range = document.selection.createRange().duplicate();
|
||||
var start, end;
|
||||
|
||||
if (typeof 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 programatically 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 setModernSelection(node, offsets) {
|
||||
var selection = global.getSelection();
|
||||
|
||||
var length = node[getTextContentAccessor()].length;
|
||||
var start = Math.min(offsets.start, length);
|
||||
var end = typeof offsets.end === 'undefined' ?
|
||||
start : Math.min(offsets.end, length);
|
||||
|
||||
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();
|
||||
selection.addRange(range);
|
||||
selection.extend(endMarker.node, endMarker.offset);
|
||||
range.detach();
|
||||
}
|
||||
}
|
||||
|
||||
var ReactDOMSelection = {
|
||||
/**
|
||||
* @param {DOMElement} node
|
||||
*/
|
||||
get: document.selection ? getIESelection : getModernSelection,
|
||||
|
||||
/**
|
||||
* @param {DOMElement|DOMTextNode} node
|
||||
* @param {object} offsets
|
||||
*/
|
||||
set: document.selection ? setIESelection : setModernSelection
|
||||
};
|
||||
|
||||
module.exports = ReactDOMSelection;
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var getTextContentAccessor = require('getTextContentAccessor');
|
||||
var ReactDOMSelection = require('ReactDOMSelection');
|
||||
|
||||
// It is not safe to read the document.activeElement property in IE if there's
|
||||
// nothing focused.
|
||||
@@ -88,103 +88,59 @@ var ReactInputSelection = {
|
||||
* -@return {start: selectionStart, end: selectionEnd}
|
||||
*/
|
||||
getSelection: function(input) {
|
||||
var range;
|
||||
if (input.contentEditable === 'true' && window.getSelection) {
|
||||
var selection = window.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
range = selection.getRangeAt(0);
|
||||
var commonAncestor = range.commonAncestorContainer;
|
||||
if (commonAncestor && commonAncestor.nodeType === 3) {
|
||||
commonAncestor = commonAncestor.parentNode;
|
||||
}
|
||||
if (commonAncestor === input) {
|
||||
return {start: range.startOffset, end: range.endOffset};
|
||||
}
|
||||
var selection;
|
||||
|
||||
if ('selectionStart' in input) {
|
||||
// Modern browser with input or textarea.
|
||||
selection = {
|
||||
start: input.selectionStart,
|
||||
end: input.selectionEnd
|
||||
};
|
||||
} else if (document.selection && input.nodeName === 'INPUT') {
|
||||
// IE8 input.
|
||||
var range = document.selection.createRange();
|
||||
// There can only be one selection per document in IE, so it must
|
||||
// be in our element.
|
||||
if (range.parentElement() === input) {
|
||||
selection = {
|
||||
start: -range.moveStart('character', -input.value.length),
|
||||
end: -range.moveEnd('character', -input.value.length)
|
||||
};
|
||||
}
|
||||
return {start: 0, end: 0};
|
||||
}
|
||||
|
||||
if (!document.selection) {
|
||||
// Mozilla, Safari, etc.
|
||||
return {start: input.selectionStart, end: input.selectionEnd};
|
||||
}
|
||||
|
||||
range = document.selection.createRange();
|
||||
if (range.parentElement() !== input) {
|
||||
// There can only be one selection per document in IE, so if the
|
||||
// containing element of the document's selection isn't our text field,
|
||||
// our text field must have no selection.
|
||||
return {start: 0, end: 0};
|
||||
}
|
||||
|
||||
var value = input.value || input[getTextContentAccessor()];
|
||||
var length = value.length;
|
||||
|
||||
if (input.nodeName === 'INPUT') {
|
||||
return {
|
||||
start: -range.moveStart('character', -length),
|
||||
end: -range.moveEnd('character', -length)
|
||||
};
|
||||
} else {
|
||||
var range2 = range.duplicate();
|
||||
range2.moveToElementText(input);
|
||||
range2.setEndPoint('StartToEnd', range);
|
||||
var end = length - range2.text.length;
|
||||
range2.setEndPoint('StartToStart', range);
|
||||
return {
|
||||
start: length - range2.text.length,
|
||||
end: end
|
||||
};
|
||||
// Content editable or old IE textarea.
|
||||
selection = ReactDOMSelection.get(input);
|
||||
}
|
||||
|
||||
return selection || {start: 0, end: 0};
|
||||
},
|
||||
|
||||
/**
|
||||
* @setSelection: Sets the selection bounds of a textarea or input and focuses
|
||||
* the input.
|
||||
* -@input Set selection bounds of this input or textarea
|
||||
* -@rangeObj Object of same form that is returned from get*
|
||||
* -@offsets Object of same form that is returned from get*
|
||||
*/
|
||||
setSelection: function(input, rangeObj) {
|
||||
var range;
|
||||
var start = rangeObj.start;
|
||||
var end = rangeObj.end;
|
||||
setSelection: function(input, offsets) {
|
||||
var start = offsets.start;
|
||||
var end = offsets.end;
|
||||
if (typeof end === 'undefined') {
|
||||
end = start;
|
||||
}
|
||||
if (document.selection) {
|
||||
// IE is inconsistent about character offsets when it comes to carriage
|
||||
// returns, so we need to manually take them into account
|
||||
if (input.tagName === 'TEXTAREA') {
|
||||
var cr_before =
|
||||
(input.value.slice(0, start).match(/\r/g) || []).length;
|
||||
var cr_inside =
|
||||
(input.value.slice(start, end).match(/\r/g) || []).length;
|
||||
start -= cr_before;
|
||||
end -= cr_before + cr_inside;
|
||||
}
|
||||
range = input.createTextRange();
|
||||
|
||||
if ('selectionStart' in input) {
|
||||
input.selectionStart = start;
|
||||
input.selectionEnd = Math.min(end, input.value.length);
|
||||
} else if (document.selection && input.nodeName === 'INPUT') {
|
||||
var range = input.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveStart('character', start);
|
||||
range.moveEnd('character', end - start);
|
||||
range.select();
|
||||
} else {
|
||||
if (input.contentEditable === 'true') {
|
||||
if (input.childNodes.length === 1) {
|
||||
range = document.createRange();
|
||||
range.setStart(input.childNodes[0], start);
|
||||
range.setEnd(input.childNodes[0], end);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
} else {
|
||||
input.selectionStart = start;
|
||||
input.selectionEnd = Math.min(end, input.value.length);
|
||||
input.focus();
|
||||
}
|
||||
ReactDOMSelection.set(input, offsets);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactInputSelection;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var getTestDocument = require('getTestDocument');
|
||||
|
||||
var getNodeForCharacterOffset = require('getNodeForCharacterOffset');
|
||||
|
||||
// Create node from HTML string
|
||||
function createNode(html) {
|
||||
var node = (getTestDocument() || document).createElement('div');
|
||||
node.innerHTML = html;
|
||||
return node;
|
||||
}
|
||||
|
||||
// Check getNodeForCharacterOffset return value matches expected result.
|
||||
function expectNodeOffset(result, textContent, nodeOffset) {
|
||||
expect(result.node.textContent).toBe(textContent);
|
||||
expect(result.offset).toBe(nodeOffset);
|
||||
}
|
||||
|
||||
describe('getNodeForCharacterOffset', function() {
|
||||
it('should handle siblings', function() {
|
||||
var node = createNode('<i>123</i><i>456</i><i>789</i>');
|
||||
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 0), '123', 0);
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 4), '456', 1);
|
||||
});
|
||||
|
||||
it('should handle trailing chars', function() {
|
||||
var node = createNode('<i>123</i><i>456</i><i>789</i>');
|
||||
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 3), '123', 3);
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 9), '789', 3);
|
||||
});
|
||||
|
||||
it('should handle trees', function() {
|
||||
var node = createNode(
|
||||
'<i>' +
|
||||
'<i>1</i>' +
|
||||
'<i>' +
|
||||
'<i>' +
|
||||
'<i>2</i>' +
|
||||
'<i></i>' +
|
||||
'</i>' +
|
||||
'</i>' +
|
||||
'<i>' +
|
||||
'3' +
|
||||
'<i>45</i>' +
|
||||
'</i>' +
|
||||
'</i>'
|
||||
);
|
||||
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 3), '3', 1);
|
||||
expectNodeOffset(getNodeForCharacterOffset(node, 5), '45', 2);
|
||||
expect(getNodeForCharacterOffset(node, 10)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle non-existent offset', function() {
|
||||
var node = createNode('<i>123</i>');
|
||||
|
||||
expect(getNodeForCharacterOffset(node, -1)).toBeUndefined();
|
||||
expect(getNodeForCharacterOffset(node, 4)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule getNodeForCharacterOffset
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Given any node return the first leaf node without children.
|
||||
*
|
||||
* @param {DOMElement|DOMTextNode} node
|
||||
* @return {DOMElement|DOMTextNode}
|
||||
*/
|
||||
function getLeafNode(node) {
|
||||
while (node && node.firstChild) {
|
||||
node = node.firstChild;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next sibling within a container. This will walk up the
|
||||
* DOM if a node's siblings have been exhausted.
|
||||
*
|
||||
* @param {DOMElement|DOMTextNode} node
|
||||
* @return {?DOMElement|DOMTextNode}
|
||||
*/
|
||||
function getSiblingNode(node) {
|
||||
while (node) {
|
||||
if (node.nextSibling) {
|
||||
return node.nextSibling;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object describing the nodes which contain characters at offset.
|
||||
*
|
||||
* @param {DOMElement|DOMTextNode} root
|
||||
* @param {number} offset
|
||||
* @return {?object}
|
||||
*/
|
||||
function getNodeForCharacterOffset(root, offset) {
|
||||
var node = getLeafNode(root);
|
||||
var nodeStart = 0;
|
||||
var nodeEnd = 0;
|
||||
|
||||
while (node) {
|
||||
if (node.nodeType == 3) {
|
||||
nodeEnd = nodeStart + node.textContent.length;
|
||||
|
||||
if (nodeStart <= offset && nodeEnd >= offset) {
|
||||
return {
|
||||
node: node,
|
||||
offset: offset - nodeStart
|
||||
};
|
||||
}
|
||||
|
||||
nodeStart = nodeEnd;
|
||||
}
|
||||
|
||||
node = getLeafNode(getSiblingNode(node));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getNodeForCharacterOffset;
|
||||
Reference in New Issue
Block a user