﻿
/* 
	HistoryNavigator : A customizable history navigator that implements the basic behaviour of history conroling and can be customized with event handlers and evaluation functions, and other options.
	
	Author: Alaa Sarhan (02-18-2011 Saydnaya, Syria) <sarhan.alaa@duetotech.com> <www.duetotech.com>.
	Version: 1.0.0
	
	I'm glad to offer this script which can be used any way and any where while the authority and version information are kept along with it.
	
	I'm also glad to recieve suggestions and fixes on <sarhan.alaa@duetotech.com>
	
	Enjoy it.


* Concept
 
 
* ProtoType (for developers)
 	
 	each instance of HistoryNavigator will have the following prototype:
 	
 	- Properties:
 	=============
 		-> fnReqCmpr : a function that recieves two requests as arguments and returns the result of equality test of those two requests. By default (if not specified), this function pretends that the request is a string. Therfore, as equality test, it compares if the two requests are equalt strings;
 		------------------------------------------------------------
 		-> fnNav : a function that is called with a request passed as an argument. This function is responsible for navigating to the passed request. By default, it will pretend that the request is a string and it will set (window.location) property to that string (the request).
 		------------------------------------------------------------
 		-> evBefNav : an event handler that will be called right before the navigation function fnNav would be called. if this handler returns false it will stop the navigation. It will be passed the number of navigation steps (negative is back, positive is forward, 0 is refresh) and a references to the HistoryNavigator object.
 		------------------------------------------------------------
		-> evAftNav : an event handler that will be called right before the navigation function fnNav would be called. It will be passed the number of navigation steps (negative is back, positive is forward, 0 is refresh) and a references to the HistoryNavigator object.
 		------------------------------------------------------------
 		-> evBefUpdt : an event handler that will be called right before an update would happen to the navigator history (i.e. before altering the back and forward history). If this handler returns false, it will cancel the update. This handler will be passed the update request and a references to the HistoryNavigator object.
 		------------------------------------------------------------
 		-> evAftUpdt : an event handler that will be called right after an update happens to the navigator history (i.e. after altering the back and forward history). This handler will be passed the update request and a references to the HistoryNavigator object.
 		------------------------------------------------------------
 		-> evBefClr : an event handler that will be called right before any clearing of the history might happen (i.e. clearing back or forward history, or both of them). If it returns false, the clear will be cancelled. It will be passed the type of the clear ('back', 'forward', 'all') and a reference to the HistoryNavigator object.
 		------------------------------------------------------------
 		-> backDepth : this property specifies the max length of back entries that will be kept in the history. Default is 30.
 		-> forwardDepth : this property specifies the max length of forward entries that will be kept in the history. Default is backDepth.
 		
 		-> back : back history container which is now implemented using Arrays.
 		
 		-> forward : forward history container which is now implemented using Arrays.
 		
 		-> current : current request container.
 		
 		-> refreshAutoDetect : a specifier that indicates whether to automatically detect refresh requests and ignore them or not. Default to true;
 		
	==================================================================================================
 		
 		
* Interface
 
 	- Methods
	=========
		-> Update(request): Updates the history with the given request (which is ment to be the current request).
			> Raises: evBefUpdt, evAftUpdt
			> Returns: true on success, or string describing the error.
			> Calls: fnReqCmpr
		
		-> Go(steps): Navigates in the history with the given number of steps. negative numbers will cause nivigation in the back history, and positive numbers will cause nivigation in the forward history, and 0 will cause a refresh of the current request.
			> Raises: evBefNav
			> Returns: true on success, or string describing the error.
			> Calls: fnNav
			
		-> Back(): Calls Go(-1)
		
		-> Forward(): Calls Go(1).
		
		-> Refresh(): Calls Go(0).
		
		-> Clear(what): Clears history depending on what value ('back', 'forward', 'all'). default is 'forward'.
			> Raises: evevBefClr.
			> Returns: true on success, or string describing the the error.
			
		-> BackLength(): Returns the number of entries in the back history.
		
		-> ForwardLength() : Returns the number of entries in the forward history.
		
		-> Find(req, [where]): Finds a request in the history. Optionally, search may be specified with the 'where' argument which can be ('back', 'forward', 'both') and default is 'both'.
			> Returns: the number of steps in the history to reach the found request, or 0 if it was not found.
			> Calls: fnReqCmpr
			
		-> HistoryNavigator (constructor): If used with New, it returns a new instance of HistoryNavigator.
			> Params:
				- fnRequestComparer : The request comparer function. It recieves two requests and returns whether they are equal or not.
				- fnNavigator : The navigation function. It is responsible to navigate to the passed request to it.
				- evOnBeforeNavigation : An event handler that will be fired right before any navigation.
				- evOnAfterNavigation : An event handler that will be fired right after any navigation.
				- evOnBeforeUpdate : An event handler that will be fired right before updating the history (altering the history).
				- evOnAfterUpdate : An event handler that will be fired right after updating the history.
				- evOnBeforeClear : An event handler that will be fired right before clearing the history. (with the clear method).
				 - backDepth : The max length of the back history.
				 - forwardDepth : The maxlength of the forward history.
				 - refreshAutoDetect : Specifies whether to automatically detect duplicate requests between the current and the update request and to avoid the update or not.
 */


// Implementation

// Constructor
HistoryNavigator = function( fnRequestComparer, fnNavigator, evOnBeforeNavigation, evOnAfterNavigation, evOnBeforeUpdate, evOnAfterUpdate, evOnBeforeClear, backDepth, forwardDepth, refreshAutoDetect)
{
	if(typeof(fnRequestComparer) == "function")
		this.fnReqCmpr = fnRequestComparer;
		
	if(typeof(fnNavigator) == "function")
		this.fnNav = fnNavigator;
		
	if(typeof(evOnBeforeNavigation) == "function")
		this.evBefNav = evOnBeforeNavigation;
		
	if(typeof(evOnAfterNavigation) == "function")
		this.evAftNav = evOnAfterNavigation;
		
	if(typeof(evOnBeforeUpdate) == "function")
		this.evBefUpdt = evOnBeforeUpdate;
		
	if(typeof(evOnAfterUpdate) == "function")
		this.evAftUpdt = evOnAfterUpdate;
		
	if(typeof(evOnBeforeClear) == "function")
		this.evBefClr = evOnBeforeClear;
		
	if(typeof(backDepth) == "number")
		this.backDepth = backDepth;
		
	if(typeof(forwardDepth) == "number" && forwardDepth < backDepth)
		this.forwardDepth = forwardDepth;
	else
		this.forwardDepth = backDepth;
		
	if(typeof(refreshAutoDetect) == "boolean")
		this.refreshAutoDetect = refreshAutoDetect;
}


// Properties
HistoryNavigator.prototype.backDepth = 30;
HistoryNavigator.prototype.forwardDepth = 30;
HistoryNavigator.prototype.back = [];
HistoryNavigator.prototype.forward = [];
HistoryNavigator.prototype.current = null;
HistoryNavigator.prototype.refreshAutoDetect = true;

// Evaluators
HistoryNavigator.prototype.fnReqCmpr = function(req1, req2) { return (typeof(req1) != 'string' || typeof(req2) != 'string')? false : (req1 == req2) };

HistoryNavigator.prototype.fnNav = function(req){ if(typeof(req) != "string") return "Request is not a string."; else { window.location = encodeURI(req); return true;} }

// Events
HistoryNavigator.prototype.evBefNav = null;
HistoryNavigator.prototype.evAftNav = null;
HistoryNavigator.prototype.evBefUpdt = null;
HistoryNavigator.prototype.evAftUpdt = null;
HistoryNavigator.prototype.evBefClr = null;

// Methods
HistoryNavigator.prototype.Update = function(req)
{
	// Checking Arguments
	if(typeof(req) == "undefined" || req == null)
		return -1;
	
	// Refresh Auto Detect
	if(this.refreshAutoDetect && this.fnReqCmpr(this.current, req))
		return true;
	
	// Raising evBefUpdt event
	if(typeof(this.evBefUpdt) == "function" )
		if(!this.evBefUpdt(req, this))
			return -2;
		
	// Altering Back History
	if(this.current != null)
	{
		while(this.back.length > this.backDepth)
			this.back.shift();
		this.back.push(this.current);
	}
	
	// Altering Current
	this.current = req;
	
	// Altering Forward History
	this.forward = [];
	
	// Raising evAftUpdt
	if(typeof(this.evAftUpdt) == "function")
		this.evAftUpdt(req, this);
	
	// Return
	return true;
}

HistoryNavigator.prototype.Go = function(steps)
{
	// Checking Arguments
	if(typeof(steps) == "undefined")
		return -1;
	if(typeof(steps) != "number")
		steps = Number(steps);
		
	// Checking Existence of History Element and Finding the Request
	req = null;
	if(steps == 0) // refresh
	{
		if(this.current == null)
			return -4;
			
		// Raising evBefNav event
		if(typeof(this.evBefNav) == "function")
			if(!this.evBefNav(steps, this)) return -2;
			
		req = this.current;
	}
	else
	{
		st = Math.abs(steps);
		
		if(steps > 0 && this.forward.length >= st)
		{
			rec = this.back;
			snd = this.forward;
			maxLen = this.backDepth;
		}
		else if(steps < 0 && this.back.length >= st)
		{
			rec = this.forward;
			snd = this.back;
			maxLen = this.forwardDepth;
		}
		else
			return -4;
		
		// Raising evBefNav event
		if(typeof(this.evBefNav) == "function")
			if(!this.evBefNav(steps, this)) return -2;
		
		// Altering History
		while((rec.length + st) > maxLen)
			rec.shift();
			
		rec.push(this.current); st--;
		while(st > 0)
			rec.push(snd.pop());
			
		req = snd.pop();
	}
		
	// Navigating
	this.current = req;
	this.fnNav(req);
	
	// Raising evAftNav event
	if(typeof(this.evAftNav) == "function")
		this.evAftNav(steps, this);
	
	// Return
	return true;
}

HistoryNavigator.prototype.Back = function()
{
	this.Go(-1);
}

HistoryNavigator.prototype.Forward = function()
{
	this.Go(1);
}

HistoryNavigator.prototype.Refresh = function()
{
	this.Go(0);
}

HistoryNavigator.prototype.Clear = function(what)
{
	// Checking Arguments
	if(typeof(what) == "string" && what.search(/^back|forward|all$/i) >= 0)
		what = what.toLowerCase();
	else
		what = "forward";
	
	// Raising evBefClr event
	if(typeof(this.evBefClr) == "function")
		this.evBefClr(what, this);
	
	// Clearing
	if(what == "all")
	{
		this.back = [];
		this.forward = [];
	}
	else if(what == "back")
		this.back = [];
	else
		this.forward = [];
	
	// Return
	return true;
}

HistoryNavigator.prototype.BackLength = function()
{
	return this.back.length;
}

HistoryNavigator.prototype.ForwardLength = function()
{
	return this.forward.length;
}

HistoryNavigator.prototype.Find = function(req, where)
{
	// Checking Arguments
	if(typeof(req) == "undefined" || req == null)
		return -1;
		
	if(typeof(where) == "string" || !where.search(/^back|forward|both$/i) >= 0)
		where = where.toLowerCase();
	else
		where = "both";
		
	found = [];	
	
	if(where == "back" || where == "both")
		for(i = 0; i < this.back.length; i++)
			if(this.fnReqCmpr(req, this.back[i]))
				found[found.length] = -(i+1);
	
	if(where == "forward" || where == "both")
		for(i = 0; i < this.forward.length; i++)
			if(this.fnReqCmpr(req, this.forward[i]))
				found[found.length] = i+1;
				
	return found;
	
}

