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.
$.fn.Method1 = function(){
};
$("div").Method1();
bind()
allows you to bind methods to events.
$("input[type=button]").bind("mouseover mouseenter", function(){
alert('hi');
});
data()
allows you to store any custom data as key-value pairs.
$(this).data("mouseposition", {x:200, y:300});
addClass()
allows you to add CSS class(es) dynamically.
$("p").addClass("class1 class2");
removeClass()
allows you to remove CSS class(es).
$("p").removeClass("class1");
scrollTop()
scrolls the element vertically.
$("myDiv").scrollTop(20);
scrollLeft()
scrolls the element horizontally.
$("myDiv").scrollLeft(20);
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:
$("#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:
$.fn.mapScroll = function(direction){
};
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.
(function($){
$.fn.mapScroll = function(direction){
};
})(jQuery);
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:
var x = 25;
(function(){
var x = 35;
alert(x);
})();
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.
(function($){
$.fn.mapScroll = function(direction){
};
})(jQuery);
Create event handlers for the mouseup
, mousedown
, and mousemove
events, and wrap them in a private object eventHandlers
.
(function($){
var eventHandlers = {
mousemove: function(e){
},
mouseup: function(e){
},
mousedown: function(e){
}
};
$.fn.mapScroll = function(direction){
};
})(jQuery);
Our skeleton is pretty much ready. Let's finish up the public method mapScroll
first.
$.fn.mapScroll = function(direction){
return this.each(function(){
$(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:
$("p").show().addClass("paragraphstyle").mapScroll("both");
Without chaining, we have to call the methods individually:
$("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:
$.fn.mapScroll = function(direction){
return this.each(function(){
$(this).disableTextSelect();
$(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:
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:
var scrollFn = function($this,x,y){
$this.scrollTop( $this.scrollTop() + y );
}
Likewise, if it is "both", then
var scrollFn = function($this,x,y){
$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:
$(this).data("scrollFn", scrollFn);
Now we can get scrollFn
anywhere in our plug-in by:
var scrollFn = $(this).data("scrollFn");
Here is the complete implementation of the mapScroll
method:
$.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(){
$(this).disableTextSelect();
$(this).bind("mousemove", eventHandlers.mousemove)
.bind("mouseup", eventHandlers.mouseup)
.bind("mousedown", eventHandlers.mousedown);
$(this).addClass("openhand");
$(this).data("scrollFn", scrollFn)
});
};
Let's see the event handlers now.
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.
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.
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.
$(this).bind("mouseup mouseout", eventHandlers.mouseupout);
Here is our complete code:
(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){
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(){
$(this).disableTextSelect();
$(this).bind("mousemove", eventHandlers.mousemove)
.bind("mouseup mouseout", eventHandlers.mouseupout)
.bind("mousedown", eventHandlers.mousedown);
$(this).addClass("openhand");s
$(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**