Last active
November 4, 2017 04:31
-
-
Save jmakeig/c3dca6699b237817dfb2ba8f31db8fc8 to your computer and use it in GitHub Desktop.
Create a selectable Range from text offsets, flattening child nodes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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