Category: JavaScript


jQuery.ganttView a Lightweight Gantt Chart for jQuery

June 11th, 2010 — 8:05am

I just wanted to drop a quick post about a new jQuery plugin that I’ve been working on for a recent project. As the title of the post implies it renders a simple HTML based Gantt chart using jQuery and the excellent date.js library. The screenshot below should give you an idea of what can be done with the plugin.





The current version is fairly stable and has been tested in FireFox 3.6, Safari 4, Chrome 5, and IE8 and works well in all those browsers. There are likely a few issues with it in IE7 and I wouldn’t even bother checking IE6, unless someone sends me a patch I have no interest in supporting IE6.


The source code and documentation can be found on GitHub: http://github.com/thegrubbsian/jquery.ganttView


Let me know what you think and if there are any specific features you’d like to see added.

4 comments » | JavaScript, jQuery

jQuery Plugin: tablePager

June 5th, 2009 — 10:38am

Here’s a quick table pager plugin for jQuery. Unlike a number of the other plugins out there, this one doesn’t alter the DOM tree of the table itself it just hides the rows that are not available for the current page. In this way you can still get access to all the elements of the table throughout the lifetime of the page.

Plugin Source:

(function($) {
    $.fn.tablePager = function(options) {
 
        /*
        options = {
        firstBtn: Element,
        prevBtn: Element,
        nextBtn: Element,
        lastBtn: Element,
        indicator: Element,
        sizeSelect: Element,
        pageSize: Number,
        onPageChange: Callback,
        onPageSizeChange: Callback
        };
        */
 
        var tbl = this;
        var defaults = { pageSize: 10 };
        var opts = $.extend(defaults, options);
 
        var pageIndex = 0;
        var pageSize = opts.pageSize;
        var rowCount = getRows().length;
        var pageCount = getPageCount();
 
        function init() {
            bindControlHandlers();
            updatePaging();
            updateIndicator();
        }
 
        function getRows() {
            return tbl.find("tbody tr");
        }
 
        function bindControlHandlers() {
            if (opts.firstBtn) { 
	        opts.firstBtn.unbind().bind("click", 
		    function() { changePage(0); }); 
            }
            if (opts.prevBtn) { 
		opts.prevBtn.unbind().bind("click", 
			function() { changePage(pageIndex - 1); }); 
	    }
            if (opts.nextBtn) { 
		opts.nextBtn.unbind().bind("click", 
			function() { changePage(pageIndex + 1); }); 
	    }
            if (opts.lastBtn) { 
		opts.lastBtn.unbind().bind("click", 
			function() { changePage(pageCount - 1); }); 
	    }
            if (opts.sizeSelect) { 
		opts.sizeSelect.unbind().bind("change", 
                    function() { changePageSize(parseInt($(this).val())); }); 
	    }
        }
 
        function getPageCount() {
            return (rowCount % pageSize) ? 
		Math.floor(rowCount / pageSize) + 1 : (rowCount / pageSize);
        }
 
        function changePage(toIndex) {
            if (toIndex >= 0 && toIndex <= (pageCount - 1)) {
                pageIndex = toIndex;
                updatePaging();
                updateIndicator();
                if (opts.onPageChange) {
                    opts.onPageChange.call(pageIndex);
                }
            }
        }
 
        function changePageSize(toSize) {
            pageSize = toSize;
            pageCount = getPageCount();
            updatePaging();
            updateIndicator();
            if (opts.onPageSizeChange) {
                opts.onPageSizeChange.call(pageSize);
            }
        }
 
        function updateIndicator() {
	    if (opts.indicator) {
            	opts.indicator.val((pageIndex + 1) + "/" + pageCount);
	    }
        }
 
        function updatePaging() {
            var start = pageIndex * pageSize;
            var end = start + pageSize;
            var i = 0;
            var rows = getRows();
            rows.each(function() {
                if (i >= start && i < end) {
                    $(rows[i]).show();
                } else {
                    $(rows[i]).hide();
                }
                i++;
            });
        }
 
        init();
 
        return {
            changePage: function(moveTo) { changePage(moveTo - 1); },
            getPageSize: function() { return pageSize; },
            getPageIndex: function() { return pageIndex; }
        };
    };
})(jQuery);

Usage:

var pager = $("#testTable").tablePager({
    firstBtn: $(".first"), // The element that pages to the first page
    prevBtn: $(".prev"), // The element that pages to the previous page
    nextBtn: $(".next"), // The element that pages to the next page
    lastBtn: $(".last"), // The element that pages to the last page
    indicator: $(".curpage"), // The input element that stores the current page (i.e. "1/5")
    sizeSelect: $(".pagesize"), // The select element that indicates the current page size
    pageSize: 5, // The initial page size
    onPageChange: function(newIndex) { alert(newIndex); }, // Page change callback
    onPageSizeChange: function(newSize) { alert(newSize); } // Page size change callback
});
 
pager.changePage(2); // Causes the page to change from outside the plugin
var pageIndex = pager.getPageIndex(); // Returns the current page index
var pageSize = pager.getPageSize(); // Returns the current page size

And here’s some example markup for the pager elements:

<span class="first">First</span>&nbsp;&nbsp;
<span class="prev">Prev</span>&nbsp;&nbsp;
<span class="next">Next</span>&nbsp;&nbsp;
<span class="last">Last</span>
<br><br>
<input type="text" class="curpage" />&nbsp;&nbsp;
<select class="pagesize">
    <option value="5">5</option>
    <option value="10">10</option>
    <option value="20">20</option>
</select>

As you can see it’s really simple and flexible. You can supply any elements you want to the pager. If you don’t supply a first and last button elements for example then those will be ignored, if you don’t specify an indicator then that will be ignored too. The most important thing is that your DOM stays intact, this allows you to continue to manipulate the “hidden” portions of the table (i.e. the stuff not on the current page) without having to access some crazy expando property that other pager plugins use to stash the un-shown portions of the table.

As always, I’d appreciate any feedback.

2 comments » | JavaScript, jQuery

Custom JavaScript Events with the Observer Pattern

January 28th, 2009 — 1:34pm

Anyone that’s worked with JavaScript in the browser knows that it’s all about events.  And in our daily work with the DOM we use the provided events like “click”, “change”, and “load” all over the place.  But what if you want to create new events for custom objects that might not even have any associated UI.  Unfortunately, the W3C standard for custom events in JavaScript has spotty implementation across browsers, or none at all in some cases.

So, what are we to do…well…here’s a simple implementation of the observer pattern that can help solve the lack of internal support for custom events.  Here’s the Observer “classes”:

var Observer = function() {
    this.observations = [];
};
 
var Observation = function(name, func) {
    this.name = name;
    this.func = func;
};
 
Observer.prototype = {
    observe: function(name, func) {
        var exists = this.observations.findAll(function(i) {
            return i.name == name && i.func == func; }).length > 0;
        if (!exists) { this.observations.push(new Observation(name, func)); }
    },
    unobserve: function(name, func) {
        this.observations.remove(function(i) {
            return i.name == name && i.func == func;
        });
    },
    fire: function(name, data, scope) {
        var funcs = this.observations.findAll(function(i) {
            return i.name == name; });
        funcs.forEach(function(i) { i.func.call(scope || window, data); });
    }
};

First thing you’ll notice is methods on the “observations” array like forEach() and findAll().  These methods are added by my set of JavaScript extensions found in a previous post.  You’ll also notice that this is very simple…so simple in fact that I’m not going to go through it line-by-line…I’m just going to demonstrate.  Here’s how you might use this object:

var Person = function() {
 
    this.name = "John Doe";
    this.age = 23;
    this.observer = new Observer();
 
    this.changeName = function(newName) {
        this.name = newName;
        this.observer.fire("nameChanged", newName);
    };
 
    this.changeAge = function(newAge) {
        this.age = newAge;
        this.observer.fire("ageChanged", newAge);
    };
};
 
var p1 = new Person();
var p2 = new Person();
 
p1.observer.observe("nameChanged", alertNameChanged);
p2.observer.observe("ageChanged", alertAgeChanged);
 
function alertNameChanged(data) { alert("name changed to: " + data); }
function alertAgeChanged(data) { alert("age changed to: " + data); }
 
p1.changeName("James Doeson");
p2.changeAge(35);

Super simple…create an observer, observe a few things by name, fire a few things by name.  So now you have a behavior very similar to custom events.  One thing to note is that you can change the scope of the callback by passing a third parameter to the observe() method.  So, if you want this to reference some different object within the callback function, just pass it in.

Comment » | JavaScript

Useful JavaScript Extensions

January 25th, 2009 — 5:24pm

Over the years I’ve collected a set of useful extensions to the Object, Array, and String constructs in JavaScript and I thought I might publish them.  They are all pretty simple but it’s a pain to have to write these over and over for each new project so I just compiled them into on file and off we go.  There are probably improvements that could be made to these methods and new ones to be added so please feel free to share your ideas and I’ll incorporate them back in.

Here is a listing of all the methods that this file adds:

ObjUtil.isString(Object)
ObjUtil.isNumber(Object)
ObjUtil.isBoolean(Object)
ObjUtil.isArray(Object)
Object.isFunc(Object)
ObjUtil.isDate(Object)
ObjUtil.isRegEx(Object)

ObjUtil.addOverload(Object, String, Function)
This function adds some pseudo function overloading to JavaScript.  The first argument is the name of the function you’d like to expose and the second is the function to be called using that name.  The limitation here is that the only way to distinguish between method implementations is by the number of arguments (JavaScript does not discriminate by type or name).  Thanks to John Resig for the inspiration for this method.

var obj = { };
obj.addOverload("doSomething", function(a) { ... });
obj.addOverload("doSomething", function(a, b) { ... });
obj.doSomething(a);
obj.doSomething(a, b);

ObjUtil.compareTo(Object, Object)
A generic comparison method that can be used in sorting functions.  Handles differences in lexicographical order of strings by converting to lower case.

Function.partial()
Adds partial function capability through simple currying.  Thanks to Oliver Steele’s Functional.js library for this method.

var add = function(a, b, c) { return a + b + c; };
var addPart = add.partial(1, undefined, 3);
alert(addPart(2)); // alerts 6

String.toBoolean()
Converts the following values to true/false: “true” or “1″.

Array.copy()
Copies an array’s contents to a new array.  Keep in mind that if the array isn’t made of simple types like string, number, or boolean that the new array will contain references to the old array’s objects.  However, using the copy facilitates sorting and other mutations without affecting the original array.

Array.forEach(Function)
Takes a in a function and calls that function once for every item in the array.

var arr = [1,2,3];
arr.forEach(function(i) { alert(i); });

Array.find(Function)
Takes a function that returns a boolean and returns the item that matches the functions criteria.  If more than one item match the first one is returned.

var arr = [{name:"Bill", age:17}, {name:"Bob", age:27}, {name:"Brian", age:34}];
var item = arr.find(function(i) { return i.name == "Bob"; });

Array.findAll(Function)
Takes a function that returns a boolean and returns an array of all items matching the functions criteria.

var arr = [{name:"Bill", age:17}, {name:"Bob", age:27}, {name:"Brian", age:34}];
var items = arr.findAll(function(i) { return i.age > 25; });

Array.contains(Object)
Tests if an array contains a certain item, returns a boolean.

Array.distinct()
Returns a new array of distinct values from the source array.

Array.min()
Returns the minimum alpha-numeric value in the array.

Array.max()
Returns the maximum alpha-numeric value in the array.

Array.first()
Returns the first item in an array or null if the array is empty.

Array.last()
Returns the last item in an array or null if the array is empty.

Array.sortAscending()
Sorts the array in ascending numerical or lexicographical order depending on if the first item in the array is a number or not.

Array.sortDescending()
Sorts the array in descending numerical or lexicographical order depending on if the first item in the array is a number or not.

Array.randomize()
Randomizes the order of the array.

Array.count(), Array.count(Function)
Returns the number of items in the array.  If a function is passed in it counts the items matching the functions criteria.

var birds = [{name:"Bill", birdType:"ostrich"}, {name:"Bob", birdType:"penguin"}, {name:"Brian", birdType:"penguin"}];
var penguinCount = birds.count(function(i) { i.birdType == 'penguin'; });

Array.except(Array)
Takes two Array objects and returns a new array of only the items that are unique to both of the first two.

var arrA = [1,2,3,4,5];
var arrB = [3,4,5,6,7,8];
var itmes = arrA.except(arrB);

Array.intersect(Array)
Takes two Array objects and returns a new array of only the items shared between the first two.

var arrA = [1,2,3,4,5];
var arrB = [3,4,5,6,7,8];
var itmes = arrA.intersect(arrB);

Array.average()
Returns the average numeric value in the array.

Array.sum()
Returns the sum of the numeric values in the array.

Array.union(Array)
Returns a new array after joining two others but does not include duplicates.

var arrA = [1,2,3,4,5];
var arrB = [3,4,5,6,7,8];
var newArr = arrA.union(arrB);

Array.safePush(Object)
Adds an item to an array but only if that item doesn’t already exist in the array.

Array.getRandom()
Returns a random item from the array.

Array.insertAt(Number, Object)
Adds an item to an array at the specified position (first parameter).

Array.remove(Function)
Removes items matching the functions criteria from the array.

var arr = [{id:1, name:"Bob"}, {id:2, name:"Tom"}, {id:3, name:"Jim"}];
arr.remove(function(i) { return i.name == "Tom"; });

Array.indexOf(Object)
Returns the index of the item in the array or -1 if the item isn’t found.

Download JavaScriptExtensions.zip

Note: There is a “tests.html” file which contains examples and unit tests for all of these extensions.  The unit tests are written for FireUnit for FireBug.

3 comments » | JavaScript

jQuery vs. the ASP.NET Update Panel Revisited – Event Delegation

December 5th, 2008 — 1:17pm

Recently I was again struggling with the ASP.NET Update Panel killing my JavaScript interactions because it replaces the DOM elements it surrounds on every refresh.  My previous solution for this was simply to rebind those events after each Update Panel refresh was completed.  This works perfectly well, but it always seemed a little heavy handed to me.

So in digging into this issue a little more, I came upon a few posts that talked about using event delegation to solve this problem (thanks to everyone out there who wrote about delegation, cause you're smarter than I am).  The basic idea is that you attach events at a higher level in the DOM tree and use the DOM's event bubbling mechanism to catch them.  For example, let's say we have the following ASPX snippet:

<div id="container">
    <ul>
        <asp:UpdatePanel>
            <li class="listItem">Item One</li>
            <li class="listItem">Item Two</li>
            <li class="listItem">Item Three</li>
            <li class="listItem">Item Four</li>
        </asp:UpdatePanel>
    </ul>
</div>

Now let's assume that you've done this somewhere in JavaScript:

function bindLiEvents() {
    $("li.listItem").bind("click", function() { alert($(this).html()); });
}

When this JavaScript is executed jQuery loops over all the found elements and individually attaches an event handler to each.  So already there is an inherent performance gain to be had by not doing this looping.  Furthermore there is now the problem with the Update Panel.  If the user takes some action that causes the panel to refresh, then the four <li> elements are destroyed and recreated and unless I call bindLiEvents again after the refresh I loose this functionality.

Enter the event delegation concept.  Why not attach my handler to a DOM element that's outside the Update Panel – the <div> element in this case.  This may seem a bit counter intuitive, but if you're aware of how event bubbling works in the DOM you can see how this might work.

Event bubbling simply means that events at lower levels in the DOM tree are propagated to higher levels.  For example when an <li> is clicked in our example, the click event is also fired on the <ul> and the <div> and beyond.  We can use this behavior to our advantage in this instance because event bubbling is intrinsic to DOM elements – when new elements are added they automatically exhibit this behavior.

So now let's look at how we might attach an event to the <div> element:

function bindLiEvents() {
    $("#container").delegate("click", "li.listItem",
        function() { alert($(this).html()); });
}

Don't worry about where the delegate method comes from – it's not native to jQuery but I'll get to that.  What's happening here is that I'm binding a very generic event handler to the <div> element, and telling that handler to listen for click events on elements that match the "li.listItem" selector.  So now, when an event bubbles up from an <li> element the handler on the <div> fires, tests if the event originated on an <li> with a class of "listItem" and then calls the function callback.

There are advantages to using the approach.  First, event handlers are attached only once, and only at a single point which saves time when doing the initial bind.  Second, any new elements added to the Update Panel will automatically fire the delegated event handler – no rebind is required.

So if you want to try this out for yourself, here is a jQuery plugin for the delegate method:

(function ($) {
    var bubbledEvents = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
        'mouseover', 'mouseout', 'keydown', 'keypress', 'keyup'];
    var allowedEvents = { };
    $.each(bubbledEvents, function(idx,event) { allowedEvents[event] = true; });
    $.fn.extend({
        delegate: function (event, selector, func) {
            return $(this).each(function () {
                if (allowedEvents[event])
                    $(this).bind(event, function (e) {
                        var el = $(e.target), result = false;
                        while (!$(el).is("body")) {
                            if ($(el).is(selector)) {
                                result = func.apply($(el)[0], [e]);
                                if (result === false) { e.preventDefault(); }
                                return;
                            }
                            el = $(el).parent();
                        }
                    });
            });
        },
      
  undelegate: function (event) { return $(this).each(function () {
            $(this).unbind(event); }); }
    });
})(jQuery);

2 comments » | JavaScript, jQuery

Back to top