Treehouse

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!

ydkjs-2.png

Recent Features

  • CSS Animations Between Media Queries

    CSS animations are right up there with sliced bread. CSS animations are efficient because they can be hardware accelerated, they require no JavaScript overhead, and they are composed of very little CSS code. Quite often we add CSS transforms to elements via CSS during...

  • 5 HTML5 APIs You Didn&#8217;t Know Existed

    When you say or read "HTML5", you half expect exotic dancers and unicorns to walk into the room to the tune of "I'm Sexy and I Know It."  Can you blame us though?  We watched the fundamental APIs stagnate for so long that a basic feature...

Incredible Demos

  • MooTools Documentation Search Favelet

    I'm going to share something with you that will blow your mind: I don't have the MooTools documentation memorized. I just don't. I visit the MooTools docs frequently to figure out the order of parameters of More classes and how best to use...

  • Fx.Rotate:  Animated Element Rotation with MooTools

    I was recently perusing the MooTools Forge and I saw a neat little plugin that allows for static element rotation: Fx.Rotate. Fx.Rotate is an extension of MooTools' native Fx class and rotates the element via CSS within each A-grade browser it...

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!