Skip to content

Instantly share code, notes, and snippets.

@HeimMatthias
Last active April 3, 2024 16:29
Show Gist options
  • Select an option

  • Save HeimMatthias/ddbb6661b2a8223126ca1f3afc894c74 to your computer and use it in GitHub Desktop.

Select an option

Save HeimMatthias/ddbb6661b2a8223126ca1f3afc894c74 to your computer and use it in GitHub Desktop.
Wraps the currently selected text in span-nodes, with support for Firefox's multiple selection feature, retains selection
/* see implementation example here: jsfiddle.net/s0etmpnf/ */
/* checks whether an (empty) text node is ignored by the browser or rendered */
function isTextNodeUnparsedWhitespace(node) {
if (/[^\t\n\r ]/.test(node.textContent)) return false;
const range = document.createRange();
range.selectNodeContents(node);
const rects = range.getClientRects();
if ((rects.length > 0) && (rects[0].width > 0)) return false;
return true;
}
/* wrapper function to embed selected text nodes in span */
function wrapSelectedTextNodes(id, className) {
getSelectedTextNodes().forEach((selection, index) => {
selection.forEach((textNode, nodeNumber) => {
let span = document.createElement('span');
if (nodeNumber==0) span.id=id+"-"+index;
else span.setAttribute("for",id+"-"+index);
span.classList.add(className);
textNode.before(span);
span.appendChild(textNode);
});
});
}
/* splits partially selected text nodes in parts, returns all selected text nodes as a two-dimensional array */
function getSelectedTextNodes() {
let returnArray = new Array();
let selection = window.getSelection();
for (let rangeNumber = selection.rangeCount-1; rangeNumber >= 0; rangeNumber--) {
let rangeNodes = new Array();
let range = selection.getRangeAt(rangeNumber);
if (range.startContainer === range.endContainer && range.endContainer.nodeType === Node.TEXT_NODE) {
range.startContainer.splitText(range.endOffset);
let textNode = range.startContainer.splitText(range.startOffset);
rangeNodes.push(textNode);
} else {
/* edge-case for rare circumstances where, the end-container may contain a text node, but not be the text node itself, end-container is redefined for iterator */
let startContainer=range.startContainer;
let endContainer=range.endContainer;
if (range.endContainer.nodeType != Node.TEXT_NODE && range.endContainer.childNodes.length>range.endOffset) endContainer=range.endContainer.childNodes[range.endOffset];
if (range.startContainer.nodeType != Node.TEXT_NODE && range.startOffset>0) startContainer=range.startContainer.childNodes[range.startOffset-1];
/* collect all text nodes inside range, ignore them if they are marked as non-selectable through css */
let textIterator = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, (node) => (node.compareDocumentPosition(startContainer)==Node.DOCUMENT_POSITION_PRECEDING && node.compareDocumentPosition(endContainer)==Node.DOCUMENT_POSITION_FOLLOWING) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT );
while (node = textIterator.nextNode()) { if (!isTextNodeUnparsedWhitespace(node) && window.getComputedStyle(node.parentElement).userSelect!="none") rangeNodes.push(node);}
/* separate first and final text node */
if (range.endContainer.nodeType === Node.TEXT_NODE && window.getComputedStyle(range.endContainer.parentElement).userSelect!="none") {
range.endContainer.splitText(range.endOffset);
rangeNodes.push(range.endContainer);
}
if (range.startContainer.nodeType === Node.TEXT_NODE && window.getComputedStyle(range.startContainer.parentElement).userSelect!="none") {
rangeNodes.unshift(range.startContainer.splitText(range.startOffset));
}
}
returnArray.unshift(rangeNodes);
}
return returnArray;
}
@HeimMatthias
Copy link
Author

New version:

  • respects CSS style user-select:none
  • complex workaround for rare edge-cases where selection end is not returned as a text-node but parent and offset

known issues:

  • will fail to return rendered all white-space nodes if they are part of hidden/not displayed elements
  • does not take display:none; or visibility:hidden; into consideration.

@HeimMatthias
Copy link
Author

Anyone wishing to implement a more concise method for parsing whitespace-only text-nodes should read this: https://www.w3.org/TR/CSS22/text.html#propdef-white-space

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment