Browsers treat these tools a little bit differently and I want to build something that I can use to more easily manage selections. The area of the page which the user has selected, and to select areas on their behalf.
Important attributes of a Range
object are startContainer
, endContainer
, startOffset
, and endOffset
with these attributes we can figure out what on the page is selected and even manipulate that selection. Containers denote the node that the selection starts and ends within. Offsets tell us how many characters to skip.
Pretend the following methods are enclosed inside a nice namespace.
export function getSelection (): Selection
{
if (window.getSelection)
return window.getSelection();
else if (document.getSelection)
return document.getSelection();
}
Note: In this article we are not supporting IE8, if you are there is quite an extensive library called rangy which you should look at.
The standards compliant way to retrieve the current page selection is with getSelection. The Selection
object returned by this method doesn't give us all of the information we need by itself.
export function getRange (element?: HTMLElement): Range
{
let selection = getSelection();
if (!selection)
return;
let range: Range;
if (selection.getRangeAt)
range = selection.getRangeAt(0);
else {
range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
}
if (!element)
return range;
if (element.contains(range.startContainer) && element.contains(range.endContainer))
return range;
}
Safari can behave a little bit strangely so in some cases it is necessary to build our Range object manually. Now we have the area which is selected on the page.
I've added the ability to only return the selection if it exists within a specific element. If the provided element contains both container nodes then we know that the a range is from within a specific area.
export function setRange (range?: Range)
{
let selection = getSelection();
if (!selection)
return;
selection.removeAllRanges();
if (range)
selection.addRange(range);
}
This will select the area of the page you define within a range. The anatomy of a Range
object as stated earlier, is the container and offset at both the start and end of the area. Keep in mind we want to use textnodes, there is a bit of difference between a div element, and the textnode or other nodes it may contain. It is often necessary for example to use the element's firstChild
attribute when defining Range
.
interface IRangePosition
{
node: Node;
offset: number;
}
export function createRange (positionStart: IRangePosition, positionEnd?: IRangePosition): Range
{
if (!positionStart)
return;
let range = document.createRange();
range.setStart(positionStart.node, positionStart.offset);
if (positionEnd)
range.setEnd(positionEnd.node, positionEnd.offset);
else
range.collapse(true);
return range;
}
This helper will assist in creating ranges from plain key value pairs.
A range which is "collapsed" is simply a cursor position, which will be useful in cases where you are working with contenteditable divs.
In addition it might be useful to ignore everything to do with divs and find an IRangePosition
based on a character index. If you can imagine the following function finds a Node
and offset for you which you can use to create a range.
export function findPosition (parent: Node, index?: number): IRangePosition
{
if (!index && index != 0)
return;
let position = 0;
for (let i = 0; i < parent.childNodes.length; i++) {
let node = parent.childNodes.item(i);
let length = node.textContent.length;
if (position + length >= index) {
if (node.nodeType == 3)
return { node: node, offset: index - position };
else
return findPosition(node, index - position);
}
position += length;
}
return undefined;
}
We walk through the tree regularly checking the length of the text content in each childNode
in order to pinpoint the correct node and offset within it.
This will work as a starting point towards normalising selections on your page.