Skip to content

Instantly share code, notes, and snippets.

@jmakeig
Last active November 4, 2017 04:31
Show Gist options
  • Select an option

  • Save jmakeig/c3dca6699b237817dfb2ba8f31db8fc8 to your computer and use it in GitHub Desktop.

Select an option

Save jmakeig/c3dca6699b237817dfb2ba8f31db8fc8 to your computer and use it in GitHub Desktop.
Create a selectable Range from text offsets, flattening child nodes.
<div id="L1">abcdef<span>gh<span>ijk</span>lmn</span>opqrstuvwxyz0123456789</div>
<div id="L2">ab<span>cdefgh</span>ijk<span>lmnopqrstuvwxyz01234567</span>89</div>
<div id="L3">abcdefghijklmnopqrst<span>uvwxyz0123456789</span></div>
<button id="Range">Range me!</button>
<button id="Select">Select me!</button>
let tmpRange;
document.querySelector('button#Range').addEventListener('click', evt => {
const selection = window.getSelection();
tmpRange = getRange(selection);
console.log(tmpRange);
});
document.querySelector('button#Select').addEventListener('click', evt => {
const range = rangeFromOffsets(
document.querySelector(`div#L${tmpRange.start.row}`),
tmpRange.start.column - 1,
document.querySelector(`div#L${tmpRange.end.row}`),
tmpRange.end.column - 1
);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
function textOffsetFromNode(parent, child, childOffset = 0) {
if (!parent) return;
if (!child) return offset;
const iter = document.createNodeIterator(parent, NodeFilter.SHOW_TEXT);
let node;
let offset = 0;
while (iter.nextNode()) {
node = iter.referenceNode;
if (node === child) {
return offset + childOffset;
}
if (Node.TEXT_NODE === node.nodeType) {
offset += node.textContent.length;
}
}
throw new Error(
`Couldn’t find ${String(child)} as a child of ${String(parent)}`
);
}
function getLineNumber(node, matcher = 'div') {
return parseInt(getLine(node, matcher).id.slice(1), 10);
}
function getLine(node, matcher = 'div') {
do {
if (node.matches && node.matches(matcher)) {
return node;
}
} while ((node = node.parentNode));
return undefined;
}
/**
* Given a `Selection`, determine the `Range`, where
* `start` is always before `end`, regardless
* from which direction the selection was made.
*
* @param {Selection} selection
* @returns {Object}
*/
function getRange(selection) {
if (!selection) return;
if (!(selection instanceof Selection))
throw new TypeError(String(selection.constructor.name));
const anchor = {
row: getLineNumber(selection.anchorNode),
column:
textOffsetFromNode(
getLine(selection.anchorNode),
selection.anchorNode,
selection.anchorOffset
) + 1,
};
const focus = {
row: getLineNumber(selection.focusNode),
column:
textOffsetFromNode(
getLine(selection.focusNode),
selection.focusNode,
selection.focusOffset
) + 1,
};
if (
anchor.row < focus.row ||
(anchor.row === focus.row && anchor.column <= focus.column)
) {
return {
start: anchor,
end: focus,
};
} else {
return {
start: focus,
end: anchor,
};
}
}
/**
*
* @param {Node} parentStart
* @param {number} start
* @param {Node} parentEnd
* @param {number} end
* @return {Range}
*/
function rangeFromOffsets(
parentStart,
start = 0,
parentEnd = parentStart,
end = 0
) {
const range = document.createRange();
const s = nodeFromTextOffset(parentStart, start);
const e = nodeFromTextOffset(parentEnd, end);
range.setStart(childTextNodeOrSelf(s.node), s.offset);
range.setEnd(childTextNodeOrSelf(e.node), e.offset);
return range;
}
/**
*
* @param {Node} parent
* @param {number} offset
* @return {Object} - `{ node: Node, offset: number }`
*/
function nodeFromTextOffset(parent, offset = 0) {
if (!parent) return;
const iter = document.createNodeIterator(parent, NodeFilter.SHOW_TEXT);
let counter = 0;
let node;
let last;
// Find the start node (could we somehow skip this seemingly needless search?)
while (counter < offset && iter.nextNode()) {
node = iter.referenceNode;
if (node.nodeType === Node.TEXT_NODE) {
last = offset - counter;
counter += node.textContent.length;
}
}
return { node: node, offset: last };
}
/**
* Descendent-or-self until you get a `TextNode`
*
* @param {Node} node
* @return {TextNode} - Or `undefined` if there are not text
* children, e.g. `<br/>`
*/
function childTextNodeOrSelf(node) {
if (!node) return;
if (!(node instanceof Node)) throw new TypeError(node.constructor.name);
if (Node.TEXT_NODE === node.nodeType) {
return node;
}
if (node.firstChild) {
return childTextNodeOrSelf(node.firstChild);
}
return undefined;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment