javascript user selection highlighting
Asked Answered
M

9

29

I'm trying to find a way with javascript to highlight the text the user selects when they click some odd highlight button (as in <span style="background-color:yellow">highlighted text</span>). It only has to work with either WebKit or Firefox, but it seems to be well nigh impossible because it has to work in the following cases:

<p>this is text</p>
<p>I eat food</p>

When the user selects from "is text" through "I eat" in the browser (can't just put a span there).

and this case:

<span><span>this is text</span>middle text<span>this is text</span></span>

When the user selects from "is text" to "this is" in the browser (even though you can wrap your highlight spans around each element in the selection, I'd like to see you try to get that middle text highlighted).

This problem doesn't seem to be solved anywhere, frankly I doubt it's possible.

It would be possible if you could get the Range that you get from the selection as a string complete with html which could be parsed and then replaced, but as far as I can tell you can't get the raw html of a Range.. pity.

Mishmash answered 20/11, 2008 at 10:6 Comment(0)
F
85

This answer is probably a few years too late for you, but I faced a similar problem and wanted to document it here, since it is the first hit on google.

To reiterate, the problem is that you would like to just capture the Range object from the User Selection and surround it with a styled div, like so:

function highlightSelection() {
    var userSelection = window.getSelection().getRangeAt(0);
    highlightRange(userSelection);

}

function highlightRange(range) {
    var newNode = document.createElement("div");
    newNode.setAttribute(
       "style",
       "background-color: yellow; display: inline;"
    );
    range.surroundContents(newNode);
}

But as Original Parent states, this is unsafe. It will work if the selection does not cross element boundaries, but it will throw a DOM eror if the Range created by the User Selection is an unsafe range which crosses the boundaries of HTML tags.


The solution is to produce an array of smaller Range objects, none of which individually crosses an element barrier, but which collectively cover the Range selected by the user. Each of these safe Ranges can be highlighted as above.

function getSafeRanges(dangerous) {
    var a = dangerous.commonAncestorContainer;
    // Starts -- Work inward from the start, selecting the largest safe range
    var s = new Array(0), rs = new Array(0);
    if (dangerous.startContainer != a)
        for(var i = dangerous.startContainer; i != a; i = i.parentNode)
            s.push(i)
    ;
    if (0 < s.length) for(var i = 0; i < s.length; i++) {
        var xs = document.createRange();
        if (i) {
            xs.setStartAfter(s[i-1]);
            xs.setEndAfter(s[i].lastChild);
        }
        else {
            xs.setStart(s[i], dangerous.startOffset);
            xs.setEndAfter(
                (s[i].nodeType == Node.TEXT_NODE)
                ? s[i] : s[i].lastChild
            );
        }
        rs.push(xs);
    }

    // Ends -- basically the same code reversed
    var e = new Array(0), re = new Array(0);
    if (dangerous.endContainer != a)
        for(var i = dangerous.endContainer; i != a; i = i.parentNode)
            e.push(i)
    ;
    if (0 < e.length) for(var i = 0; i < e.length; i++) {
        var xe = document.createRange();
        if (i) {
            xe.setStartBefore(e[i].firstChild);
            xe.setEndBefore(e[i-1]);
        }
        else {
            xe.setStartBefore(
                (e[i].nodeType == Node.TEXT_NODE)
                ? e[i] : e[i].firstChild
            );
            xe.setEnd(e[i], dangerous.endOffset);
        }
        re.unshift(xe);
    }

    // Middle -- the uncaptured middle
    if ((0 < s.length) && (0 < e.length)) {
        var xm = document.createRange();
        xm.setStartAfter(s[s.length - 1]);
        xm.setEndBefore(e[e.length - 1]);
    }
    else {
        return [dangerous];
    }

    // Concat
    rs.push(xm);
    response = rs.concat(re);    

    // Send to Console
    return response;
}

It is then possible to (appear to) highlight the User Selection, with this modified code:

function highlightSelection() {
    var userSelection = window.getSelection().getRangeAt(0);
    var safeRanges = getSafeRanges(userSelection);
    for (var i = 0; i < safeRanges.length; i++) {
        highlightRange(safeRanges[i]);
    }
}

Note that you'' probably need some fancier CSS to make the many disparate elements a user could look nice together. I hope that eventually this helps some other weary soul on the internet!

Working snippet

document.addEventListener('mouseup', highlightSelection);

function highlightSelection() {
  var userSelection = window.getSelection().getRangeAt(0);
  var safeRanges = getSafeRanges(userSelection);
  for (var i = 0; i < safeRanges.length; i++) {
    highlightRange(safeRanges[i]);
  }
}

function highlightRange(range) {
  var newNode = document.createElement("div");
  newNode.setAttribute(
    "style",
    "background-color: yellow; display: inline;"
  );
  range.surroundContents(newNode);
}

function getSafeRanges(dangerous) {
  var a = dangerous.commonAncestorContainer;
  // Starts -- Work inward from the start, selecting the largest safe range
  var s = new Array(0), rs = new Array(0);
  if (dangerous.startContainer != a) {
    for (var i = dangerous.startContainer; i != a; i = i.parentNode) {
      s.push(i);
    }
  }
  if (s.length > 0) {
    for (var i = 0; i < s.length; i++) {
      var xs = document.createRange();
      if (i) {
        xs.setStartAfter(s[i - 1]);
        xs.setEndAfter(s[i].lastChild);
      } else {
        xs.setStart(s[i], dangerous.startOffset);
        xs.setEndAfter((s[i].nodeType == Node.TEXT_NODE) ? s[i] : s[i].lastChild);
      }
      rs.push(xs);
    }
  }

  // Ends -- basically the same code reversed
  var e = new Array(0), re = new Array(0);
  if (dangerous.endContainer != a) {
    for (var i = dangerous.endContainer; i != a; i = i.parentNode) {
      e.push(i);
    }
  }
  if (e.length > 0) {
    for (var i = 0; i < e.length; i++) {
      var xe = document.createRange();
      if (i) {
        xe.setStartBefore(e[i].firstChild);
        xe.setEndBefore(e[i - 1]);
      } else {
        xe.setStartBefore((e[i].nodeType == Node.TEXT_NODE) ? e[i] : e[i].firstChild);
        xe.setEnd(e[i], dangerous.endOffset);
      }
      re.unshift(xe);
    }
  }

  // Middle -- the uncaptured middle
  if ((s.length > 0) && (e.length > 0)) {
    var xm = document.createRange();
    xm.setStartAfter(s[s.length - 1]);
    xm.setEndBefore(e[e.length - 1]);
  } else {
    return [dangerous];
  }

  // Concat
  rs.push(xm);
  response = rs.concat(re);

  // Send to Console
  return response;
}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>
Ferret answered 10/10, 2012 at 15:59 Comment(6)
I wish I could give you two upvotes for your hard work on this.Kyungkyushu
Good work, but.. The limitation of this solution is separating a whole into its parts and leaving those parts separated. You'd also need a common class or attribute lookup to re-unify them, when utilising them as the user expects (has created them).Lashanda
Thanks a lot for solving this problem for all of us. This works great. One point which I want to highlight is that this function returns a few empty ranges creating a few unnecessary span elements. I have added an additional condition in my highlightRange function to avoid that issue. This condition to skip any empty range. Thanks, if (range.toString() !== "" && range.toString().match(/\w+/g) !== null) { }Truett
This works well for the most part, but fails in when highlighting the direct child comment to the parent comment at reddit.com/r/Tinder/comments/gyb7vc/… "dm me..." remains unhighlighted.Firstnighter
thanks for your efforts, that is a great work, but I have an issue, I want to unhighlight the highlighted text or some of it, can that be done? please answer me, I am waiting for you, and thanks.Anthropogeography
You've just saved my day, thanks a lot bro. You are helping even after 10 years of your original answer. What a LEGEND !!!!Zima
G
15

Well, you can do it using DOM manipulation. This works in Firefox:

var selection = window.getSelection();
var range = selection.getRangeAt(0);
var newNode = document.createElement("span");
newNode.setAttribute("style", "background-color: pink;");
range.surroundContents(newNode); 

Seems to work in the current version of Safari as well. See https://developer.mozilla.org/en/DOM/range.surroundContents and http://www.w3.org/TR/2000/REC-DOM-Level-2-Traversal-Range-20001113/ranges.html

Grower answered 25/11, 2008 at 13:53 Comment(1)
This doesn't work if the selection crosses element boundaries (e.g. if it spans multiple paragraphs).Lepidus
N
9

This is my first time posting here, but looking through your answers, wouldn't something like this work? I have a sample here: http://henriquedonati.com/projects/Extension/extension.html

function highlightSelection() {
    var userSelection = window.getSelection();
    for(var i = 0; i < userSelection.rangeCount; i++) {
        highlightRange(userSelection.getRangeAt(i));
    }

}

function highlightRange(range) {
    var newNode = document.createElement("span");
    newNode.setAttribute(
       "style",
       "background-color: yellow; display: inline;"
    );
    range.surroundContents(newNode);
}
Naturalism answered 31/7, 2016 at 8:35 Comment(2)
This doesn't work when trying to highlight across multiple elementsMccallister
@henrique Is there any chance to store what has been highlighted into the Db? So when you come back it's still highlighted?Repugn
S
9

Here is a complete code to highlight and dehighlight the text

<!DOCTYPE html>
    <html>
        <head>
            <style type="text/css">
                .highlight
                {
                    background-color: yellow;
                }
                #test-text::-moz-selection { /* Code for Firefox */

                    background: yellow;
                }

                #test-text::selection {

                    background: yellow;
                }

            </style>
        </head>

        <body>
            <div id="div1" style="border: 1px solid #000;">
                <div id="test-text">
                    <h1> Hello How are you </h1>
                    <p >
                        Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
                    </p>
                </div>
            </div>
            <br />

        </body>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
            <script type="text/javascript">
                mouseXPosition = 0;
                $(document).ready(function () {

                    $("#test-text").mousedown(function (e1) {
                        mouseXPosition = e1.pageX;//register the mouse down position
                    });

                    $("#test-text").mouseup(function (e2) {
                        var highlighted = false;
                        var selection = window.getSelection();
                        var selectedText = selection.toString();
                        var startPoint = window.getSelection().getRangeAt(0).startOffset;
                        var endPoint = window.getSelection().getRangeAt(0).endOffset;
                        var anchorTag = selection.anchorNode.parentNode;
                        var focusTag = selection.focusNode.parentNode;
                        if ((e2.pageX - mouseXPosition) < 0) {
                            focusTag = selection.anchorNode.parentNode;
                            anchorTag = selection.focusNode.parentNode;
                        }
                        if (selectedText.length === (endPoint - startPoint)) {
                            highlighted = true;

                            if (anchorTag.className !== "highlight") {
                                highlightSelection();
                            } else {
                                var afterText = selectedText + "<span class = 'highlight'>" + anchorTag.innerHTML.substr(endPoint) + "</span>";
                                anchorTag.innerHTML = anchorTag.innerHTML.substr(0, startPoint);
                                anchorTag.insertAdjacentHTML('afterend', afterText);
                            }

                        }else{
                            if(anchorTag.className !== "highlight" && focusTag.className !== "highlight"){
                                highlightSelection();  
                                highlighted = true;
                            }

                        }


                        if (anchorTag.className === "highlight" && focusTag.className === 'highlight' && !highlighted) {
                            highlighted = true;

                            var afterHtml = anchorTag.innerHTML.substr(startPoint);
                            var outerHtml = selectedText.substr(afterHtml.length, selectedText.length - endPoint - afterHtml.length);
                            var anchorInnerhtml = anchorTag.innerHTML.substr(0, startPoint);
                            var focusInnerHtml = focusTag.innerHTML.substr(endPoint);
                            var focusBeforeHtml = focusTag.innerHTML.substr(0, endPoint);
                            selection.deleteFromDocument();
                            anchorTag.innerHTML = anchorInnerhtml;
                            focusTag.innerHTml = focusInnerHtml;
                            var anchorafterHtml = afterHtml + outerHtml + focusBeforeHtml;
                            anchorTag.insertAdjacentHTML('afterend', anchorafterHtml);


                        }

                        if (anchorTag.className === "highlight" && !highlighted) {
                            highlighted = true;
                            var Innerhtml = anchorTag.innerHTML.substr(0, startPoint);
                            var afterHtml = anchorTag.innerHTML.substr(startPoint);
                            var outerHtml = selectedText.substr(afterHtml.length, selectedText.length);
                            selection.deleteFromDocument();
                            anchorTag.innerHTML = Innerhtml;
                            anchorTag.insertAdjacentHTML('afterend', afterHtml + outerHtml);
                         }

                        if (focusTag.className === 'highlight' && !highlighted) {
                            highlighted = true;
                            var beforeHtml = focusTag.innerHTML.substr(0, endPoint);
                            var outerHtml = selectedText.substr(0, selectedText.length - beforeHtml.length);
                            selection.deleteFromDocument();
                            focusTag.innerHTml = focusTag.innerHTML.substr(endPoint);
                            outerHtml += beforeHtml;
                            focusTag.insertAdjacentHTML('beforebegin', outerHtml );


                        }
                        if (!highlighted) {
                            highlightSelection();
                        }
                        $('.highlight').each(function(){
                            if($(this).html() == ''){
                                $(this).remove();
                            }
                        });
                        selection.removeAllRanges();
                    });
                });

                function highlightSelection() {
                    var selection;

                    //Get the selected stuff
                    if (window.getSelection)
                        selection = window.getSelection();
                    else if (typeof document.selection != "undefined")
                        selection = document.selection;

                    //Get a the selected content, in a range object
                    var range = selection.getRangeAt(0);

                    //If the range spans some text, and inside a tag, set its css class.
                    if (range && !selection.isCollapsed) {
                        if (selection.anchorNode.parentNode == selection.focusNode.parentNode) {
                            var span = document.createElement('span');
                            span.className = 'highlight';
                            span.textContent = selection.toString();
                            selection.deleteFromDocument();
                            range.insertNode(span);
    //                        range.surroundContents(span);
                        }
                    }
                }

            </script>
    </html>

https://jsfiddle.net/Bilalchk123/1o4j0w2v/

Silvana answered 11/8, 2017 at 7:29 Comment(1)
Not work for text selection across multiple paragraphs.Booby
M
6

    function load(){
      window.document.designMode = "On";
      //run this in a button, will highlight selected text
      window.document.execCommand("hiliteColor", false, "#768");
    }
   
    <html>
    <head>

    </head>
    <body contentEditable="true" onload="load()">
      this is text
    </body>
    </html>
Mishmash answered 20/11, 2008 at 23:2 Comment(3)
This is easily the best idea. Good answer, but it would really be an idea to turn designMode off again afterwards.Lepidus
Although the posts are quite old, I still see this as the best solution I can find right nowAvie
Sadly this feature is obsolete. developer.mozilla.org/en-US/docs/Web/API/Document/execCommandFirstnighter
C
3

I just finished releasing a package that is a typescript port of texthighlighter (a deprecated library). Just converting it to typescript has caught a few bugs and made it easier to work on in the future. Checkout https://www.npmjs.com/package/@funktechno/texthighlighter. This has no dependencies and allows for highlighting user selection, merging highlights, removinging highlights, serializing and deserializing (applying from data) highlights.

Note you will need to use the javascript mouseup event to properly trigger it.

import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, optionsImpl } from "@/../node_modules/@funktechno/texthighlighter/lib/index";
const domEle = document.getElementById("sandbox");
const options: optionsImpl = {};
if (this.color) options.color = this.color;
if (domEle) doHighlight(domEle, true, options);

this is how I triggered it in a vue ts project

<div
     id="sandbox"
     @mouseup="runHighlight($event)"
>text to highlight</div>
Convene answered 12/7, 2020 at 3:1 Comment(0)
A
2

I was having the same problem today, highlighting the selected tags ranging over multiple tags. The solution:

  1. Find a way to extract the selected portion along with the HTML tags.
  2. Wrap the extracted portion with a span element and put it back in the DOM .

Refer the code below , for further clarification.

function getRangeObject(selectionObject){
    try{ 
        if(selectionObject.getRangeAt)
            return selectionObject.getRangeAt(0);
    }
    catch(ex){
        console.log(ex);
    }
}
document.onmousedown = function(e){
    var text;
    if (window.getSelection) {
        /* get the Selection object */
        userSelection = window.getSelection()

        /* get the innerText (without the tags) */ 
        text = userSelection.toString();

        /* Creating Range object based on the userSelection object */
        var rangeObject = getRangeObject(userSelection);

        /* 
           This extracts the contents from the DOM literally, inclusive of the tags. 
           The content extracted also disappears from the DOM 
        */
        contents = rangeObject.extractContents(); 

        var span = document.createElement("span");
        span.className = "highlight";
        span.appendChild(contents);

        /* Insert your new span element in the same position from where the selected text was extracted */
        rangeObject.insertNode(span);

    } else if (document.selection && document.selection.type != "Control") {
            text = document.selection.createRange().text;
    }
};
Ancilin answered 31/12, 2015 at 9:39 Comment(0)
R
1

since HTML use <mark> element as highlighted text, maybe it's easy to use this node, instead of using your own css, much more clean code:

function highlightRange(range) {
   var newNode = document.createElement('mark');
   range.surroundContents(newNode);
}
// original select range function
function highlight() {
    var userSelection = window.getSelection();
    for(var i = 0; i < userSelection.rangeCount; i++) {
        highlightRange(userSelection.getRangeAt(i));
    }
}

Reseda answered 6/9, 2021 at 6:36 Comment(0)
F
0

The other answers advocate modifying the DOM to enclose the text to highlight with a <span> or a <mark>. This has several drawbacks:

  • it pollutes the DOM in contentEditable elements
  • it is not compatible with frontend frameworks like React, which are responsible for the DOM

Fortunately, recent browsers have a native API for highlighting text: the CSS Custom Highlight API. It requires a bit too much JavaScript to search a string in text nodes (using a tree walker) to be pasted here. But with a third-party library like highlight-search-term, it's a one-liner:

<script type="module">
import { highlightSearchTerm } from "https://cdn.jsdelivr.net/npm/[email protected]/src/index.js";
highlightSearchTerm({ search: 'KEYWORD',  selector: ".content" });
</script>

Put the above snippet at the end of the page body, replacing KEYWORD by your search keyword and .content with the CSS selector of the page element(s) where you want to highlight words.

This solution is also compatible with all frontend frameworks like React, Angular, Vue or Svelte, and it works for contentEditable elements.

More examples at https://www.npmjs.com/package/highlight-search-term

Funkhouser answered 2/5 at 7:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.