Boudy hesham
Boudy hesham

Reputation: 352

How to highlight a user selected text which is not contained within a single text node using vanilla js?

I want to get the user selection then replace them with a mark tag on each text node inside of an element if the user selected two text node which are child, grandchild (for ex: inside a tag which is a child), so surround the first node with <mark>first text node</mark> and the second one like this <mark><a>partial of second</a></mark><a>rest which is not highlighted</a>

<div>
<p id="first" data-selectable-paragraph="">
At the very first age, JavaScript was called LiveScript. An engineer named Brendan Eich has created JavaScript in 1995. There was a little confused about the name with Java and JavaScript. After several months Microsoft released JScript with Internet Explorer 3. After that Netscape submitted JavaScript to Ecma International. In 1999 ECMAScript edition 3 launched and it has stayed pretty much stable ever since.
</p>
<p>
Another Selection <span> you could do</span>
</p>
<p id="second" data-selectable-paragraph="">
Range is something I discovered recently, which again showed me that the possibilities with the Javascript and the DOM are truly endless. As stated on Mozilla’s developer site, range ‘represents a fragment of a document that can contain nodes and parts of text nodes’. So, if you create of select a section of a document, range could tell you the nodes that it contains, its starting and ending positions relative to the document, it can clone its content ,and much more. (Read more from the docs: 
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Range">
https://developer.mozilla.org/en-US/docs/Web/API/Range
</a>)
</p>
</div>

let's say that the user selected 3 texts and highlighted them which I want to result in this structure-> Brief explanation: if the parent or grandparent has data-selectable-paragraph then highlight the text nodes inside the element and its parent if it is the grandchild

<div>
<p id="first" data-selectable-paragraph>
At the very first age, JavaScript was called LiveScript. An engineer named Brendan Eich has created JavaScript in 1995. There was a little confusion about the name with Java and JavaScript. After several months Microsoft released JScript with Internet Explorer 3. After that Netscape submitted JavaScript to Ecma International. In 1999 ECMAScript edition 3 <mark>launched and it has stayed pretty much stable ever since.</mark>
</p>
<p class='second' data-selectable-paragraph>
<mark>Another Selection </mark><mark><span> you could do</span></mark>
</p>
<p id="third" data-selectable-paragraph>
<mark>
Range is something I discovered recently, which again showed me that the possibilities with the Javascript and the DOM are truly endless. As stated on Mozilla’s developer site, range ‘represents a fragment of a document that can contain nodes and parts of text nodes’. So, if you create of select a section of a document, range could tell you the nodes that it contains, its starting and ending positions relative to the document, it can clone its content ,and much more. (Read more from the docs:
</mark>
<mark>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Range">
https://developer.mozilla.org/en-US/docs/Web/API/Range
</a>
</mark>
<mark>)</mark>
</p>
</div>

what i've found so far was window.getSelection().getRangeAt(0).commonAncestorContainer.hasAttribute("data-selectable-paragraph"); on mouse up but i don't know what to do next

EDIT:

Selected text

  1. launched and it has stayed pretty much stable ever since. its parent is the first P.
  2. Another Selection you could do its parent is the second P.
  3. Range is something I discovered recently, which again showed me that the possibilities with the Javascript and the DOM are truly endless. As stated on Mozilla’s developer site, range ‘represents a fragment of a document that can contain nodes and parts of text nodes’. So, if you create or select a section of a document, the range could tell you the nodes that it contains, its starting and ending positions relative to the document, it can clone its content, and much more. (Read more from the docs: https://developer.mozilla.org/en-US/docs/Web/API/Range) its parent is the third P.

There is before after snippets so please take them into consideration

Upvotes: 2

Views: 589

Answers (1)

Souleste
Souleste

Reputation: 1984

Here is a start. I'm simply allowing you to mark the text once, not set and unset the marks.

window.onload = function () {
    var btn = document.getElementsByClassName("get_selection")[0];
    btn.addEventListener("click", function () {
        var wrap = ["A"]; // nodes that should be wrapped in mark (rather than node's textContent wrapped in mark)
        var sel = window.getSelection();
        var range = sel.getRangeAt(0);

        if (!range.startContainer.isSameNode(range.endContainer)) {
            // get all nodes within the range commonAncestorContainer node
            var treeWalker = document.createTreeWalker(
                range.commonAncestorContainer,
                NodeFilter.SHOW_ALL
            );

            var nodeList = [];
            var currentNode = treeWalker.currentNode;
            while (currentNode) {
                nodeList.push(currentNode);
                currentNode = treeWalker.nextNode();
            }

            var start = null; // index that our selected nodes start
            var end = null; // index that our selected nodes end
            var selNodes = nodeList.filter(function (val, i) {
                // filter the node list
                var node = nodeList[i];
                start = start ?? (val.isSameNode(range.startContainer) ? i : null); // if same as start node
                end = end ?? (val.isSameNode(range.endContainer) ? i : null); // if same as end node
                var lesser = start == null || i <= start; // is before start node?
                var greater = end != null && i >= end; // is after end node?
                return (
                    !lesser &&
                    !greater &&
                    !node.isSameNode(range.endContainer.parentNode) && // node is not same as end node's parent
                    node != undefined &&
                    node != null &&
                    node.textContent.replace(/\t|\n/g, "") != "" &&
                    node.textContent.replace(/\t|\n/g, "") != undefined &&
                    !node.contains(range.endContainer) && // node does not contain end node
                    !node.isSameNode(range.endContainer.parentNode) // node is not same as end node's parent
                );
            });

            // mark node at start of selection
            var sParent = range.startContainer.parentNode;
            var sText = range.startContainer.textContent;
            var mark = document.createElement("mark");
            // wrap a tags in mark
            if (
                wrap.includes(sParent.nodeName) &&
                sText.replace(/\t+|\n+/gm, "") ==
                    sText.substring(range.startOffset).replace(/\t+|\n+/gm, "")
            ) {
                var node = sParent.cloneNode(true);
                mark.append(node);
                sParent.after(mark);
                sParent.remove();
            } else {
                mark.textContent = sText.substring(range.startOffset);
                range.startContainer.textContent = sText.substring(range.startOffset, -1);
                range.startContainer.after(mark);
            }

            // mark node at end of selection
            var eParent = range.endContainer.parentNode;
            // console.log("end parent: ", eParent);
            var eText = range.endContainer.textContent;
            var mark = document.createElement("mark");
            // wrap a tags in mark
            if (
                wrap.includes(eParent.nodeName) &&
                eText.replace(/\t+|\n+/gm, "") ==
                    eText.substring(range.endOffset, -1).replace(/\t+|\n+/gm, "")
            ) {
                var node = eParent.cloneNode(true);
                mark.append(node);
                eParent.after(mark);
                eParent.remove();
            } else {
                mark.textContent = eText.substring(range.endOffset, -1);
                range.endContainer.textContent = eText.substring(range.endOffset);
                range.endContainer.before(mark);
            }

            // mark nodes in between start and end
            selNodes.forEach(function (val, idx) {
                var currentNode = selNodes[idx];
                var mark = document.createElement("mark");
                if (currentNode.nodeType === Node.TEXT_NODE) {
                    // if text node, insert mark after node and remove node
                    mark.textContent = currentNode.textContent;
                    currentNode.after(mark);
                    currentNode.remove();
                } else {
                    if (wrap.includes(currentNode.nodeName)) {
                        var node = currentNode.cloneNode(true);
                        mark.append(node);
                        currentNode.after(mark);
                        currentNode.remove();
                    } else {
                        // reset the node's html and append mark
                        mark.textContent = currentNode.textContent;
                        currentNode.innerHTML = "";
                        currentNode.appendChild(mark);
                    }
                }
            });
        } else {
            var parentNode = range.startContainer.parentNode;
            var mark = document.createElement("mark");
            if (wrap.includes(parentNode.nodeName)) {
                var node = parentNode.cloneNode(true);
                node.textContent = sel.toString();
                mark.append(node);
                parentNode.after(mark);
                parentNode.remove();
            } else {
                var sText = document.createTextNode(
                    range.startContainer.textContent
                        .substring(range.startOffset, -1)
                        .toString()
                );
                var eText = document.createTextNode(
                    range.endContainer.textContent.substring(range.endOffset).toString()
                );
                mark.textContent = sel.toString();
                range.startContainer.after(eText);
                range.startContainer.after(mark);
                range.startContainer.after(sText);
                range.startContainer.remove();
            }
        }
    });
};
<button class="get_selection">Set Markers</button>
<div>
  <p id="first" data-selectable-paragraph>
    At the very first age, JavaScript was called LiveScript. An engineer named Brendan Eich has created JavaScript in 1995. There was a little confusion about the name with Java and JavaScript. After several months Microsoft released JScript with Internet
    Explorer 3. After that Netscape submitted JavaScript to Ecma International. In 1999 ECMAScript edition 3 launched and it has stayed pretty much stable ever since.
  </p>
  <p class='second' data-selectable-paragraph>
    Another Selection <span> you could do</span>
  </p>
  <p id="third" data-selectable-paragraph>

    Range is something I discovered recently, which again showed me that the possibilities with the Javascript and the DOM are truly endless. As stated on Mozilla’s developer site, range ‘represents a fragment of a document that can contain nodes and parts
    of text nodes’. So, if you create of select a section of a document, range could tell you the nodes that it contains, its starting and ending positions relative to the document, it can clone its content ,and much more. (Read more from the docs:

    <a href="https://developer.mozilla.org/en-US/docs/Web/API/Range">
            https://developer.mozilla.org/en-US/docs/Web/API/Range
        </a> )
  </p>
</div>

Upvotes: 3

Related Questions