Wednesday, January 21, 2009

Object Oriented Javascript Programming

Example of using closure to access 'this' object in a different context, e.g. in a global context when using setTimeout or setInterval.
//Example of using closures to access 'this' object in a different context,
//e.g. in a global context when using setTimeout()
function Person(name, count) {
 this.name = name;
 this.count = count;
}
Person.prototype.countDown = function() {
 var self = this;
 var closureFunc = function() {
     console.log(self.name + ": " + self.count);

     self.count–;

     if (self.count > 0) {
         self.countDown();
     }
 }
 setTimeout(closureFunc, 500);
}
The usage is pretty simple:
  1. just copy the code to a file or run the code
  2. then instantiate a timer by hooking it to an existing div tag on the page
  3. then invoke two functions on the timer object: timer.createHTML(); timer.start();
click here to try it.
Example Usage:
Now here's the javascript for the CustomTimer.
/*
    Usage:

    var myCt = new com.clt.CustomTimer('myCt', 20, 'div1');
    myCt.createHTML();      //creates tabular div progress bar
        [and/or]
    myCt.createHTML2();     //creates continuous div progress bar
    myCt.addTickEventHandler(function(txt, timerObj) { alert(txt+'\n'+timerObj.id); });
    myCt.addTickEventHandler(function(txt, timerObj) { fireAjaxWebRequest(....); })
    
    //optionally add extra event handlers to the timer div
    //$addHandler($get('div1'), 'click', com.clt.createDelegate(myCt, myCt.timerDiv_onclick));

    myCt.start();
    myCt.pause();
    myCt.resume(); or myCt.tickTock();

    myCt.reset();
    myCt.start();
    
    To reInitialize with a different time and widthFactor:
    ---------------------------------------------------------------------
    myCt.reInitializeTimer(15000, 2);
            OR
    reInitializeTimer = function(queryIntervalInMS, widthFactor) {
        var numTicks = queryIntervalInMS / 1000;
        var timerId = _timerObject.id;
        _timerObject.dispose(false);
        _timerObject = new com.clt.CustomTimer(timerId, numTicks, 'timerDivId');
        if (_timerObject) {
            _timerObject.widthFactor = widthFactor;
            _timerObject.createHTML2();
            _timerObject.addTickEventHandler(_timerEventHandler);
            _timerObject.start();
        }
    }
    
 */
var com;
if (typeof com == 'undefined' || com == null) {
    com = {};
}
if (typeof com.clt == 'undefined') {
    com.clt = {};
}
//public static variable
com.clt.numCustomTimerObj = 0;
//Utility function
com.clt.createDelegate = function(instance, method){
    return function() {
        return method.apply(instance, arguments);
    }
}
//function in global namespace, usage: var divEl = dget('div1');
function dget(id){
    return document.getElementById(id);
}



/*********************************************************************************

Implements:
 Observer design pattern: listeners/eventHandlers are registered, events fired and
    corresponding listeners/eventHandlers are invoked.
TODO:
    use a logger - if IE use logging in a txtbox,
        if firefox & firebug use console.log(),
        if ie & 'asp.net ajax' use sys.debug.trace()
        
Dependencies:
    - asp.net ajax library:
        - $clearHandlers
        - $addHandler
        - $removeHandler
    - homegrown:
        - log("message","optional_log_level=debug") 

*********************************************************************************/

/*
valid values for CustomTimer.state =
 started, paused, stopped, created, initialized, disposed

cycle:
 - created (when constructed)
 - initialized (when everything is ready, including html)
 - started
 - paused (optional)
 - stopped (when reset)
 - disposed
*/

com.clt.CustomTimer = function(id, numTicks, divId, tableId, tableRowId) {
    com.clt.numCustomTimerObj++;
    this.id = id;
    this.currentTick = 0;
    this.numTicks = numTicks;
    this.divId = divId;
    this.divEl = dget(this.divId);
    this.divElEventHandlers = new Array();

 this.backgroundColor = 'gray';
 this.height = '10px';
 this.activeColor = 'red';

 this.tableCellWidth = '1px';
    this.tableId = tableId;
    this.tableRowId = tableRowId;
    this.tableEl = null;
    this.tableRowEl = null;

    this.timerProg;
    this.updateIntervalInMs = 1000;

    this.progDivContainerEl = null;
    this.progDivContainerId = null;
    this.progressDivId = null;
    this.progressDivEl = null;
    //multiplier for progress bar when using div instead of table
    this.widthFactor = 2;

    this.tickEventHandlers = new Array();
    this.startTime = null;
    this.state = 'created';
 this.prevTickEventTime = null;
 this.totalTickEvents = 0; 
}

com.clt.CustomTimer.prototype.update = function() {

}

com.clt.CustomTimer.prototype.addTickEventHandler = function(handler) {
    var index = this.tickEventHandlers.length;
    this.tickEventHandlers[index] = handler;
    return index;
}

// removes by index or the function eventHandler Object
com.clt.CustomTimer.prototype.removeTickEventHandler = function(indexOrFunction) {
    if (typeof indexOrFunction == 'number') {
        if (indexOrFunction >= this.tickEventHandlers.length) {
            //error
            return null;
        }
        var f = this.tickEventHandlers[indexOrFunction];
        this.tickEventHandlers[indexOrFunction] = null;
        return f;
    } else {
        for (var i = 0; i < this.tickEventHandlers.length; i++) {
            if (this.tickEventHandlers[i] == indexOrFunction) {
                var f = this.tickEventHandlers[i];
                this.tickEventHandlers[i] = null;
                return f;
            }
        }
    }
    return null;
}


/*
    @param data:Object = {'startTime':this.startTime, 'prevTickTime':prevTickTime, 'endTime':new Date()}
*/
com.clt.CustomTimer.prototype.raiseTickEvent = function() {
 this.totalTickEvents++;
 var endTime = new Date();
 this.currentTick = 0;
 this.resetUI(); 
 this.state = 'started';
 
 //var dataObj = {'startTime':this.startTime.getTime(), 'prevTickTime':prevTickTime.getTime(), 'endTime':endTime.getTime()}
 
 var txt = "start time: " + this.startTime + "\nprev tick time:"+this.prevTickEventTime
  +"\nend time:" + endTime + "\nTotalTickEvents: " + this.totalTickEvents;
    this.prevTickEventTime = endTime; 
 for (var i = 0; i < this.tickEventHandlers.length; i++) {
        var f = this.tickEventHandlers[i];
        if (f) {
            if (f != null && typeof f == 'function') {
    var self = this;
                f(txt, self);
            }
        }
    }
}

com.clt.CustomTimer.prototype.setNumTicks = function(totalTicks) {
    this.numTicks = totalTicks;
    this.reset();
}

com.clt.CustomTimer.prototype.setUpdateInterval = function(inMilliseconds){
    this.updateIntervalInMs = inMilliseconds;
}

//this creates a div progress bar as opposed to a tabular prog bar
com.clt.CustomTimer.prototype.createHTML2 = function() {
    if (this.divEl) {
        if (this.progDivContainerEl) {
            //do nothing
        } else {
            if (this.progDivContainerId) {
                //do nothing, use specified divId
            } else {
                //define a new ID for this div tag
                this.progDivContainerId = this.id + "_Container";
            }

            this.progDivContainerEl = dget(this.progDivContainerId);
            if (this.progDivContainerEl) {
                // do nothing, use existing div
            } else {
                // create a new div, append to parent
                this.progDivContainerEl = document.createElement("div");
                this.progDivContainerEl.id = this.progDivContainerId;
                this.divEl.appendChild(this.progDivContainerEl);
            }

            this.progDivContainerEl.style.backgroundColor = this.backgroundColor;
            this.progDivContainerEl.style.width = "" + (this.numTicks * this.widthFactor) + "px";
            this.progDivContainerEl.style.height = this.height;
            this.progDivContainerEl.style.border = "1px solid black";
        }

        //.....

        if (this.progressDivEl) {
            //do nothing
        } else {
            if (this.progressDivId) {
                //do nothing
            } else {
                this.progressDivId = this.id + "_ProgressBar";
            }

            this.progressDivEl = dget(this.progressDivId);
            if (this.progressDivEl) {
                //do nothing, use existing div
            } else {
                //create a new div, append to parent
                this.progressDivEl = document.createElement("div");
                this.progressDivEl.id = this.progressDivId;
                this.progDivContainerEl.appendChild(this.progressDivEl);
            }

            this.progressDivEl.style.backgroundColor = this.activeColor;
            this.progressDivEl.style.width = "0px";
            this.progressDivEl.style.height = "10px";
        }
        this.reset();
        this.state = 'initialized';
    } else {
        //log error
        if(typeof log != 'undefined') {
   log('Unable to createHTML2(), DIV element is null','warn');
  }
    }
}

//creates a tabular progres bar with discrete visible timer steps/ticks
com.clt.CustomTimer.prototype.createHTML = function() {
    if (this.divEl) {
        this.tableEl = dget(this.tableId);
        if (this.tableEl) {
            //do noting
        } else {
            if (this.tableId) {
                //do nothing
            } else {
                this.tableId = this.id + "_Table";
            }

            this.tableEl = dget(this.tableId);
            if (this.tableEl) {
                //do nothing
            } else {
                this.tableEl = document.createElement("table");
                this.tableId = this.id + "_Table";
                this.divEl.appendChild(this.tableEl);
            }
            this.tableEl.id = this.tableId;
            this.tableEl.style.backgroundColor = this.backgroundColor;
            this.tableEl.style.height = this.height;
        }

        //......


        this.tableRowEl = dget(this.tableRowId);
        if (this.tableRowEl) {
            //do nothing
        } else {
            if (this.tableRowId) {
                //do nothing
            } else {
                this.tableRowId = this.id + "_TableRow";
            }

            this.tableRowEl = dget(this.tableRowId);
            if (this.tableRowEl) {
                //do nothing
            } else {
                this.tableRowEl = document.createElement("tr");
                this.tableRowEl.id = this.tableRowId;
                this.tableEl.appendChild(this.tableRowEl);
            }
            this.tableRowEl.style.backgroundColor = this.backgroundColor;
        }

        if (this.tableEl && this.tableRowEl) {
            this.reset();
            this.state = 'initialized';
            //this.createRows();
        } else {
            //serious error - log message
        }
    } else {
        //error
        if(typeof log != 'undefined') {
   log('Unable to createHTML2(), DIV element is null','warn');
  }
    }
}

//this updates the progress bar after a 1 sec, i.e. if numTicks=10, then timer will stop at exactly the 10th second mark
com.clt.CustomTimer.prototype.start = function() {
    if (this.state == 'paused') {
        this.resume();
        return;
    }        
    this.startTime = new Date();
    var f = com.clt.createDelegate(this, this.tickTock);
    //this.timerProg = setTimeout(f, this.updateIntervalInMs);
    f(); //execute a tick immediately
    this.timerProg = setInterval(f, this.updateIntervalInMs);
    this.state = 'started';
}

//this updates the progress bar immediately, i.e. if numTicks=10, then timer will stop at 11th second mark
com.clt.CustomTimer.prototype.tickTock = function() {
    if (this.numTicks) {
        if (this.currentTick <= this.numTicks) {
            if (this.startTime == null) {
                this.startTime = new Date();
            }

            if (this.tableRowEl) {
                var el1 = this.tableRowEl.getElementsByTagName("td")[this.currentTick];
                if (el1)
                    el1.style.backgroundColor = this.activeColor;
            }

            if (this.progressDivEl) {
                this.progressDivEl.style.width = "" + (this.widthFactor * (this.currentTick)) + "px";
            }

            if (this.currentTick == this.numTicks) {
                this.raiseTickEvent();
                return;
                //clearInterval(this.timerProg);
            }

            this.currentTick++;
            //var f = com.clt.createDelegate(this, this.tickTock);
            //this.timerProg = setTimeout(f, this.updateIntervalInMs);
        } else {
            //clearInterval(this.timerProg);
        }
    }
}

com.clt.CustomTimer.prototype.resume = function() {
    var f = com.clt.createDelegate(this, this.tickTock);
    this.tickTock();
    this.timerProg = setInterval(f, this.updateIntervalInMs);
    this.state = 'started';
}

com.clt.CustomTimer.prototype.pause = function() {
    if (this.numTicks) {
        //clearTimeout(this.timerProg);
        clearInterval(this.timerProg);
        this.state = 'paused';
    } else {

    }
}

com.clt.CustomTimer.prototype.reInitializeTimer = function(tickIntervalInMS, widthFactor) {
    var numTicks = tickIntervalInMS / 1000;
    this.reset();
    this.numTicks = numTicks;
    this.widthFactor = widthFactor;
    this.createHTML2();
    this.start();
}

com.clt.CustomTimer.prototype.resetUI = function() {
 if (this.tableRowEl) {
  this.tableRowEl.innerHTML = "";
  this.createRows();
 }
 if (this.progressDivEl) {
  this.progressDivEl.style.width = "0px";
 }
}

com.clt.CustomTimer.prototype.reset = function() {
    if (this.numTicks) {
        this.currentTick = 0;
        //this.startTime = null;
        clearTimeout(this.timerProg);
  this.resetUI();
        this.state = 'stopped';
    } else {
        //log error msg
    }
}

com.clt.CustomTimer.prototype.createRows = function() {
    if (this.tableRowEl && this.numTicks) {
        for (var i = 0; i < this.numTicks; i++) {
            var tdElem = document.createElement("td");
            tdElem.style.backgroundColor = this.backgroundColor;
            tdElem.style.width = this.tableCellWidth;
            tdElem.innerHTML = ''; //' '
            this.tableRowEl.appendChild(tdElem);
        }
    } else {
        //log error msg
    }
}

com.clt.CustomTimer.prototype.dispose = function(removeParentDiv) {
    //remove any eventHandlers for DOM nodes

    //set reference to event handlers to null
    if (this.tickEventHandlers) {
        this.tickEventHandlers.splice(0, this.tickEventHandlers.length);
        this.tickEventHandlers = null;
    }

    //remove html DOM elements & set reference to DOM nodes to null
    if (this.progDivContainerId) {
        var el = dget(this.progDivContainerId);
        if (el) el.parentNode.removeChild(el);
        el = null;
    }
    if (this.tableId) {
        var el = dget(this.tableId);
        if (el) el.parentNode.removeChild(el);
        el = null;
    }

    this.tableEl = null;
    this.progDivContainerEl = null;
    this.progressDivEl = null;
    this.tableEl = null;
    this.tableRowEl = null;
    if (removeParentDiv) {
        if (this.divEl)
            this.divEl.parentNode.removeChild(this.divEl);
        var domEl1 = dget(this.divId);
        if (domEl1) {
            domEl1.parentNode.removeChild(domEl1);
            domEl1 = null;
        }            
    }
    this.divEl = null;

    ///

    if (this.timerProg) {
        clearInterval(this.timerProg);
        this.timerProg = null;
    }

    //set everything else to null, not necessary
    this.id = null;
    this.currentTick = null;
    this.numTicks = null;
    this.updateIntervalInMs = null;
    this.widthFactor = null;
    this.startTime = null;

    this.divId = null;
    this.tableId = null;
    this.tableRowId = null;
    this.progDivContainerId = null;
    this.progressDivId = null;
    this.state = 'disposed';
    
    return true;
}

com.clt.CustomTimer.prototype.timerDiv_onclick = function(event) {
    if (!this) {
        alert('Timer is not available');
        return;
    }
    var timerState = this.state;
    if (timerState == null) {
        alert('Timer state is NULL');
        return;
    }

    var userConfirm = false;
    if (timerState == 'stopped') {
        userConfirm = confirm('Are you sure you want to START the timer?');
        if (userConfirm) {
            this.reset();
            this.start();
        }
    } else if (timerState == 'started') {
        userConfirm = confirm('Are you sure you want to PAUSE the timer?');
        if (userConfirm) {
            this.pause();
        }
    } else if (timerState == 'paused') {
        userConfirm = confirm('Are you sure you want the timer to CONTINUE?');
        if (userConfirm) {
            this.start();
        }
    } else if (timerState == 'disposed') {
        alert('Timer is not available');
    } else if (timerState == 'created') {
        this.createHTML2();
        //add event handlers
        this.start();
    } else if (timerState == 'initialized') {
        this.start();
    } else {
        alert('Invalid timer state[' + timerState + '], doing nothing.');
    }
}

//============================== END CLASS ==========================

No comments:

Post a Comment