Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Creating a simple jQuery plug-in

4.86/5 (7 votes)
26 Sep 2010CPOL5 min read 42.1K   530  
This article demonstrates the steps in creating a simple jQuery plug-in with a sample called Map Scroller.

Introduction

We see a lot of JavaScript libraries floating around. A JavaScript library makes developers' life easy by avoiding the need to directly deal with the dirty waters of browser compatibility issues. They save a lot of time by allowing programmers to code what they intend to do and not worry about browsers.

jQuery is one of the libraries I admire. It's light-weight, easy to use, beautiful, and so on. An interesting feature of jQuery is plug-ins; they are JavaScript files that can be plugged in when needed.

In this article, we will see how to create a simple plug-in called Map Scroller that lets any overflowing HTML element to scroll like in Google Maps.

Let's dive into the article :)

Requirements

  • jQuery v1.4.2
  • jquery.DisableTextSelect.js

Tips in jQuery

  • $ is the main object in jQuery. Everything in jQuery is based upon this, or uses this in some way.
  • $ wraps an object called fn. To add a plug-in to jQuery, we have to add our method(s) into this object.
  • JavaScript
    $.fn.Method1 = function(){
    };
    
    $("div").Method1();
  • bind() allows you to bind methods to events.
  • JavaScript
    $("input[type=button]").bind("mouseover mouseenter", function(){
        alert('hi');
    });
  • data() allows you to store any custom data as key-value pairs.
  • JavaScript
    $(this).data("mouseposition", {x:200, y:300});
  • addClass() allows you to add CSS class(es) dynamically.
  • JavaScript
    $("p").addClass("class1 class2");
  • removeClass() allows you to remove CSS class(es).
  • JavaScript
    $("p").removeClass("class1");
  • scrollTop() scrolls the element vertically.
  • JavaScript
    $("myDiv").scrollTop(20);
    //scrolls the element down to 20 pixels from the top position.
  • scrollLeft() scrolls the element horizontally.
  • JavaScript
    $("myDiv").scrollLeft(20);
    //scrolls the element right to 20 pixels from the left position.

Map Scroller

Higher Level Plan

  • Create a method called mapScroll(direction).
  • The input parameter direction can be any of these values: "vertical", "horizontal", or "both". It represents which direction the scrolling should happen. If you don't pass any parameter, then it is taken as "both".
  • Add this method into jQuery's fn object, so any overflowing HTML element can achieve this scrolling by:
  • JavaScript
    $("#OverflowingDiv").mapScroll("vertical");

Lower Level Plan

  • For achieving this scrolling, we have to disable then text selection for that element. A plug-in named DisableTextSelect helps us do that.
  • Bind event handlers to the mousedown, mousemove, and mouseup events of the element.
  • Enable scrolling in the mousedown event handler.
  • Disable scrolling in the mouseup event handler.
  • Calculate the distance the mouse moved from the old and new mouse positions, and scroll the element in the mousemove event handler.

Code

Let's create the skeleton first:

JavaScript
$.fn.mapScroll = function(direction){
        
    //implementation starts
    
};

Calling $ directly is not safe in the global scope, because there may be some other libraries that have their own implementation of $. So to avoid global variable collisions, we have to create a private space.

JavaScript
(function($){

    $.fn.mapScroll = function(direction){
    
        //implementation starts
        
    };

})(jQuery); //$ and jQuery both are same.

Now all the variables used between the pair of braces will not get interrupted by outside. We have used a closure to create the private space.

Below is a typical Closure:

JavaScript
var x = 25;
/*Closure starts*/
(function(){
    var x = 35;
    alert(x);
})();
/*Closure ends*/
alert(x);

When the browser crosses the above code, first it alerts 35, and then 25. Closures help to create a private scope and more.

We are going to use some private methods in our plug-in; let's drop them in the private space.

JavaScript
(function($){

    //private space 
        //Add your private members here
    $.fn.mapScroll = function(direction){
    
        //implementation starts
        
    };

})(jQuery);

Create event handlers for the mouseup, mousedown, and mousemove events, and wrap them in a private object eventHandlers.

JavaScript
(function($){

    //private methods
    var eventHandlers = {
        mousemove: function(e){            
            //calculate the distance moved and scroll
        },
        
        mouseup: function(e){
            //reset the flag
        },
        
        mousedown: function(e){
            //capture the mouse position at initial time
            //set a flag
        }
    };

    $.fn.mapScroll = function(direction){
    
        //hook the event handlers 
        
    };

})(jQuery);

Our skeleton is pretty much ready. Let's finish up the public method mapScroll first.

JavaScript
$.fn.mapScroll = function(direction){

    return this.each(function(){
        //disable text selection using the plugin jquery.DisableTextSelect.js
        $(this).disableTextSelect();
        ....
    });
    
};

Wait! What do return and each() do above? $() returns an array of HTML elements, so we have to iterate all of them, and for that, we use the each() method. return makes our plug-in chainable. jQuery is famous for Chaining. Chaining allows us to call a set of methods on an object like below:

JavaScript
$("p").show().addClass("paragraphstyle").mapScroll("both"); //a chain

Without chaining, we have to call the methods individually:

JavaScript
$("p").show();
$("p").addClass("paragraphstyle");
$("p").mapScroll("both");

We are applying chaining, so we can do $("scrollDiv").mapScroll().show(). Applying chaining is very simple, just return the object instead of not returning anything.

Back to mapScroll()! Bind the event handlers:

JavaScript
$.fn.mapScroll = function(direction){

    return this.each(function(){
        //disable text selection
        $(this).disableTextSelect();    
        
        //binding event-handlers
        $(this).bind("mousemove", eventHandlers.mousemove)
               .bind("mouseup", eventHandlers.mouseup)
               .bind("mousedown", eventHandlers.mousedown);
        ...
        
    });
    
};

We have to talk about the mouse-move event handler right here. In the mouse-move event, we need to know the direction. If the direction is "vertical", then we should scroll only in the vertical direction. Likewise, if it is "horizontal", then scroll only in the horizontal direction, else both.

Somehow if we pass the direction parameter to the mouse-move handler, then by writing a set of if-else statements, we can do that. But I'm not so happy with that approach because every time the mouse moves, the conditions get evaluated. It would be nice if it happens only once.

I thought of putting the scroll logic in a separate method called scrollFn. The trick is, the implementation of scrollFn is dynamically decided based on the direction parameter. Little confusing? See the code below:

JavaScript
var scrollFn = (function(){
    switch(direction || "both")
    {
        case "vertical":
            return function($this,x,y){ 
                $this.scrollTop( $this.scrollTop() + y ); 
            };
        case "horizontal":
            return function($this,x,y){ 
                $this.scrollLeft( $this.scrollLeft() + x ); 
            };
        case "both":
            return function($this,x,y){ 
                $this.scrollTop( $this.scrollTop() + y).scrollLeft($this.scrollLeft() + x );
            };                
    }        
})();

The same closure concept! But this time, a different thing than the private scope. This approach is called Memoizing. The body part of the function scrollFn is dynamically created at initialization time.

So if the direction value is "vertical", scrollFn is:

JavaScript
var scrollFn = function($this,x,y){
    //scrolls only in vertical direction
    $this.scrollTop( $this.scrollTop() + y ); 
}

Likewise, if it is "both", then

JavaScript
var scrollFn = function($this,x,y){
    //scrolls in both directions
    $this.scrollTop( $this.scrollTop() + y).scrollLeft($this.scrollLeft() + x );
}

Where $this is the object, and x and y are the distance in pixels the object has to be scrolled either horizontally and vertically. We have to find a way to get this method in the mouse-move handler. That is really simple! We just have to store it using the data() method:

JavaScript
$(this).data("scrollFn", scrollFn);
//"scrollFn" is the key, scrollFn is the value.

Now we can get scrollFn anywhere in our plug-in by:

JavaScript
var scrollFn = $(this).data("scrollFn");

Here is the complete implementation of the mapScroll method:

JavaScript
$.fn.mapScroll = function(direction){

    var scrollFn = (function(){
        switch(direction || "both")
        {
            case "vertical":
                return function($this,x,y){ 
                    $this.scrollTop( $this.scrollTop() + y ); 
                };
            case "horizontal":
                return function($this,x,y){ 
                    $this.scrollLeft( $this.scrollLeft() + x ); 
                };
            case "both":
                return function($this,x,y){ 
                    $this.scrollTop( $this.scrollTop() + y).scrollLeft(
                                     $this.scrollLeft() + x );
                };
        }
    })();

    return this.each(function(){
        //disabling text selection
        $(this).disableTextSelect();
            
        //binding event-handlers
        $(this).bind("mousemove", eventHandlers.mousemove)
               .bind("mouseup", eventHandlers.mouseup)
               .bind("mousedown", eventHandlers.mousedown);
        
        //adding a css class to set custom cursor 
        $(this).addClass("openhand");    
        
        //storing the scroll function.
        $(this).data("scrollFn", scrollFn)    
    });
    
};

Let's see the event handlers now.

JavaScript
mousedown: function(e){
    $(this).data("scroll", true)
           .data("position", {x : e.pageX, y : e.pageY});
           
    $(this).removeClass("openhand")
           .addClass("closedhand");
}

In the mousedown event handler, we enable the scrolling by setting the flag scroll to true. The flag and initial mouse positions are stored using data(). Also, we swap the CSS classes to change the cursor.

JavaScript
mouseup: function(e){
    $(this).data("scroll", false);
    
    $(this).removeClass("closedhand")
           .addClass("openhand");
}

In the mouseup event handler, we disable the scrolling by setting the flag scroll to false and reverting the cursor CSS classes.

JavaScript
mousemove: function(e){
    var $this = $(this), 
        scroll = $this.data("scroll"), 
        prevPos = $this.data("position"),
        scrollFn = $this.data("scrollFn");
    
    if(scroll){
        var diffX = prevPos.x - e.pageX, 
            diffY = prevPos.y - e.pageY;
                
        scrollFn($this, diffX, diffY);
        
        $this.data("position", {x:e.pageX, y:e.pageY});
    }
}

In the mousemove event handler, we calculate the differences between the old and new mouse positions and scroll the content using the scrollFn method that is stored. Finally, we overwrite the old mouse position with the new one.

Before seeing the complete code, I found an issue during mouse-out, the event we are not listening to... So I did the same thing that we did in mouse-up, that is disable the scrolling when the user brings the mouse-out of the element.

JavaScript
//both mouseup & mouseout binded to same handler
$(this).bind("mouseup mouseout", eventHandlers.mouseupout);

Here is our complete code:

JavaScript
(function($){

    var eventHandlers = {
        mousemove: function(e){
            var $this = $(this), 
                scroll = $this.data("scroll"), 
                prevPos = $this.data("position"),
                scrollFn = $this.data("scrollFn");

            if(scroll){            
                var diffX = prevPos.x - e.pageX, 
                    diffY = prevPos.y - e.pageY;
                        
                scrollFn($this, diffX, diffY);
                
                $this.data("position", {x:e.pageX, y:e.pageY});
            }
        },

        mouseupout: function(e){
            $(this).removeClass("closedhand")
                   .addClass("openhand");
                   
            $(this).data("scroll", false);
        },
        
        mousedown: function(e){
            $(this).removeClass("openhand")
                   .addClass("closedhand");
                   
            $(this).data("scroll", true)
                   .data("position", {x : e.pageX, y : e.pageY});
        }
    };

    $.fn.mapScroll = function(direction){

        //assigning scroll function
        var scrollFn = (function(){
            switch(direction || "both")
            {
                case "vertical":
                    return function($this,x,y){ 
                        $this.scrollTop( $this.scrollTop() + y ); 
                    };
                   case "horizontal":
                    return function($this,x,y){ 
                        $this.scrollLeft( $this.scrollLeft() + x ); 
                    };
                case "both":
                    return function($this,x,y){ 
                        $this.scrollTop( $this.scrollTop() + y).scrollLeft(
                                         $this.scrollLeft() + x );
                    };
            }
        })();

        return this.each(function(){
            //disabling text selection
            $(this).disableTextSelect();
            
            //binding event-handlers
            $(this).bind("mousemove", eventHandlers.mousemove)
                   .bind("mouseup mouseout", eventHandlers.mouseupout)
                   .bind("mousedown", eventHandlers.mousedown);

            //adding class
            $(this).addClass("openhand");s
            
            //caching the scroll function.
            $(this).data("scrollFn", scrollFn);
        });
    };

})(jQuery);

That's all!

Demo

Configure the attached code in IIS and run the MapScroller.html page to see our plug-in in action.

If you think this article was useful, don't forget to vote.

**Coding is poetry**

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)