JavaScript Highlight substring in element

What is the best way to highlight a specific substring in a div element in an event? By highlight, I mean apply some CSS style, like yellow background or something.

Basically I need a simple JS client side function of the form:

function (element, start, end) { 
  // element is the element to manipulate.  a div or span will do
  // start and end are the start and end positions within the text of that
  // element where highlighting should be.
  // do the stuff
}

Only one highlight will be active.

Answer:1

Why using a selfmade highlighting function is a bad idea

The reason why it's probably a bad idea to start building your own highlighting function from scratch is because you will certainly run into issues that others have already solved. Challenges:

  • You would need to remove text nodes with HTML elements to highlight your matches without destroying DOM events and triggering DOM regeneration over and over again (which would be the case with e.g. innerHTML)
  • If you want to remove highlighted elements you would have to remove HTML elements with their content and also have to combine the splitted text-nodes for further searches. This is necessary because every highlighter plugin searches inside text nodes for matches and if your keywords will be splitted into several text nodes they will not being found.
  • You would also need to build tests to make sure your plugin works in situations which you have not thought about. And I'm talking about cross-browser tests!

Sounds complicated? If you want some features like ignoring some elements from highlighting, diacritics mapping, synonyms mapping, search inside iframes, separated word search, etc. this becomes more and more complicated.

Use an existing plugin

When using an existing, well implemented plugin, you don't have to worry about above named things. The article 10 jQuery text highlighter plugins on Sitepoint compares popular highlighter plugins.

Have a look at mark.js

mark.js is such a plugin that is written in pure JavaScript, but is also available as jQuery plugin. It was developed to offer more opportunities than the other plugins with options to:

  • search for keywords separately instead of the complete term
  • map diacritics (For example if "justo" should also match "justò")
  • ignore matches inside custom elements
  • use custom highlighting element
  • use custom highlighting class
  • map custom synonyms
  • search also inside iframes
  • receive not found terms

DEMO

Alternatively you can see this fiddle.

Usage example:

// Highlight "keyword" in the specified context
$(".context").mark("keyword");

// Highlight the custom regular expression in the specified context
$(".context").markRegExp(/Lorem/gmi);

It's free and developed open-source on GitHub (project reference).

Answer:2

If you have no attached events or complicated HTML, you can just do search and replace on the HTML:

element.innerHTML = element.innerHTML.replace(/search/gi, function(match) {
    return '<span class="highlight">' + match + '</span>'
});

If you want something better, you can manipulate the DOM directly without using innerHTML, which will preserve events and work for more complicated HTML:

/*
 * Takes in an array of consecutive TextNodes and returns a document fragment with `word` highlighted
 */
function highlight_text_nodes($nodes, word) {
    if (!$nodes.length) {
        return;
    }

    var text = '';

    // Concatenate the consecutive nodes to get the actual text
    for (var i = 0; i < $nodes.length; i++) {
        text += $nodes[i].textContent;
    }

    var $fragment = document.createDocumentFragment();

    while (true) {
        // Tweak this if you want to change the highlighting behavior
        var index = text.toLowerCase().indexOf(word.toLowerCase());

        if (index === -1) {
            break;
        }

        // Split the text into [before, match, after]
        var before = text.slice(0, index);
        var match = text.slice(index, index + word.length);
        text = text.slice(index + word.length);

        // Create the <mark>
        var $mark = document.createElement('mark');
        $mark.className = 'found';
        $mark.appendChild(document.createTextNode(match));

        // Append it to the fragment
        $fragment.appendChild(document.createTextNode(before));
        $fragment.appendChild($mark);
    }

    // If we have leftover text, just append it to the end
    if (text.length) {
        $fragment.appendChild(document.createTextNode(text));
    }

    // Replace the nodes with the fragment
    $nodes[0].parentNode.insertBefore($fragment, $nodes[0]);

    for (var i = 0; i < $nodes.length; i++) {
        var $node = $nodes[$nodes.length - i - 1];
        $node.parentNode.removeChild($node);
    }
}


/*
 * Highlights all instances of `word` in `$node` and its children
 */
function highlight($node, word) {
    var $children = $node.childNodes;
    var $current_run = [];

    for (var i = 0; i < $children.length; i++) {
        var $child = $children[i];

        if ($child.nodeType === Node.TEXT_NODE) {
            // Keep track of consecutive text nodes
            $current_run.push($child);
        } else {
            // If we hit a regular element, highlight what we have and start over
            highlight_text_nodes($current_run, word);
            $current_run = [];

            // Ignore text inside of our <mark>s
            if ($child.nodeType === Node.ELEMENT_NODE && $child.className !== 'found') {
                highlight($child, word);
            }
        }
    }

    // Just in case we have only text nodes as children
    if ($current_run.length) {
        highlight_text_nodes($current_run, word);
    }
}

/*
 * Removes all highlighted <mark>s from the given node
 */
function unhighlight($node) {
    var $marks = [].slice.call($node.querySelectorAll('mark.found'));

    for (var i = 0; i < $marks.length; i++) {
        var $mark = $marks[i];

        // Replace each <mark> with just a text node of its contents
        $mark.parentNode.replaceChild(document.createTextNode($mark.childNodes[0].textContent), $mark);
    }
}

Demo: https://jsfiddle.net/wLkbbo5m/4/

If you want even more features, just use a library (like mark.js). There's no point in reinventing the entire wheel.

Answer:3

You can use mark.js which provides a simple and powerful

$('p').mark('sit');
mark {
  background: orange;
  color: black;
}
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<script src="https://cdn.jsdelivr.net/mark.js/8.6.0/jquery.mark.min.js"></script>

<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>
Answer:4

Try Highlighter.js library here: https://gist.github.com/wliwanag/03d95916c7ba5d17e226

It supports: Highlighting of text in child elements. Use words in a text as search key (e.g. sample below will highlight "simply", "1500" etc..)

To use:

var searchInput = "simply 1500 bee y";
var textSource = $("#text-container").html();
var htmlSource = $("#html-container").html();

$("#text-result").html(highlightHtml(textSource, searchInput));
$("#html-result").html(highlightHtml(htmlSource, searchInput));

Fiddle

Answer:5

In a React project

I was worried about how other libraries' DOM-manipulation would work in a React project, so I used react-hightlight-words to solve this problem.

See the table of props for configuration.

Out of the box it takes search words instead of index ranges, but solves the same problem as in the question unless one is locked to indices. However, if that is the case, the findChunks property can be given a function to arbitrarily detect parts to hightlight. (See here how the default findChunks is defined.)

Simple example from the description on GitHub.

import React from "react";
import ReactDOM from "react-dom";
import Highlighter from "react-highlight-words";

ReactDOM.render(
    <Highlighter
        highlightClassName="YourHighlightClass"
        searchWords={["and", "or", "the"]}
        autoEscape={true}
        textToHighlight="The dog is chasing the cat. Or perhaps they're just playing?"
    />,
    document.getElementById("root")
);

Install with npm i --save react-highlight-words

Answer:6

I need height on the div 50px in default and it has to be changed to 300px onmouseover. I coded in below manner to implement it. <style type="text/css"> #div1{ height:50px; overflow:hidden; } #...

I need height on the div 50px in default and it has to be changed to 300px onmouseover. I coded in below manner to implement it. <style type="text/css"> #div1{ height:50px; overflow:hidden; } #...

  1. expand div height onmouseover

Is there a means of detecting whether an element appears before or after another element in markup? This is regardless of position in DOM. It could be a child, a sibling, a parent or a parent's parent....

Is there a means of detecting whether an element appears before or after another element in markup? This is regardless of position in DOM. It could be a child, a sibling, a parent or a parent's parent....

I'm relativly new to Backbone.js I have a JSON like the picture shows ! I saw some Answers in relation with Backbone-relational, but still dont get the point! How can i convert this JSON to ...

I'm relativly new to Backbone.js I have a JSON like the picture shows ! I saw some Answers in relation with Backbone-relational, but still dont get the point! How can i convert this JSON to ...

I'm currently researching a way to produce non-photorealistic rendering in webgl. The best looking algorithm I've found so far for edge detection was implemented with OpenGL's geometry shader, here. ...

I'm currently researching a way to produce non-photorealistic rendering in webgl. The best looking algorithm I've found so far for edge detection was implemented with OpenGL's geometry shader, here. ...