Highlighter: A MooTools Search & Highlight Plugin

By  on  

Searching within the page is a major browser functionality, but what if we could code a search box in JavaScript that would do the same thing? I set out to do that using MooTools and ended up with a pretty decent solution.

The MooTools JavaScript Class

var Highlighter = new Class({
			
	/* implements */
	Implements: [Options],

	/* options */
	options: {
		autoUnhighlight: true,
		caseSensitive: false,
		elements: '*',
		className: '',
		onlyWords: false,
		tag: 'span'
	},
	
	/* initialization */
	initialize: function(options) {
		/* set options */
		this.setOptions(options);
		this.elements = $$(this.options.elements);
		this.words = [];
	},
	
	/* directs the plugin to highlight elements */
	highlight: function(words,elements,className) {
		
		/* figure out what we need to use as element(s) */
		var elements = $$(elements || this.elements);
		var klass = className || this.options.className;
		if (words.constructor === String) { words = [words]; }
		
		/* auto unhighlight old words? */
		if(this.options.autoUnhighlight) { this.unhighlight(); }
		
		/* set the pattern and regex */
		var pattern = '(' + words.join('|') + ')';
		pattern = this.options.onlyWords ? '\\b' + pattern + '\\b' : pattern;
		var regex = new RegExp(pattern, this.options.caseSensitive ? '' : 'i');
		
		/* run it for each element! */
		elements.each(function(el) { this.recurse(el,regex,klass); },this);
		
		/* make me chainable! */
		return this;
	}, 
	
	/* unhighlights items */
	unhighlight: function(words) {
		//var selector = this.options.tag + (word ? '[rel=' + word + ']' : '');
		if (words.constructor === String) { words = [words]; }
		words.each(function(word) {
			word = (this.options.caseSensitive ? word : word.toUpperCase());
			if(this.words[word]) {
				var elements = $$(this.words[word]);
				elements.set('class','');
				elements.each(function(el) {
					var tn = document.createTextNode(el.get('text'));
					el.getParent().replaceChild(tn,el);
				});
			}
		},this);
		return this;
	},
	
	/* recursed function */
	recurse: function(node,regex,klass) {
			if (node.nodeType === 3) {
				var match = node.data.match(regex);
				if (match) {
					/* new element */
					var highlight = new Element(this.options.tag);
					highlight.addClass(klass);
					var wordNode = node.splitText(match.index);
					wordNode.splitText(match[0].length);
					var wordClone = wordNode.cloneNode(true);
					highlight.appendChild(wordClone);
					wordNode.parentNode.replaceChild(highlight, wordNode);
					highlight.set('rel',highlight.get('text'));
					var comparer = highlight.get('text');
					if(!this.options.caseSensitive) { comparer = highlight.get('text').toUpperCase(); }
					if(!this.words[comparer]) { this.words[comparer] = []; }
					this.words[comparer].push(highlight);
					return 1;
				}
			} else if ((node.nodeType === 1 && node.childNodes) && !/(script|style)/i.test(node.tagName) && !(node.tagName === this.options.tag.toUpperCase() && node.className === klass)) {
				for (var i = 0; i < node.childNodes.length; i++) {
					i += this.recurse(node.childNodes[i],regex,klass);
				}
			}
			return 0;
		}
	});

The class does provide a few options:

  • autoUnhighlight: (defaults to true) Defines whether or not to auto-unhighlight highlighted words when searched.
  • caseSensitive: (defaults to false) Defines whether the search should be case sensitive.
  • elements: (defaults to '*') Defines which elements are searchable.
  • className: (defaults to '') The class name that will represent the highlighted word class. Gets applied to a span.
  • onlyWords: (defaults to false) Defines whether the class should only find words.
  • tag: (defaults to 'span') Defines the generated element type which will contain the highlighted text.

The class has two main methods:

  • highlight: Highlights the given text. Accepts the words, elements, and classname as parameters.
  • unhighlight: Unhighlights the given text. Accepts words as parameters.

The MooTools Usage

/* sample usage */
window.addEvent('domready',function() {
	
	/* instance */
	var highlighter = new Highlighter({
		elements: '#sample-content li',
		className: 'highlight',
		autoUnhighlight: false
	});
	
	/* submit listener */
	document.id('submit').addEvent('click',function() { if(document.id('search').value) { highlighter.highlight(document.id('search').value); } });
	document.id('submit3').addEvent('click',function() { if(document.id('search3').value) { highlighter.highlight(document.id('search3').value,'*','highlight1'); } });
	document.id('submit2').addEvent('click',function() { if(document.id('search2').value) { highlighter.unhighlight(document.id('search2').value); } });
	
	document.id('search').addEvent('keypress',function(e) { if(e.key == 'enter') { document.id('submit').fireEvent('click'); } });
	document.id('search3').addEvent('keypress',function(e) { if(e.key == 'enter') { document.id('submit3').fireEvent('click'); } });
	document.id('search2').addEvent('keypress',function(e) { if(e.key == 'enter') { document.id('submit2').fireEvent('click'); } });
	
});

What's great is that there are only two functions to use publicly for this class: highlight() and unhighlight().

It's important for me to mention that this class is not perfect! One glaring issue is that if you search for a word, then unhighlight the word, and then look for that word with the next word ("Lorem" => "Lorem ipsum"), the searcher doesn't find the second word due to the way the nodes are in place. If you have a solution to fix that, please let me know. This class was based on http://bartaz.github.com/sandbox.js/jquery.highlight.html.

Happy searching!

Recent Features

  • By
    5 Ways that CSS and JavaScript Interact That You May Not Know About

    CSS and JavaScript:  the lines seemingly get blurred by each browser release.  They have always done a very different job but in the end they are both front-end technologies so they need do need to work closely.  We have our .js files and our .css, but...

  • By
    Create a CSS Cube

    CSS cubes really showcase what CSS has become over the years, evolving from simple color and dimension directives to a language capable of creating deep, creative visuals.  Add animation and you've got something really neat.  Unfortunately each CSS cube tutorial I've read is a bit...

Incredible Demos

  • By
    Reverse Element Order with CSS Flexbox

    CSS is becoming more and more powerful these days, almost to the point where the order of HTML elements output to the page no longer matters from a display standpoint -- CSS lets you do so much that almost any layout, large or small, is possible.  Semantics...

  • By
    Create a Twitter AJAX Button with MooTools, jQuery, or Dojo

    There's nothing like a subtle, slick website widget that effectively uses CSS and JavaScript to enhance the user experience.  Of course widgets like that take many hours to perfect, but it doesn't take long for that effort to be rewarded with above-average user retention and...

Discussion

  1. pretty slick ;)

  2. tlx

    Really cool!

  3. Alexander

    Great!!!

  4. To get rid of the “unhighlight/repeat search” issue, you can just scrub out the highlight class, i.e.

    $$('span').removeClass('highlight1');
    

    at the start of a search.

  5. i love jquery.

    JQUERY IN
    MOOTOOLS OUT :)

  6. Hi David,

    your script is really fantastic, but there is an error on it. If the string don’t match in every letters the highlight doesn’t work. Eg.: Lorem Ipsum. If I write Lore Ipsun, or Ipsun, Loren, etc. Don’t return the letters matched. Right? The scripts was that way, or it is an error?

  7. rodreego: That’s a known shortcoming of the script…so far. I’ll be looking to improve it soon.

  8. emse

    hey david
    how can I combine this script with a smoothscroll? I’m an absolute beginner with mootools

  9. What would you be trying to SmoothScroll to? The functionalities are different.

  10. emse

    i have a large page where every item is posted once. i want the user to type e.g. “Nightwish” and the script should find the word and scroll down to it (and highlight)

  11. I see. You’ll need to implement Events on the class — I’d recommend an “onFind” event that gets fired when a match is found. Then you can direct the element to be scrolled to.

  12. witold

    hi, i use the following cod to highlight / unhighlight searched word by clicking on a checkbox:

    $('colorme').addEvent('change',function() { 
    
    if($('colorme').checked==true) { 
    highlighter.highlight(Cookie.read('search_value'),'*','highlight');	} 
    else { highlighter.unhighlight(Cookie.read('search_value')); }
    			
    });				
    

    for some reason i get the following error

    el.getParent() is null

    can you tell me what’s wrong

    regards, witold

  13. this information most important for me.
    thanks…

  14. this seems like a great plugin, judging by the demo, but I just can’t get it working… if I use autoUnhighlight: true, when trying to search I always get the error

    words is undefined

    in

    if (words.constructor === String) { words = [words]; }

    which is in the unhighlight function…

    in any case, it won’t even search and I don’t think I do anything wrong…

  15. Katie

    Is there any way for the code to unhighlight the old search automatically when a new search is done?

  16. mrazi

    Hi David,

    this can fix the problem when searching for a word with another word after unhighligting itself.

         elements.each(function(el) {
               var tn = document.createTextNode(el.get('text'));
               el.getParent().replaceChild(tn,el);
               tn.parentNode.normalize(); //add this line
          });
    

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!