Skip to content

Instantly share code, notes, and snippets.

@bwg
Last active December 18, 2015 15:39
Show Gist options
  • Save bwg/5806242 to your computer and use it in GitHub Desktop.
Save bwg/5806242 to your computer and use it in GitHub Desktop.
cross browser double click selection/range fun
// Browsers behave differently when dbl clicking to select
// a range.
//
// Chrome and IE use the text node that contains the selected text
// as the startContainer with an offset to exclude any whitespace
// characters at the start of the node.
//
// Firefox will use a text node *before* the selected text
// as the startContainer, with a positive offset set to the end
// of the node. If there is no previous sibling of the selected text
// or the previous sibling is not a text node, the node containing
// the selected text is used as the startContainer with a positive
// offset to exclude whitespace at the start of the node.
//
// Chrome uses the same text node as the startContainer for the
// endContainer, with a positive offset to exclude whitespace
// at the end of the node
//
// Firefox follows the mostly the same rules for the endContainer
// as it does for the startContainer. Any sibling text node
// *after* the selected text will be used as the endContainer,
// but with a 0 offset to exclude whitespace. If there is no next
// sibling or the next sibling is not a text node, the endContainer
// will be the same as the startContainer, with a positive offset
// to exclude any whitespace at the end of the node.
//
// IE will aways use a following text node as the endContainer,
// even if it is a child of a non-text sibling. The offset will
// include any whitespace characters at the beginning of the node.
// If there is no following text node, the endContainer will be the
// same as the startContainer, with a positive offset to include all
// contiguous whitespace characters.
//
// Examples:
// [] = startContainer, {} = endContainer, s:e = start/end offset
//
// given the snippet `Lorem <b>Ipsum</b> Dolor Sit`,
//
// dbl clicking to select the text `Lorem`
// Chrome: `[{Lorem }]<b>Ipsum</b> Dolor Sit` s0:e5
// Firefox: `[{Lorem }]<b>Ipsum</b> Dolor Sit` s0:e5
// IE: `[Lorem ]<b>{Ipsum}</b> Dolor Sit` s0:e0
//
// dbl clicking to select the text `Ipsum`
// Chrome: `Lorem <b>[{Ipsum}]</b> Dolor Sit` s0:e5
// Firefox: `[Lorem ]<b>Ipsum</b>{ Dolor Sit}` s6:e0
// IE: `Lorem <b>[Ipsum]</b>{ Dolor Sit}` s0:e1
//
// dbl clicking to select the text `Dolor`
// Chrome: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s1:e6
// Firefox: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s1:e6
// IE: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s1:e7
//
// dbl clicking to select the text `Sit`
// Chrome: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s7:e10
// Firefox: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s7:e10
// IE: `Lorem <b>Ipsum</b>[{ Dolor Sit}]` s7:e10
//
// Unable to find any specification on what the `correct` behavior
// is, but Chrome seems to be the most logical, so we'll use that
// as the target behavior for normalization.
var startNode = range.startNode(),
startOffset = range.startOffset(),
endNode = range.endNode(),
endOffset = range.endOffset();
// If the startOffset is at the end of the startContainer
// assume the nextSibling contains the selected text (Firefox)
if (startNode.get('text').length === startOffset) {
startNode = range.startNode(startNode.get('nextSibling'));
}
// If the trimmed text of the endContainer is an empty string
// assume the previousSibling contains the selected text (IE, Firefox)
if (0 === Y.Lang.trim(endNode.get('text').substring(0, endOffset)).length) {
// IE will put the endNode in a child of a sibling node, so use the
// startNode if the current endNode doesn't have a previous sibling
endNode = range.endNode(endNode.get('previousSibling') || startNode);
// set the endOffset based on the node type
if (1 === endNode.get('nodeType')) {
endOffset = range.endOffset(endNode.get('childNodes').size());
} else {
endOffset = range.endOffset(endNode.get('text').length);
}
}
// In most cases (Firefox being the sometimes exception),
// startContainer will already be the correct text node. If
// startContainer is an element node, unset it so the traverse
// function can set it.
if (1 === startNode.get('nodeType')) {
startNode = null;
}
// traverse the range and find the text nodes we want
// to use for the start/end containers
range.traverse(function(node) {
var text = node.get('text');
if (!text.length || 1 === node.get('nodeType')) {
// don't want empty or element nodes
return;
}
if (!startNode) {
startNode = node;
// set the offset to exclude leading whitespace
startOffset = text.length - Y.Lang.trimLeft(text).length;
}
endNode = node;
// set the endOffset to the first occurrence of whitespace
// after the startOffset
endOffset = text.indexOf(' ', startOffset);
if (-1 === endOffset) {
// no whitespace after the startOffset, use the whole
// length of the text as the endOffset
endOffset = text.length;
}
});
range.startNode(startNode, startOffset);
range.endNode(endNode, endOffset);
this.selection.select(range);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment