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

Incredible Demos

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!