/**
 * pbjs JavaScript Framework v0.3.6
 * 2011 Niek Saarberg
 */

"use strict";

/**
 * Create global namespace
 */
if( typeof window.PB === 'undefined' ) {

	window.PB = {};
}

PB.VERSION = '0.3.6';


/**
 * Detect browser
 */
PB.Browser = function ( global ) {

	var ua = navigator.userAgent.toLowerCase();

	return {

		IE: /msie/.test(ua),
		Opera: !!global.opera, // /opera/.test(ua),
		Chrome: /chrome/.test(ua),
		Safari: /safari/.test(ua),
		Firefox: /firefox/.test(ua)
	};
}( this );

/*
http://www.ics.uci.edu/pub/ietf/uri/#Related

results in the following subexpression matches:

   $1 = http:
   $2 = http
   $3 = //www.ics.uci.edu
   $4 = www.ics.uci.edu
   $5 = /pub/ietf/uri/
   $6 = <undefined>
   $7 = <undefined>
   $8 = #Related
   $9 = Related
*/

/**
 * Viewport
 */
(function(){

	PB.Viewport = {

		width: function () {

			return document.documentElement.clientWidth;
		},

		height: function () {

			return document.documentElement.clientHeight;
		},

		scrollWidth: function () {

			return Math.max(

				document.documentElement.clientWidth,
				document.body.scrollWidth, document.documentElement.scrollWidth,
				document.body.offsetWidth, document.documentElement.offsetWidth
			);
		},

		scrollHeight: function () {

			return Math.max(

				document.documentElement.clientHeight,
				document.body.scrollHeight, document.documentElement.scrollHeight,
				document.body.offsetHeight, document.documentElement.offsetHeight
			);
		},

		scrollLeft: function () {

			return document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset || 0;
		},

		scrollTop: function () {

			return document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset || 0;
		}
	};
})();

/**
 * Language extensions
 */
(function(){

var objectProto = Object.prototype,
	arrayProto = Array.prototype,
	functionProto = Function.prototype,
	stringProto = String.prototype,
	dateProto = Date.prototype,
	slice = arrayProto.slice;

/**
 * Object methods
 */

/**
 * Extend object with another
 *
 * @param object dest
 * @param object original
 * @return object
 */
Object.extend = function ( dest, original ) {

	var prop;

	for( prop in original ) {

		if( original.hasOwnProperty( prop ) ) {

			dest[prop] = original[prop];
		}
	}

	return dest;
};

/**
 * Extend object with another, existing keys are not overwriten
 *
 * @param object dest
 * @param object original
 * @return object
 */
Object.extendIfNotExists = function ( dest, original ) {

	var prop;

	for( prop in original ) {

		if( original.hasOwnProperty( prop ) && typeof dest[prop] === "undefined" ) {

			dest[prop] = original[prop];
		}
	}

	return dest;
};

/**
 * Ecma5
 */
Object.extendIfNotExists(Object, {

	/**
	 * Retrieve keys from object as array
	 *
	 * @param object object
	 * @return array
	 */
	keys: function ( object ) {

		if ( this === null || Object.isObject( object ) === false ) {

			throw new TypeError();
		}

		var result = [],
			key;

		for( key in object ) {

			if( object.hasOwnProperty( key ) ) {

				result.push( key );
			}
		}

		return result;
	},

	/**
	 * Nasty solution to retrieve objectPrototype
	 *
	 * Note: constructor could be overwriten.. so it`s not the best solution
	 * Todo: Test, test, test... fix fix fix! Not working properly
	 */
	getPrototypeOf: function ( object ) {

		var constructor = object.constructor,
			oldConstructor;

		if( typeof object.__proto__ !== "undefined" ) {

			return object.__proto__;
		}

		if( object.hasOwnProperty( object.constructor ) ) {

			oldConstructor = object.constructor;

			if( (delete object.constructor) === false ) {

				return null;
			}

			constructor = object.constructor;
			object.constructor = oldConstructor;
		}

		return constructor ? constructor.prototype : null;
	}
});

/**
 * Custom
 */
Object.extend(Object, {

	/**
	 * Returns true if given var is an object
	 *
	 * @param object object
	 * @return boolean
	 */
	isObject: function ( object ) {

		return Object.prototype.toString.call( object ) === "[object Object]";
	},

	/**
	 * Create a clone from given object
	 *
	 * Note, deeper properties {key:{deep:{}}} are references instead
	 * of clone.
	 *
	 * Todo: Should be possible to do a deep clone
	 *
	 * @param object object
	 * @return boolean
	 */
	clone: function ( object ) {

		return Object.extend( {}, object );
	},

	/**
	 * Will transform any primite into an array, string excepted
	 *
	 * Deprecated since 0.3.3
	 */
	/*toArray: function ( object ) {

		var property,
			result;

		if( !object ) {

			return [];
		}

		if( (result = Array.prototype.slice.call( object, 0 )).length !== 0 ) {

			return result;
		}

		result = [];

		if( Object.isObject( object ) === true ) {

			for( property in object ) {

				if( object.hasOwnProperty(property) ) {

					result.push( object[property] );
				}
			}

			return result;
		}

		return result;
	},*/

	/**
	 * Implementation to forEach an object
	 *
	 * @param object object -> to iterate
	 * @param function fn
	 * @param mixed scope
	 * @return void
	 */
	forEach: function ( object, fn, scope ) {

		var property;

		if( Object.isObject(object) === false || typeof fn !== "function" ) {

			throw new TypeError();
		}

		for( property in object ) {

			if( object.hasOwnProperty( property ) ) {

				fn.call(scope, object[property], property, object);
			}
		}
	}
});

/**
* Array methods
*/

/**
 * Ecma5
 */
if( Array.isArray === undefined ) {

	/**
	 * Implementation to check if object is an array
	 *
	 * @param mixed object
	 * @return boolean
	 */
	Array.isArray = function ( object ) {

		if( typeof object !== "object" || object === null || typeof object.length === "undefined" ) {

			return false;
		}

		return true;
	};
}

Object.extendIfNotExists(arrayProto,{

	/**
	 * Iterate trough array
	 *
	 * @param function fn
	 * @param mixed scope
	 * @param void
	 */
	forEach: function ( fn, scope ) {

		if ( this === null || typeof fn !== "function" ) {

			throw new TypeError();
		}

		var length = this.length,
			i = 0;

		while ( i < length ) {

			fn.call(scope, this[i], i, this);

			i++;
		}
	},

	/**
	 * Searches the given array for a value and returns the found index or -1 if none found
	 *
	 * Note! Comparsion is done with ===
	 *
	 * @param mixed searchValue
	 * @param integer startIndex
	 * @return integer
	 */
	indexOf: function ( searchValue, startIndex ) {

		if ( this === null ) {

			throw new TypeError();
		}

		var length = this.length;

		startIndex = startIndex || 0;

		if( length <= startIndex || length === 0 ) {

			return -1;
		}

		while( startIndex < length ) {

			if ( this[startIndex] === searchValue ) {

				return startIndex;
			}

			startIndex++;
		}

	    return -1;
	},

	/**
	 * Searches the given array reversed for a value and returns the found index or -1 if none found
	 *
	 * Note! Comparsion is done with ===
	 *
	 * @param mixed searchValue
	 * @param integer stopIndex
	 * @return integer
	 */
	lastIndexOf: function ( searchValue, stopIndex ) {

		if ( this === null ) {

			throw new TypeError();
		}

		var length = this.length;

		stopIndex = stopIndex || 0;

		if( length <= stopIndex || length === 0 ) {

			return -1;
		}

		while( stopIndex <= length ) {

			length--;

			if ( this[length] === searchValue ) {

				return length;
			}
		}

	    return -1;
	},

	/**
	 * Iterate trough array and return new array with filtered values
	 *
	 * @param function fn
	 * @param scope mixed
	 * @return array
	 */
	filter: function ( fn, scope ) {

		if ( this === null || typeof fn !== "function" ) {

			throw new TypeError();
		}

		var result = [],
			i = 0,
			length = this.length;

		while ( i < length ) {

			if( !!fn.call(scope, this[i], i, this) ) {

				result.push( this[i] );
			}

			i++;
		}

		return result;
	},

	/**
	 * Iterate trough array and return true when <b>all</b> values match
	 *
	 * @param function fn
	 * @param mixed scope
	 * @return boolean
	 */
	every: function ( fn, scope ) {

		if ( this === null || typeof fn !== "function" ) {

			throw new TypeError();
		}

		var length = this.length,
			i = 0;

		while ( i < length ) {

			if( fn.call(scope, this[i], i, this) === false ) {

				return false;
			}

			i++;
		}

		return true;
	},

	/**
	 * Return new array with modified values
	 *
	 * @param function fn
	 * @param mixed scope
	 * @return boolean
	 */
	map: function ( fn, scope ) {

		if ( this === null || typeof fn !== "function" ) {

			throw new TypeError();
		}

		var length = this.length,
			result = new Array( length ),
			i = 0;

		while ( i < length ) {

			result[i] = fn.call(scope, this[i], i, this);

			i++;
		}

		return result;
	},

	/**
	 * Iterate trough array and return true when atleast one value matches
	 *
	 * @param function fn
	 * @param mixed scope
	 * @return boolean
	 */
	some: function ( fn, scope ) {

		if ( this === null || typeof fn !== "function" ) {

			throw new TypeError();
		}

		var length = this.length,
			i = 0;

		while ( i < length ) {

			if( fn.call(scope, this[i], i, this) === true ) {

				return true;
			}

			i++;
		}

		return false;
	},

	reduce: function () {


	},

	reduceRight: function () {


	}


});

/**
 * Custom
 */
Object.extend(arrayProto, {

	/**
	 * Clone array
	 *
	 * @return array
	 */
	clone: function () {

		return this.slice(0);
	},

	/**
	 * Empty array
	 *
	 * @return array
	 */
	empty: function () {

		this.length = 0;
		return this;
	},

	/**
	 * Remove given value from array
	 *
	 * @param mixed value
	 * @return array
	 */
	remove: function ( value ) {

		var i = this.indexOf(value);

		if( i !== -1 ) {

			this.splice( i, 1 );
		}

		return this;
	},


	/**
	 * return new array with unique values
	 */
	unique: function () {


	},

	/**
	 * return keys in array
	 */
	keys: function () {


	},

	/**
	 * hasOwnProperty
	 */
	key_exists: function () {


	}
});

/**
* Function methods
*/

/**
 * Ecma 5
 */
Object.extendIfNotExists(functionProto,{

	/**
	 * Created a wrapper function around the `this` object
	 *
	 * @param mixed scope
	 * @param [mixed] additional arguments
	 * @return function
	 */
	bind: function ( scope/*, arg1, argN*/ ) {

		var _args = slice.call( arguments, 1 ),
			fn = this;

		return function () {

			return fn.apply( scope, _args.concat( slice.call( arguments, 0 ) ) );
		};
	}
});

/**
 * Custom
 */
Object.extend(functionProto, {

	bindAsEventListener: function ( scope/*, arg1, argN*/ ) {

		var _args = slice.call( arguments, 1 ),
			fn = this;

		return function ( event ) {

			var args = [event || window.event].concat( _args );

			fn.apply( scope, args );
		};
	},

	bindReversed: function ( scope ) {

		var _args = slice.call( arguments, 1 ),
			fn = this;

		return function () {

			return fn.apply( scope, slice.call( arguments, 0 ).concat( _args ) );
		};
	},

	/**
	 * Delay the execution of the function
	 *
	 * @param integer timeout -> in miliseconds
	 * @return timer
	 */
	delay: function ( timeout ) {

		var _args = slice.call( arguments, 1 ),
			fn = this;

		return window.setTimeout(function (){

			return fn.apply( fn, _args );
		}, timeout);
	}
});

/**
* String methods
*/


/**
 * In the official ecma5 specifications the trim/trimLeft/trimRight methods can't handle
 * an additional arg to trim the string with. So trim methods will be overwriten!
 */
Object.extend(stringProto,{

	/**
	 * Trim begin and end of string
	 *
	 * @param string char -> default whitespace
	 * @return string
	 */
	trim: function ( char ) {

		char = char || "\\s";

		return this.replace( new RegExp("(^["+char+"]+|["+char+"]+$)", "g"), "" );
	},

	/**
	 * Trim begin of string
	 *
	 * @param string char -> default ' '
	 * @return string
	 */
	trimLeft: function ( char ) {

		return this.replace( new RegExp("(^"+(char || "\\s")+"+)", "g"), "" );
	},

	/**
	 * Trim end of string
	 *
	 * @param string char -> default ' '
	 * @return string
	 */
	trimRight: function ( char ) {

		return this.replace( new RegExp("("+(char || "\\s")+"+$)", "g"), "" );
	},

	/**
	 * Makes the first char of the given string uppercase
	 *
	 * Notice that the string wont be lowercased! Only the first
	 * char is touched.
	 *
	 * Example 1
	 * # 'dennis'.ucfirst();
	 * # // Output: Dennis
	 * # 'DENNIS'.ucfirst();
	 * # // Output: DENNIS
	 *
	 * @return string
	 */
	ucfirst: function () {

		return this.charAt(0).toUpperCase()+this.substr(1);
	},

	/**
	 * Add additional char to string, default before
	 *
	 * Todo: Rename string arg
	 *
	 * Example 1
	 * # '6'.pad(2);
	 * # // Output: 66
	 * # 6'.pad(2, '0');
	 * # // Output: 60
	 * # 6'.pad(2, '0', true);
	 * # // Output: 06
	 * # abc'.pad(2, 'd');
	 * # // Output: abc
	 *
	 * @param integer length
	 * @param string string
	 * @param addAfter boolean
	 * @return string
	 */
	pad: function ( length, char, addAfter ) {

		var diff = length - this.length;

		char = String(char);

		if( typeof char === "undefined" || char.length !== 1 ) {

			char = String(this);
		}

		if( diff <= 0 ) {

			return String(this);
		}

		char = char.repeat(diff);

		return addAfter === true ? char+this : this+char;
	},

	/**
	 * Repeat string a given times
	 *
	 * Example 1
	 * # 'Allen'.repeat(4);
	 * # // Output: AllenAllenAllenAllen
	 *
	 * @param integer times
	 * @return string
	 */
	repeat: function ( times ) {

		return (new Array( times+1 )).join( this );
	},

	/**
	 * Replaces piece from string with truncation param. Could be splitted from
	 * the center or the end of the string.
	 *
	 * @param integer max_length -> Default 30
	 * @param string truncation char(s) -> Default '...'
	 * @param boolean truncateCenter -> Truncate from center?
	 */
	truncate: function ( max_length, truncation, truncateCenter ) {

		var stringLength = this.length;

		truncateCenter = truncateCenter || false;
		max_length = max_length >>> 0 || 30;
		truncation = typeof truncation === 'undefined' ? '...' : truncation;

		if( stringLength <= max_length ) {

			return String(this);
		}

		if( truncateCenter === true ) {

			var diff = ((stringLength + truncation.length) - max_length) / 2,
				add = (diff % 1) === 0 ? 0 : 1,
				center = Math.round(this.length / 2);

			if( add ) {

				diff = Math.floor( diff );
			}

			return this.substr( 0, center - diff )+truncation+this.substr( center + diff + add );
		}

		return this.slice( 0, max_length-truncation.length )+truncation;
	},

	/**
	 * Replaces piece from string with truncation param. Could be splitted from
	 * the center or the end of the string. Words are kept intact. So string could
	 * be significaly smaller then max_length.
	 *
	 * @param integer max_length -> Default 30
	 * @param string truncation char(s) -> Default '...'
	 * @param boolean truncateCenter -> Truncate from center?
	 */
	word_truncate: function ( max_length, truncation, truncateCenter ) {

		var length = this.length;

		truncateCenter = truncateCenter || false;
		max_length = max_length >>> 0 || 30;
		truncation = typeof truncation === 'undefined' ? '...' : truncation;

		if( length <= max_length ) {

			return this;
		}

		if( truncateCenter === false ) {

			return this.slice( 0, this.lastIndexOf( " ", max_length - truncation.length ) )+truncation;
		} else {

			var diff = ((length + truncation.length) - max_length) / 2,
				add = (diff % 1) === 0 ? 0 : 1,
				center = Math.round(length / 2);

			if( add ) {

				diff = Math.floor( diff );
			}

			return this.substr( 0, this.lastIndexOf( " ", center - diff ) )+truncation+this.substr( this.indexOf( " ", center + diff + add ) );
		}
	}
});

/**
 * Extend primitive Date object with ecma5 funtionality and some PHP like methods
 */

(function(){

/**
 * Date
 */

/**
 * Ecma5
 */
Object.extendIfNotExists(Date, {

	/**
	 * Return the current time in miliseconds
	 */
	now: function () {

		return (new Date()).getTime();
	}
});

Object.extendIfNotExists(dateProto, {

	/**
	 * Ecma5 toISOString specification
	 */
	toISOString: function () {


		return this.getUTCFullYear()+"-"
			+this.pad(this.getUTCMonth()+1)+"-"
			+this.pad(this.getUTCDate())+"T"
			+this.pad(this.getUTCHours())+":"
			+this.pad(this.getUTCMinutes())+":"
			+this.pad(this.getUTCSeconds())+"."
			+(this.getMilliseconds()).toString().pad(3, 0, true)+"Z";
	}
});

/**
 * Custom
 */
Object.extend(dateProto, {

	/**
	 * Clone a date object
	 */
	clone: function () {

		return new Date( this.getTime() );
	},

	/**
	 * Custom pad function for date like number formating
	 * For intern usage
	 */
	pad: function ( number ) {

		return number >>> 0 < 10 ? "0"+number : number;
	}
});

var _native_parse = Date.parse;

/**
 * Date.parse, extend native parse method
 *
 * @param string dateString
 * @return Date
 */
Date.parse = function ( date ) {

	var timestamp,
		matches;

	if( date instanceof Date ) {

		return date;
	}

	timestamp = _native_parse(date);

	if( isNaN(timestamp) === false ) {

		return new Date(timestamp);
	}

	date = date.trim();





	if( matches = date.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T?\s?([0-9]{2}):([0-9]{2}):([0-9]{2})/) ) {

		matches.shift();

		return new Date(matches[0], (parseInt(matches[1].trimLeft('0'))-1), matches[2], matches[3], matches[4], matches[5], 0);
	}
};

/**

Date.prototype.setISO8601 = function (string) {
    var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
        "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
        "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
    var d = string.match(new RegExp(regexp));

    var offset = 0;
    var date = new Date(d[1], 0, 1);

    if (d[3]) { date.setMonth(d[3] - 1); }
    if (d[5]) { date.setDate(d[5]); }
    if (d[7]) { date.setHours(d[7]); }
    if (d[8]) { date.setMinutes(d[8]); }
    if (d[10]) { date.setSeconds(d[10]); }
    if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
    if (d[14]) {
        offset = (Number(d[16]) * 60) + Number(d[17]);
        offset *= ((d[15] == '-') ? 1 : -1);
    }

    offset -= date.getTimezoneOffset();
    time = (Number(date) + (offset * 60 * 1000));
    this.setTime(Number(time));
}

*/
/**
 * PHP like format method
 *
 * See the PHP manual
 *
 * Todo: Translations logic
 */
var dayNames = ["Sunday", "Monday", "Tuesday", "Wendsday", "Thursday", "Friday", "Saturday"],
	dayNamesShort = ["Sun", "Mon", "Tue", "Wen", "Thu", "Fri", "Sat"],
	monthNames = ["Januari", "Februari", "March", "April", "May", "April", "March", "June", "Juli", "October", "November", "December"],
	monthNamesShort = ["Jan", "Feb", "Mar", "Apr", "May", "Apr", "Mar", "Jun", "Jul", "Oct", "Nov", "Dec"];

Object.extend(dateProto, {

	formatRegExp: /\\?([a-z])/gi,

	/**
	 * PHP like format method
	 *
	 * Example:
	 * (new Date()).format("Y-m-d H:i:s");
	 */
	format: function ( string ) {

		return string.replace( this.formatRegExp, function ( match, defaultValue ){

			return typeof this["get_"+match] === "function" ? this["get_"+match]() : defaultValue;
		}.bind(this));
	},


	/**
	 * Returns the day in 01-31 format
	 */
	get_d: function () {

		return this.pad( this.getDate() );
	},

	/**
	 * Returns the short name of the day
	 *
	 * Mon trough Sun
	 */
	get_D: function () {

		return dayNamesShort[this.getDay()];
	},

	/**
	 * Returns the day in 1-31 format
	 */
	get_j: function () {

		return this.getDate();
	},

	/**
	 * Returns the name of the day
	 *
	 * Sunday trough Saturday
	 */
	get_l: function () {

		return dayNames[this.getDay()];
	},

	/**
	 * Returns the day of the week
	 *
	 * 1 (for Monday) trough 7 (for Sunday)
	 */
	get_N: function () {

		return this.getDay() || 7;
	},

	/**
	 * Returns the day of the week
	 *
	 * 0 (for Monday) trough 6 (for Sunday)
	 */
	get_w: function () {

		return this.getDay();
	},

	/**
	 * Returns the day of the year, starting from 0
	 */
	get_z: function () {

		var firstdayThisYear = new Date( this.getFullYear(), 0, 1, 0, 0, 0 );

		return Math.floor( (this - firstdayThisYear) / 86400000 );
	},


	/**
	 * Returns the week number, starting on Monday
	 */
	get_W: function () {

		var dateClone = new Date( this.getTime() );

		dateClone.setTime( dateClone.getTime() + (dateClone.getDay() !== 1 ? 1 - dateClone.format("N") : 0) * 86400000 );

		return this.pad( Math.ceil( Math.ceil( dateClone.format("z") / 7 ) ) );
	},


	/**
	 * Returns the month name, Januari trough December
	 */
	get_F: function () {

		return monthNames[this.getMonth()];
	},

	/**
	 * Returns the month in 01 trough 12 format
	 */
	get_m: function () {

		return this.pad( this.getMonth()+1 );
	},

	/**
	 * Returns the short month name, Jan trough Dec
	 */
	get_M: function () {

		return monthNamesShort[this.getMonth()];
	},

	/**
	 * Returns the month in 1 trough 12 format
	 */
	get_n: function () {

		return this.getMonth()+1;
	},

	/**
	 * Returns the number of days in given month, 28 trough 31
	 */
	get_t: function () {

		var clone = new Date( this.getTime() );

		clone.setDate(32)

		return 32 - clone.getDate();
	},


	/**
	 * Is leap year?
	 */
	get_L: function () {

		return (new Date(this.getFullYear(), 1, 29)).getDate() === 29;
	},

	/**
	 * Returns the year in YYYY format (4 digits)
	 */
	get_Y: function () {

		return this.getFullYear();
	},

	/**
	 * Returns the year in YY format (2 digits)
	 */
	get_y: function () {

		return String(this.getFullYear()).substr(2);
	},


	/**
	 * Returns the year in YY format (2 digits)
	 */
	get_a: function () {

		return Math.floor(this.getHours() / 12) ? "pm" : "am";
	},

	/**
	 * Returns the year in YY format (2 digits)
	 */
	get_A: function () {

		return Math.floor(this.getHours() / 12) ? "PM" : "AM";
	},

	/**
	 * Swatch Internet time	000 trough 999
	 */
	get_B: function () {

		return Math.floor( ((this.getHours() * 3600) + (this.getMinutes() * 60) + this.getSeconds()) / 86.4 );
	},

	/**
	 * Returns hours in 1 trough 12 format
	 */
	get_g: function () {

		return this.getHours() / 12 && this.getHours() >= 12 ? this.getHours() - 12 : this.getHours();
	},

	/**
	 * Returns hours in 0 trough 23 format
	 */
	get_G: function () {

		return this.getHours();
	},

	/**
	 * Returns hours in 01 trough 12 format
	 */
	get_h: function () {

		return this.pad( this.get_g() );
	},

	/**
	 * Returns hours in 00 trough 23 format
	 */
	get_H: function () {

		return this.pad( this.getHours() );
	},

	/**
	 * Returns minutes in 00 trough 59 format
	 */
	get_i: function () {

		return this.pad( this.getMinutes() );
	},

	/**
	 * Returns seconds in 00 trough 59 format
	 */
	get_s: function () {

		return this.pad( this.getSeconds() );
	},

	/**
	 * Returns the current microseconds
	 */
	get_u: function () {

		return this.getTime();
	},


	/**
	 * Returns the difference in greenwich time(GMT) in hours,	Example: +0200
	 */
	get_O: function () {

		return "+"+this.pad(-this.getTimezoneOffset() / 60)+"00";
	},

	/**
	 * Returns the difference in greenwich time(GMT) with colon in hours,	Example: +02:00
	 */
	get_P: function () {

		return "+"+this.pad(-this.getTimezoneOffset() / 60)+":00";
	},

	/**
	 * Returns the timezone offset in seconds, -43200 trough 50400
	 */
	get_Z: function () {

		return -this.getTimezoneOffset()*60;
	},


	/**
	 * Returns the ISO 8601 date, Example: 2010-06-12 T 15:19:21+00:00
	 */
	get_c: function () {

		return this.format("Y-m-dTH:i:sP");
	},

	/**
	 * Returns the RFC 2822 formatted date, Example: Thu, 21 Dec 2010 15:19:00 +0200
	 */
	get_r: function () {

		return this.format("D, d M Y H:i:s O");
	},

	/**
	 * Returns the seconds since the Unix Epoch
	 */
	get_U: function () {

		return Math.floor(this.getTime() / 1000);
	}
});

/*

var regex2 = [
		/([+-]\d+\s(years?|months?|weeks?|days?|hours?|minutes?|seconds?))/gi,
		/((sunday?|monday?|tuesday?|wednesday?|thursday?|friday?|saturday?)\s(next|last)\s(years?|months?|weeks?))/gi,
		/(next|last)\s(years?|months?|weeks?|days?|hours?|minutes?|seconds?)\s?(sunday?|monday?|tuesday?|wednesday?|thursday?|friday?|saturday?)?/gi,
		/((sunday?|monday?|tuesday?|wednesday?|thursday?|friday?|saturday?)\s(last|next)\s(years?|months?|weeks?))/gi,
		/((sunday?|monday?|tuesday?|wednesday?|thursday?|friday?|saturday?))/gi,
		/((last|first)\s(days?|weeks?|months?)\s(.*?)\s?(weeks?|months?|years?))/gi
	];

var modify2 = function ( string ) {

	var matches,
		parts = [];

	console.log( string );

	for( var i = 0, length = regex2.length >>> 0; i < length; i++ ) {

		if( Array.isArray( matches = string.match( regex2[i] ) ) === true ) {

			matches.forEach(function( m ){

				string = string.replace( m, "" );
			});

			console.log(matches);

		}
	};

	return;
	console.log( string );

	matches.forEach(function( value ) {

		var day = -1;

		parts = value.toLowerCase().split(" ");

		if( (day = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].indexOf(parts[0])) !== -1 ) {

			console.log("day: "+day+", "+value);
		} else {//if ( () !== -1 ) {

			console.log(value);
		}

		return "Blep";
	}, this);
};

modify2("monday next month +1 day -1 second friday next year");
modify2("next week monday");
modify2("next year");
modify2("first day of month"
*/

})();

})();

/**
 * Element
 */
(function( global ){

var UID = 0,
	ElementCache = {},
	slice = Array.prototype.slice;

Object.extend(PB, {

	cache: ElementCache,

	/**
	 * Create unique id, troughout the app
	 */
	id: function () {

		return ++UID;
	},

	/**
	 * Create or retrieve element closure
	 *
	 * Method will check if given param could be
	 * found in Document Object Model(DOM). Founded
	 * elements are cached internaly. If element is
	 * not found, return null.
	 *
	 * Example 1
	 * # All lines return the same reference to PB.Element
	 * # PB.get('element_id');
	 * # PB.get( document.getElementById('element_id') );
	 * # PB.get( PB.get('element_id') );
	 *
	 * @param string/node/PB.Element element
	 * @return PB.Element/null
	 */
	get: function ( element ) {

		if( element === null ) {

			return null;
		}

		if( element instanceof PB.Element ) {

			return element;
		}

		if( typeof element === "string" ) {

			element = document.getElementById( element );
		}

		if( (!element || (element.nodeType !== 1 && element.nodeType !== 9)) && element !== window ) {

			return null;
		}

		if( element.__PBID !== undefined && element.__PBID in ElementCache ) {

			return ElementCache[element.__PBID];
		}

		element.__PBID = PB.id();

		return ElementCache[element.__PBID] = new PB.Element( element );
	},

	/**
	 * Element closure
	 */
	Element: function ( node ) {

		this.node = node;

		this._store = {};
	},

	_removeElement: function ( node ) {

		var element = PB.get(node);

		if( element._store && element._store.morph ) {

			element._store.morph.stop();
		}

		try {

			delete ElementCache[node.__PBID];
		} catch ( e ) {

			alert( e.message );
		}
	}
});

/**
 * Element visual
 */
Object.extend(PB.Element.prototype, {

/*	hasAttr: function ( attribute ) {

		return !!this.node.getAttribute( attribute );
	},

	getAttribute: function ( attribute ) {

		return this.node.getAttribute( attribute );
	},

	setAttribute: function ( attribute, value ) {

		this.node.setAttribute( attribute, value );
		return this;
	},

	removeAttribute: function ( attribute ) {

		this.node.removeAttribute( attribute );
		return this;
	},*/

	/**
	 * Set or get element attribute
	 * If value === null then attribute will be removed
	 */
	attr: function ( key, value ) {

		var node = this.node;

		if( typeof value === "undefined" ) {

			return node.getAttribute( key );
		} else if ( value === null ) {

			node.removeAttribute( key );
		} else {

			node.setAttribute( key, value );
		}

		return this;
	},

	/**
	 *
	 */
	nodeName: function ( nodeName ) {

		if( typeof nodeName === "string" ) {

			return this.node.nodeName.toUpperCase() === nodeName.toUpperCase();
		}

		return this.node.nodeName.toUpperCase();
	}
});

/**
 * Element visual
 */
Object.extend(PB.Element.prototype, {

	/**
	 * Set or get value of form element
	 */
	val: function ( value ) {

		if( typeof value === "undefined" ) {

			return this.node.value;
		}

		this.node.value = value;
	},

	/**
	 * Create a selection in a text field
	 * If no start and end are given then whole text
	 * is selected. If no end is given, all text behind
	 * starting point is selected.
	 *
	 * Todo: Add nodeType check
	 *
	 * The following features are defined in the DOM Range specification: [DOMRANGE]

	    Range interface
	    deleteContents() method
	    selectNodeContents() method
	    setEnd() method
	    setStart() method
	    collapsed attribute
	    endContainer attribute
	    endOffset attribute
	    startContainer attribute
	    startOffset attribute

	 */
	selectText: function( start, end ) {

		var node = this.node,
			range;

		if( typeof start === "undefined" ) {

			start = 0;
		}

		if( typeof end === "undefined" ) {

			end = this.val().length;// || this.html().length;
		}

		if( node.createTextRange ) {

			range = node.createTextRange();
			range.collapse( true );
			range.moveStart( "character", start );
			range.moveEnd( "character", end - start );
			range.select();
		} else if ( node.setSelectionRange ) {

			node.setSelectionRange( start, end );
		}

		node.focus();
	},

	serialize: function () {

		var inputs,
			data = {};

		if( this.nodeName('FORM') === false ) {

			throw new Error("PB.Element.serialize method excpects an form element");
			return data;
		}

		inputs = this.find("textarea, input, select");

		if( inputs.length === 0 ) {

			return data;
		}

		inputs.forEach(function ( field ){

			data[ field.attr('name') ] = field.val();
		});

		return data;
	}
});

/**
 * Element visual
 */
Object.extend(PB.Element.prototype, {

	/**
	 * Check if element has class
	 */
	hasClass: function ( className ) {

		return (new RegExp( "(^|\\s)"+className+"($|\\s)" )).test(this.node.className);
	},

	/**
	 * Add class(es) to element
	 *
	 * It's possible to add an array as arg
	 */
	addClass: function ( className ) {

		if( Array.isArray(className) === true ) {

			className.forEach(this.addClass, this);
			return this;
		}

		var node = this.node;

		if( this.hasClass(className) ){

			return this;
		}

		if( node.className === "" ) {

			node.className = className;
		} else {

			node.className += " "+className;
		}

		return this;
	},

	/**
	 * Remove class(es) to element
	 *
	 * It's possible to add an array as arg
	 */
	removeClass: function ( className ) {

		if( Array.isArray(className) === true ) {

			className.forEach(this.removeClass, this);
			return this;
		}

		var node = this.node;

		node.className = node.className.replace( new RegExp( "(^|\\s+)"+className+"(\\s+|$)" ), ' ' ).trim();

		if( node.className === "" ) {

			this.attr( "class", null );
		}

		return this;
	},

	/**
	 * Retrieve the value of given propertie, its posible to force
	 * the retrievement of an computed value;
	 *
	 * Refers to style class
	 */
	getStyle: function ( propertie, computed ) {

		return PB.Style.setNode( this.node ).get( propertie, computed );
	},

	/**
	 * Set the ste style(s) for an element
	 *
	 * Refers to style class, method will act as an bridge
	 */
	setStyle: function ( propertie, value ) {

		var node = this.node,
			Style = PB.Style.setNode( node ),
			i;

		if( typeof value !== "undefined" ) {

			Style.set( propertie, value );
			return this;
		}

		for( i in propertie ) {

			Style.set( i, propertie[i] );
		}

		return this;
	},

	/**
	 * Display element
	 *
	 * Uses the css propertie display
	 */
	show: function () {

		var store = this._store;

		this.setStyle({

			display: store.css_display || "block"
		});

		store.css_display = null;

		return this;
	},

	/**
	 * Hide element
	 *
	 * Uses the css propertie display
	 */
	hide: function () {

		var store = this._store,
			display = this.getStyle("display");

		if( display === "none" ) {

			return this;
		}

		store.css_display = display;

		this.setStyle({

			display: "none"
		});

		return this;
	},

	isVisible: function () {

		return this.getStyle("display") !== "none";
	},

	/**
	 * Position from offsetParent
	 */
	offset: function () {

		var element = this.node,
			x = 0,
			y = 0;

		while( element ) {

			x += element.offsetLeft;
			y += element.offsetTop;

			if( PB.get(element).getStyle("position") !== "static" ) {

				break;
			}

			element = element.offsetParent;
		}

		return {

			left: x,
			top: y
		};
	},

	/**
	 * Position on page, not viewport
	 */
	position: function () {

		var element = this.node,
			x = 0,
			y = 0;

		while( element ) {

			x += element.offsetLeft;
			y += element.offsetTop;

			element = element.offsetParent;
		}

		return {

			left: x,
			top: y
		};
	},

	/**
	 * Get element position in viewport
	 */
	viewportPosition: function () {

		var position = this.position();

		position.top -= window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
		position.left -= window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft;

		return position;
	},

	/**
	 * Set or get width
	 */
	width: function ( width ) {

		if( typeof width !== "undefined" ) {

			this.setStyle( "width", width );
			return this;
		}

		if( this.node.nodeType === 9 ) {

			return Math.max(
				this.node.documentElement.clientWidth,
				this.node.body.scrollWidth, this.node.documentElement.scrollWidth,
				this.node.body.offsetWidth, this.node.documentElement.offsetWidth
			);
		}

		if( this.isVisible() === false ) {

			this.show();
			width = this.node.offsetWidth;
			this.hide();

			return width;
		}

		return this.node.offsetWidth;
	},

	/**
	 * Get `css` width from element
	 */
	innerWidth: function () {

		return this.width() - (this.getStyle('borderLeftWidth') + this.getStyle('borderRightWidth'));
	},

	outerWidth: function () {

		return this.width() + (this.getStyle('borderLeftWidth') + this.getStyle('borderRightWidth'));
	},

	/**
	 * Set or get height
	 */
	height: function ( height ) {

		if( typeof height !== "undefined" ) {

			this.setStyle( "height", height );
			return this;
		}

		if( this.node.nodeType === 9 ) {

			return Math.max(
				this.node.documentElement.clientHeight,
				this.node.body.scrollHeight, this.node.documentElement.scrollHeight,
				this.node.body.offsetHeight, this.node.documentElement.offsetHeight
			);
		}

		if( this.isVisible() === false ) {

			this.show();
			height = this.node.offsetHeight;
			this.hide();

			return height;
		}

		return this.node.offsetHeight;
	},

	innerHeight: function () {

		return this.height() - (this.getStyle('borderTopWidth') + this.getStyle('borderBottomWidth'));
	},

	outerHeight: function () {

		return this.height() + (this.getStyle('borderTopWidth') + this.getStyle('borderBottomWidth'));
	},

	/**
	 * Get scrollheight
	 */
	scrollHeight: function () {

		return this.node.scrollHeight;
	},

	/**
	 * Get scrollwidth
	 */
	scrollWidth: function () {

		return this.node.scrollWidth;
	},

	/**
	 * Set or get scrolltop
	 */
	scrollTop: function ( x ) {

		if( typeof x === "undefined" ) {

			return this.node.scrollTop;
		}

		this.node.scrollTop = x;

		return this;
	},

	/**
	 * Set or get scrollleft
	 */
	scrollLeft: function ( y ) {

		if( typeof y === "undefined" ) {

			return this.node.scrollLeft;
		}

		this.node.scrollLeft = y;
	},

	/**
	 * Scroll to element, if no scroll container is given, then window is used
	 */
	scrollTo: function ( container ) {

		var position;

		if( typeof container === "undefined" ) {

			position = this.position();

			window.scrollTo( position.left, position.top );
		} else {


			container = PB.get(container);
			position = this.offset();

			PB.get(container).scrollLeft( position.left );
			PB.get(container).scrollTop( position.top );
		}

		return this;
	},

	/**
	 * Set or get innerHTML of an element
	 *
	 * Script tags could be evaled, globaly
	 */
	html: function ( html, evalJS ) {

		if( typeof html === "undefined" ) {

			if( evalJS ) {


			}

			return this.node.innerHTML;
		}

		this.node.innerHTML = html;

		return this;
	},

	/**
	 * Empty element
	 *
	 * Todo: Reseach about mem leaks
	 *
	 * @return PB.Element
	 */
	empty: function () {

		return this.html('');
	}
});

/**
 * Element insertion
 */
Object.extend(PB.Element.prototype, {

	/**
	 * Append given element to self
	 */
	append: function ( element ) {

		if( (element = PB.get(element)) === null ) {

			return null;
		}

		this.node.appendChild( element.node );

		return this;
	},

	/**
	 * Append self to given element
	 */
	appendTo: function ( target ) {

		if( (target = PB.get(target)) === null ) {

			return null;
		}

		target.append( this );

		return this;
	},

	/**
	 * Insert self before given element
	 */
	insertBefore: function ( target ) {

		if( (target = PB.get(target)) === null ) {

			return null;
		}

		target.parent().node.insertBefore( this.node, target.node );

		return this;
	},

	/**
	 *Insert self after given element
	 */
	insertAfter: function ( target ) {

		var nextNode;

		if( (target = PB.get(target)) === null ) {

			return null;
		}

		if( (nextNode = target.next()) instanceof PB.Element ) {

			target.parent().node.insertBefore( this.node, nextNode.node );
		} else {

			target.parent().node.appendChild( this.node );
		}

		return this;
	},

	/**
	 * Insert this as first child of given target
	 */
	insertFirst: function ( target ) {

		if( (target = PB.get(target)) === null ) {

			return null;
		}

		if( target.first() === null ) {

			target.append( this );
		} else {

			this.insertBefore( target.first() );
		}

		return this;
	},

	/**
	 * Replace element with given target
	 */
	replace: function ( target ) {

		if( (target = PB.get(target)) === null ) {

			return null;
		}

		this.insertBefore( target );

		target.remove();

		return this;
	},

	wrap: function () {


	},

	unwrap: function () {


	},

	/**
	 * Fix removal of events and in pb cache
	 */
	remove: function () {

		var node = this.node;

		PB._removeElement( node );

		node.parentNode.removeChild( node );

		this.node = node = null;
	}
});

/**
 * Element traversal
 */
Object.extend(PB.Element.prototype, {

	/**
	 * Select elements parent node
	 */
	parent: function ( element ) {

		return PB.get( this.node.parentNode );
	},

	/**
	 * Retrieve element childs nodes
	 */
	childs: function ( element ) {

		var childNodes = this.node.childNodes,
			childArray;

		try {

			childArray = slice.call(childNodes, 0);
		} catch ( ieError ) {

			var length = childNodes.length;
			childArray = Array( length );

			while( length-- ) {

				childArray[length] = childNodes[length];
			}
		}

		return childArray.filter(PB.get).map(PB.get);
	},

	/**
	 * Goto next node
	 */
	next: function ( node ) {

		var sibling = node || this.node;

		while( sibling = sibling.nextSibling ) {

			if( sibling.nodeType === 1 ) {

				return PB.get( sibling );
			}
		}

		return null;
	},

	/**
	 * Goto previous node
	 */
	prev: function ( node ) {

		var sibling = node || this.node;

		while( sibling = sibling.previousSibling ) {

			if( sibling.nodeType === 1 ) {

				return PB.get( sibling );
			}
		}

		return null;
	},

	/**
	 * Goto first child of element
	 */
	first: function () {

		var child = this.node.firstChild;

		if( child.nodeType !== 1 ) {

			child = this.next( child );
		}

		return PB.get( child );
	},

	/**
	 * Goto last child of element
	 */
	last: function () {

		var child = this.node.lastChild;

		if( child.nodeType !== 1 ) {

			child = this.prev( child );
		}

		return PB.get( child );
	},

	/**
	 * Find out if element is descendants of given element
	 */
	descendantsOf: function ( element ) {

		var node = this,
			max = 50,
			body = PB.get(document.body);

		element = PB.get(element);

		do {

			if( node === element ) {

				return true;
			}

			if( !--max || node === body ) {

				break;
			}

		} while ( node = node.parent() );

		return false;
	},

	/**
	 * The getElementsByTagName selector
	 */
	getElementsByTagName: function ( tagName ) {

		var nodes = this.node.getElementsByTagName( tagName ),
			nodesArray;

		try {

			nodesArray = slice.call(nodes, 0).map(PB.get);
		} catch ( ieError ) {

			var length = nodes.length,
				nodesArray = Array( length );

			while ( length-- ) {

				nodesArray[length] = PB.get( nodes[length] );
			}
		}

		return nodesArray;
	},

	/**
	 * Will return an array of matches nodes
	 */
	getElementsByClassName: function () {

		return !!document.getElementsByClassName
			? function ( className ) {

				return slice.call(this.node.getElementsByClassName(className), 0).map(PB.get);
			}
			: function ( className ) {

				var nodes = this.node.getElementsByTagName("*"),
					length = nodes.length,
					result = [],
					i = 0,
					node;

				while( i < length ) {

					if( (node = PB.get(nodes[i++])) && node.hasClass(className) === true ) {

						result.push( node );
					}
				}

				return result;
			};
	}(),

	/**
	 * Simple CSS selector
	 *
	 * Created to select elements be tag name, class name or both tag.className
	 */
	find: function () {

		return !!document.querySelectorAll
		? function ( query ) {

			var nodes = this.node.querySelectorAll( query ),
				result;

			try {

				result = slice.call(nodes, 0);
			} catch ( ieError ) {

				var length = nodes.length;
				result = Array( length );

				while( length-- ) {

					result[length] = nodes[length];
				}
			}

			return result.map(PB.get);
		}
		: function ( query ) {

			var nodes = [];

			if( query.indexOf(",") !== -1) {

				query.split(",").forEach(function( part ) {

					nodes = nodes.concat( this.find( part.trim() ) );
				}, this);

				return nodes;
			}

			if( query.indexOf(".") !== -1 ) {

				query = query.split(".");

				nodes = this.getElementsByClassName( query[1] );

				if( query[0] !== "" ) {

					nodes = nodes.filter(function ( node ){

						return node.node.nodeName.toLowerCase() === query[0];
					});
				}
			} else {

				nodes = this.getElementsByTagName( query );
			}

			return nodes;
		}
	}(),

	/**
	 * Tries finding the given parent with a simple selector
	 *
	 * IDEE: Name findParent to hasParent ??
	 */
	findParent: function ( parentSelector, depth ) {

		var node = this,
			depth = depth || 50,			// TODO, rename depth
			body = PB.get(document.body),
			className = '',
			nodeName = '';

		if( parentSelector.indexOf('.') !== -1 ) {

			parentSelector = parentSelector.split('.');
			className = parentSelector[1];
			parentSelector = parentSelector[0];
		}

		nodeName = parentSelector;

		do {

			if( className === '' && nodeName !== '' && node.nodeName(nodeName) === true ) {

				return node;
			} else if ( nodeName === '' && className !== '' && node.hasClass(className) === true ) {

				return node;
			} else if ( node.nodeName(nodeName) && node.hasClass(className) ) {

				return node;
			}

			if( !--depth || node === body ) {

				break;
			}

		} while ( node = node.parent() );

		return null;
	}
});

/*
cache.click = {

	uid: fn,
	uid: fn,
	uid: fn
};

cache.mouseover = {

	uid: fn,
	etc..
}

proberly create a singleton to handle al events
*/

/**
 * TODO
 * - Organize event cache
 * - Naming cleanup, vars
 * - onunload all events removal
 * - event.is("leftClick") ?? Or something
 * - Fix mousebutton codes
 */


PB.Event = {

	cache: {},
	supports_mouse_enter_leave: "onmouseenter" in document.documentElement && "onmouseleave" in document.documentElement,
	translation: {

		mouseenter: "mouseover",
		mouseleave: "mouseout"
	},
	Methods: {

		stopPropagation: function () {

			this.cancelBubble = true;
		},

		preventDefault: function () {

			this.returnValue = false;
		},

		stop: function () {

			this.preventDefault();
		    this.stopPropagation();
		}
	}
};

var EVENT_CACHE = [];

Object.extend(PB.Element.prototype, {

	/**
	 * Creates the event handler wrapper
	 */
	createEventHandler: function ( handler ) {

		var node = this.node,
			fn;

		if( typeof handler.UID !== "undefined" ) {

			return EVENT_CACHE[handler.UID];
		}

		fn = function ( event ) {

			var docEl = document.documentElement,
				body = document.body;

			/**
			 * Add event methods
			 */
			Object.extendIfNotExists(event, PB.Event.Methods);

			/**
			 * Add a target node
			 */
			if( typeof event.target === "undefined" ) {

				event.target = event.srcElement || node;
			}

			/**
			 * Add a currentTarget node, only useable on mouseover/mouseenter/mouseout/mouseleave
			 */
			if( typeof event.currentTarget === "undefined" ) {

				switch ( event.type ) {

					case 'mouseover':
					case 'mouseenter':
						event.currentTarget = event.fromElement;
						break;

					case 'mouseout':
					case 'mouseleave':
						event.currentTarget = event.toElement;
						break;
				}
			}

			/**
			 * Add which propertie
			 */
			if( typeof event.which === "undefined" ) {

				event.which = event.keyCode || event.charCode;
			}

			/**
			 * Add mouse position, relative to page
			 */
			if( typeof event.pageX === "undefined" && typeof event.clientX !== "undefined" ) {

				event.pageX = event.clientX + (docEl.scrollLeft || body.scrollLeft) - (docEl.clientLeft || 0);
				event.pageY = event.clientY + (docEl.scrollTop || body.scrollTop) - (docEl.clientTop || 0);
			}

			/**
			 * Trigger the 'original' event handler with normalized event object
			 */
			handler.call( node, event );
		};

		fn.PB_EVENT_UID = handler.PB_EVENT_UID = PB.id();

		EVENT_CACHE.push( fn );

		return fn;
	},

	on: function ( eventName, handler ) {

		var node = this.node,
			_handler = this.createEventHandler( handler ),
			eventName = !PB.Event.supports_mouse_enter_leave ? PB.Event.translation[eventName] || eventName : eventName,
			docEl = document.documentElement,
			cache = PB.Event.cache;

		if( node.addEventListener ) {

			node.addEventListener( eventName, _handler, false );
		} else {

			eventName = "on"+eventName;

			if( eventName in docEl ) {
				node.attachEvent( eventName, _handler );
			} else {

				if( typeof PB.Event.cache[eventName] === "undefined" ) {

					PB.Event.cache[eventName] = [];
				}

				PB.Event.cache[eventName].push( _handler );
			}


		}

		return this;
	},

	stop: function ( eventName, handler ) {

		var node = this.node,
			uid,
			i;

		if( typeof handler.PB_EVENT_UID !== "undefined" ) {

			uid = handler.PB_EVENT_UID;
			i = EVENT_CACHE.length;

			while( i-- ) {

				if( EVENT_CACHE[i] && EVENT_CACHE[i].PB_EVENT_UID === uid ) {

					handler = EVENT_CACHE[i];
					break;
				}
			}
		}

		if( node.addEventListener ) {

			node.removeEventListener( eventName, handler, false );
		} else {

			node.detachEvent( "on"+eventName, handler );
		}

		delete EVENT_CACHE[i];

		return this;
	},

	trigger: function ( eventName ) {

		var event;

		if( document.createEvent ) {

			event = document.createEvent( "HTMLEvents" );
			event.initEvent( eventName, true, true );

			event.eventName = eventName;

			this.node.dispatchEvent( event );
		} else if ( document.createEventObject ) {

			eventName = "on"+eventName;

			event = document.createEventObject();
			event.eventType = true ? 'ondataavailable' : 'onlosecapture';

			event.eventName = eventName;

			try {

				this.node.fireEvent( eventName, event );
			} catch ( ignoredError ) {

				if( typeof PB.Event.cache[eventName] !== "undefined" ) {

					PB.Event.cache[eventName].forEach(function( fn ){

						fn({});
					});
				}
			}
		}

		return this;
	},

	/**
	 * Removes all events
	 */
	tearDown: function () {


	}
});

/*
interface MouseEvent : UIEvent {
  readonly attribute long             screenX;
  readonly attribute long             screenY;
  readonly attribute long             clientX;
  readonly attribute long             clientY;
  readonly attribute boolean          ctrlKey;
  readonly attribute boolean          shiftKey;
  readonly attribute boolean          altKey;
  readonly attribute boolean          metaKey;
  readonly attribute unsigned short   button;
  readonly attribute EventTarget      relatedTarget;
  void               initMouseEvent(in DOMString typeArg,
                                    in boolean canBubbleArg,
                                    in boolean cancelableArg,
                                    in views::AbstractView viewArg,
                                    in long detailArg,
                                    in long screenXArg,
                                    in long screenYArg,
                                    in long clientXArg,
                                    in long clientYArg,
                                    in boolean ctrlKeyArg,
                                    in boolean altKeyArg,
                                    in boolean shiftKeyArg,
                                    in boolean metaKeyArg,
                                    in unsigned short buttonArg,
                                    in EventTarget relatedTargetArg);
};

altKey of type boolean, readonly
    Used to indicate whether the 'alt' key was depressed during the firing of the event. On some platforms this key may map to an alternative key name.
button of type unsigned short, readonly
    During mouse events caused by the depression or release of a mouse button, button is used to indicate which mouse button changed state. The values for button range from zero to indicate the left button of the mouse, one to indicate the middle button if present, and two to indicate the right button. For mice configured for left handed use in which the button actions are reversed the values are instead read from right to left.
clientX of type long, readonly
    The horizontal coordinate at which the event occurred relative to the DOM implementation's client area.
clientY of type long, readonly
    The vertical coordinate at which the event occurred relative to the DOM implementation's client area.
ctrlKey of type boolean, readonly
    Used to indicate whether the 'ctrl' key was depressed during the firing of the event.
metaKey of type boolean, readonly
    Used to indicate whether the 'meta' key was depressed during the firing of the event. On some platforms this key may map to an alternative key name.
relatedTarget of type EventTarget, readonly
    Used to identify a secondary EventTarget related to a UI event. Currently this attribute is used with the mouseover event to indicate the EventTarget which the pointing device exited and with the mouseout event to indicate the EventTarget which the pointing device entered.
screenX of type long, readonly
    The horizontal coordinate at which the event occurred relative to the origin of the screen coordinate system.
screenY of type long, readonly
    The vertical coordinate at which the event occurred relative to the origin of the screen coordinate system.
shiftKey of type boolean, readonly
    Used to indicate whether the 'shift' key was depressed during the firing of the event.
*/
PB.Style = function () {

	var getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
		cssFloat = function () {	// Nasty check to see if cssFloat or styleFloat should be used

			var div = document.createElement("div"),
				cssFloat;

			div.innerHTML = '<div style="float: left"></div>';
			cssFloat = div.childNodes[0].style.cssFloat === "left";

			div = null;

			return cssFloat;
		}();


	/**
	 * Initialize class
	 *
	 */
	var CSSStyle = function () {};

	CSSStyle.prototype = {

		unitRegex: /px$/i,
		IEOpacity: /alpha\(opacity=(.*)\)/i,

		/**
		 * Hooks are required for reading some multi value css styles
		 */
		getHooks: {

			"border": "borderLeftWidth borderLeftStyle borderLeftColor",
			"borderColor": "borderLeftColor",
			"borderWidth": "borderLeftWidth",
			"borderStyle": "borderLeftStyle",
			"padding": "paddingTop paddingRight paddingBottom paddingLeft",
			"margin": "marginTop marginRight marginBottom marginLeft",
			"borderRadius": "borderRadiusTopleft",
			"MozBorderRadius": "MozBorderRadiusTopleft"
		},

		noUnits: ["zIndex", "zoom", "fontWeight"],

		/**
		 *
		 */
		addUnits: function ( value, propertie ) {

			if( this.noUnits.indexOf(propertie) >= 0 ) {

				return value;
			}

			return typeof value === "string" ? value : value+"px";
		},

		/**
		 *
		 */
		removeUnits: function ( value ) {

			return this.unitRegex.test( value ) ? parseInt( value ) : value;
		},

		/**
		 * Set node
		 */
		setNode: function ( node ) {

			this.node = node.node || node;
			return this;
		},

		/**
		 * Retieve element style, could be forced to retrieve computed style
		 */
		get: function ( propertie, computed ) {

			if( propertie === "float" ) {

				propertie = cssFloat ? "cssFloat" : "styleFloat";
			}

			if( computed === true ) {

				return this.getComputedStyle( propertie );
			}

			return this.getDOMStyle( propertie ) || this.getComputedStyle( propertie );
		},

		/**
		 * retrieve inline style
		 */
		getDOMStyle: function ( propertie ) {

			var node = this.node,
				value = node.style[propertie];

			if( propertie === "opacity" ) {

				if( PB.Browser.IE /* && !PB.Browser.IE9 */ ) {

					value = this.node.style.filter;

					if( !value ) {

						return null;
					}

					value = value.match(this.IEOpacity);

					if( value && value[1] ) {

						return parseFloat(value[1]) / 100;
					}

					return null;
				}

				return value ? parseFloat(value) : null;
			}

			if( value === null || value === "auto" ) {

				return null;
			}

			return this.removeUnits(value);
		},

		/**
		 * Retrieve computed style
		 */
		getComputedStyle: function () {

			return getComputedStyle
				? function ( propertie ) {

					var CSS = document.defaultView.getComputedStyle( this.node, null ),
						value;

					if( propertie in this.getHooks ) {

						value = this.getHooks[propertie].split(" ").map(function( value ){

							return CSS[value];
						});

						return value.length === 1
							? this.removeUnits(value[0])
							: value.join(" ");
					}

					value = CSS[propertie];

					if( propertie === "opacity" ) {

						return value ? parseFloat(value) : 1.0;
					}

					return value === "auto" ? 0 : this.removeUnits(value);
				}
				: function ( propertie ) {

					var CSS = this.node.currentStyle,
						value;

					if( propertie in this.getHooks ) {

						value = this.getHooks[propertie].split(" ").map(function( value ){

							return CSS[value];
						});

						return value.length === 1
							? this.removeUnits(value[0])
							: value.join(" ");
					}

					if( propertie === "opacity" ) {

						value = CSS.filter;
						value = value.match(this.IEOpacity);

						if( value && value[1] ) {

							return parseInt(value[1]) / 100;
						}

						return 1.0;
					}

					value = CSS[propertie];

					return value === undefined ? null
						: value === "auto" ? 0 : this.removeUnits(value);
				}
		}(),

		/**
		 * Set CSS style
		 */
		set: function ( propertie, value ) {

			if( propertie === "opacity" ) {

				this.setOpacity( value );
			} else {

				this.node.style[propertie] = this.addUnits(value, propertie);
			}
		},

		/**
		 * Set crossbrowser opacity
		 */
		setOpacity: function () {

			return PB.Browser.IE /* && !PB.Browser.IE9 */
				? function ( value ) {

					this.node.style.filter = "alpha(opacity="+(value*100)+")";
				}
				: function ( value ) {

					this.node.style.opacity = value.toString();
				};
		}()
	};

	return new CSSStyle();
}();


})( this );
/**
 * Ajax
 */

(function(){

	/**
	 * Default headers
	 */
	var PBRequestHeaders = {

		"X-Requested-With": "PBJS-"+PB.VERSION,
		"Accept": "text/javascript, text/html, application/xml, text/xml, */*"
	};

	/**
	 * Handles xmlHttp request
	 */
	PB.Request = function ( options ) {

		this.setOptions( options );
		this.xmlHttp = this.createTransport();

		this.onStateChangeWrapper = this.onStateChange.bind(this);
	};

	PB.Request.prototype = {

		/**
		 * Create the correct transport
		 *
		 * Should be optimized
		 */
		createTransport: function () {

			try {

				return new XMLHttpRequest();
			} catch (e) {};

			try {

				return new ActiveXObject("Msxml2.XMLHTTP");
			} catch (e) {}

			try {

				return new ActiveXObject("Msxml3.XMLHTTP");
			} catch (e) {}

			try {

				return new ActiveXObject("Microsoft.XMLHTTP");
			} catch (e) {}

			return null;
		},

		/**
		 * Set all options to default and overide with given options
		 */
		setOptions: function ( options ) {

			this.free = false;

			/**
			 * update should be an element
			 */
			if( options.update ) {

				options.update = PB.get(options.update);

				if( options.update === null ) {

					throw new TypeError();
				}
			}

			this.options = Object.extend({

				method: "GET",
				asynchronous: true,
				charset: "UTF-8"
			}, options);

			return this;
		},

		/**
		 * Used to reuse an request
		 */
		setOption: function ( key, value ) {

			this.options[key] = value;

			return this;
		},

		/**
		 * Send the request
		 */
		send: function () {

			var options = this.options,
				url = options.url,
				method = options.method.toUpperCase(),
				http = this.xmlHttp,
				headers = {},
				params = options.data ? this.buildQueryString( options.data ) : null;

			if( params !== null && method !== "POST" && method !== "PUT" ) {

				url += (url.indexOf("?") === -1 ? "?" : "&")+params;
				params = null;
			}

			http.open( method, url, options.asynchronous );

			http.onreadystatechange = this.onStateChangeWrapper;

			Object.extend( headers, PBRequestHeaders );

			if( method === "POST" ) {

				http.setRequestHeader( "Content-type", "application/x-www-form-urlencoded; charset="+options.charset );
			}

			if( options.headers ) {

				Object.extend( headers, options.headers );
			}

			Object.forEach(headers, function( val, name ){

				http.setRequestHeader( name, val );
			});

			http.send( params );
		},

		/**
		 * Abort the request
		 */
		abort: function () {

			this.xmlHttp.abort();
		},

		/**
		 * Handle the onstatechange event
		 */
		onStateChange: function () {

			var http = this.xmlHttp,
				contentType;

			if( http.readyState === 4 ) {

				if( http.status >= 200 && http.status < 300 ) {

					contentType = http.getResponseHeader('Content-type');

					if( this.options.update ) {

						this.options.update.html( http.responseText, true );
					}

					if( this.options.success ) {

						http.responseJSON = null;

						if( contentType.indexOf( "application/json" ) !== -1 ) {

							http.responseJSON = eval( "(" + http.responseText + ")" );
						}

						this.options.success( http, http.status );
					}
				} else {

					if( this.options.error ) {

						this.options.error( http, http.status );
					}
				}

				http.responseJSON = null;

				http.onreadystatechange = null;

				this.free = true;
			}
		},

		/**
		 * Create uri string
		 *
		 * Should be optimized
		 *
		 * 	Example:
		 * 	{
		 * 		key1: "value1",
		 *		key2: ["val1", "val2", "val"]
		 *	}
		 *
		 *	Output:
		 *	key1=value1&key2[]=val1&key2[]=val2&key2[]=val3
		 */
		buildQueryString: function ( mixed, prefix ) {

			var queryString = "";

			prefix = typeof prefix === "string" ? prefix : false;

			if( typeof mixed === "string" ) {

				return mixed;
			} else if( Array.isArray(mixed) === true ) {

				mixed.forEach(function( value, key ){

					if( typeof value === "object" ) {

						queryString += this.buildQueryString( value, key+"[]" );
					} else {

						queryString += (prefix || key)+"="+escape(value)+"&";
					}
				}, this);
			} else if( Object.isObject(mixed) === true ) {

				Object.forEach(mixed, function( value, key ){

					if( typeof value === "object" ) {

						queryString += this.buildQueryString( value, key+"[]" );
					} else {

						queryString += (prefix || key)+"="+escape(value)+"&";
					}
				}, this);
			}

			return queryString.trimRight("&");
		}
	};

	/**
	 * Default handler to create ajax request
	 *
	 * Singleton
	 */
	var AjaxHandler = function () {

		this.cache = [];
	};

	AjaxHandler.prototype = {

		/**
		 * Do the request with given options
		 *
		 * Will return the used transport
		 */
		request: function ( options ) {

			var transport = this.getFreeTransport();

			if( transport === null ) {

				transport = new PB.Request( options );
				this.cache.push( transport );
			} else {

				transport.setOptions( options );
			}

			transport.send();

			return transport;
		},

		/**
		 * Private method
		 *
		 * Get the first free transport avaible
		 */
		getFreeTransport: function () {

			var cache = this.cache,
				length = cache.length,
				i = 0;

			for( ; i < length; i++ ) {

				if( cache[i].free ) {

					return cache[i];
				}
			}

			return null;
		}
	};

	/**
	 * To global space
	 */
	PB.Ajax = new AjaxHandler();

})();

/**
 * Extra's
 */
/**
 *
 */
PB.DOMReady = function () {

	this.onReadyStateChangeWrapper = this.onReadyStateChange.bind(this);
	this.onDOMContentLoadedWrapper = this.onDOMContentLoaded.bind(this);

	this.fireWrapper = this.fire.bind(this);

	this.addListeners();
};

PB.DOMReady.prototype = {

	ready: false,

	queue: [],

	addListeners: function () {

		if( document.addEventListener ) {

			document.addEventListener( 'DOMContentLoaded', this.onDOMContentLoadedWrapper, false );
		} else {

			PB.get(document).on( 'readystatechange', this.onReadyStateChangeWrapper );
		}

		PB.get(window).on("load", this.fireWrapper)
	},

	/**
	 * Add function to fire
	 */
	add: function ( fn ) {

		if( this.ready === true ) {

			fn();
		}

		this.queue.push( fn );
	},

	onDOMContentLoaded: function () {

		document.removeEventListener( 'DOMContentLoaded', this.fireWrapper, false );

		this.fire();
	},

	onReadyStateChange: function () {

		if( document.readyState === 'complete' ) {

			PB.get(document).stop( 'readystatechange', this.onReadyStateChangeWrapper );

			this.fire();
		}
	},

	/**
	 * Executes all listened functions
	 */
	fire: function () {

		if( this.ready === true ) {

			return;
		}

		this.ready = true;

		this.queue.forEach(function ( fn ){

			fn();
		});

		this.queue.empty();
	}
};

PB.DOMReady = new PB.DOMReady();
PB.DOMReady = PB.DOMReady.add.bind(PB.DOMReady);
(function(){

	var RGBValues = /RGB\((\d+),\s?(\d+),\s?(\d+)\)?/i,
		RGBAValues = /RGBA\((\d+),\s?(\d+),\s?(\d+),\s?(\d+.\d+)\)?/i;

	PB.Color = function ( color ) {

		this.color = {

			R: 0,
			G: 0,
			B: 0,
			A: 1
		};

		if( !color ) {

			return;
		}

		this.setColorProperties( color );
	};

	PB.Color.prototype = {

		/**
		 * Retrieve color values from mixed
		 */
		setColorProperties: function ( color ) {

			var match;

			if( Object.isObject(color) ) {

				this.set( color.R, color.G, color.B, color.A || 0 );
			} else if ( color.charAt(0) === "#" ) {

				if( color.length === 4 ) {

					color = "#"+color.charAt(1).pad(2)+color.charAt(2).pad(2)+color.charAt(3).pad(2);
				}

				match = {

					R: parseInt( color.substr(1, 2), 16 ),
					G: parseInt( color.substr(3, 2), 16 ),
					B: parseInt( color.substr(5, 2), 16 )
				};

				this.set( match.R, match.G, match.B );

			} else if ( Array.isArray( match = color.match(RGBValues) ) === true ) {

				this.set( parseInt(match[1]), parseInt(match[2]), parseInt(match[3]) );
			} else if ( Array.isArray( match = color.match(RGBAValues) ) === true ) {

				this.set( parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4]) );
			}
		},

		/**
		 *
		 */
		set: function ( R, G, B, A ) {

			this.color.R = Math.round(R);
			this.color.G = Math.round(G);
			this.color.B = Math.round(B);
			this.color.A = A || 1;
		},

		/**
		 * Returns RGBA color values as
		 *	{
		 *		R: int,
		 *		G: int,
		 *		B: int,
		 *		A: float
		 *	}
		 */
		get: function () {

			return this.color;
		},

		/**
		 * Substract color values, A arg is optional
		 */
		sub: function ( R, G, B, A ) {

			this.add( -R, -G, -B, -A || 0 );
		},

		/**
		 * Add color values, A arg is optional
		 */
		add: function ( R, G, B, A ) {

			this.color.R += R;
			this.color.R = this.color.R < 0 ? 0 : this.color.R > 255 ? 255 : this.color.R;
			this.color.G += G;
			this.color.G = this.color.G < 0 ? 0 : this.color.G > 255 ? 255 : this.color.G;
			this.color.B += B;
			this.color.B = this.color.B < 0 ? 0 : this.color.B > 255 ? 255 : this.color.B;
			this.color.A += A || 0;
			this.color.A = this.color.A < 0 ? 0 : this.color.A > 1 ? 1 : this.color.A;
		},

		/**
		 * Returns the difference between two color objects as
		 *	{
		 *		R: int,
		 *		G: int,
		 *		B: int,
		 *		A: float
		 *	}
		 *
		 * Arg must be an instance of Color
		 */
		diff: function ( Color ) {

			var color = this.get(),
				diffColor;

			if( (Color instanceof PB.Color) === false ) {

				return null;
			}

			diffColor = Color.get();

			return {

				R: color.R - diffColor.R,
				G: color.G - diffColor.G,
				B: color.B - diffColor.B,
				A: color.A - diffColor.A
			};
		},

		/**
		 * Return CSS RGB string
		 *	RGB(0, 0, 0)
		 */
		toRGB: function () {

			return "RGB("+this.color.R+", "+this.color.G+", "+this.color.B+")";
		},

		/**
		 * Return CSS RGBA string
		 *	RGB(0, 0, 0, 0)
		 */
		toRGBA: function () {

			return "RGBA("+this.color.R+", "+this.color.G+", "+this.color.B+", "+this.color.A+")";
		},

		/**
		 * Return CSS hex string
		 *	#000000
		 */
		toHex: function () {

			return "#"
				+this.color.R.toString(16).pad(2, 0, true)
				+this.color.G.toString(16).pad(2, 0, true)
				+this.color.B.toString(16).pad(2, 0, true);
		},

		/**
		 * Clone the current Color object
		 */
		clone: function () {

			return new PB.Color( this.get() );
		}
	};
})();

/**
 * Animation classes
 */
PB.Animation_Observer = function () {

	this.isRunning = false;

	this.timerID = null;

	this.observers = [];

	this.notifyWrapper = this.notify.bind(this);
};

PB.Animation_Observer.prototype = {

	attach: function ( observer ) {

		if( this.observers.indexOf( observer ) >= 0 ) {

			return;
		}

		this.observers.push( observer );

		if( this.isRunning === false ) {

			this.start();
		}
	},

	detach: function ( observer ) {

		this.observers.remove( observer );

		if( this.isRunning === true && this.observers.length === 0 ) {

			this.stop();
		}
	},

	notify: function () {

		var timestamp = Date.now();

		this.observers.forEach(function( observer ){

			if( !observer ) {

				return;
			}

			observer.update( timestamp );
		});
	},

	start: function () {

		this.isRunning = true;

		this.timerID = setInterval( this.notifyWrapper, 1000 / 40 );
	},

	stop: function () {

		this.isRunning = false;

		clearTimeout( this.timerID );
	}
};

/**
 * Create singleton
 */
PB.Animation_Observer = new PB.Animation_Observer();

/**
 * Element morphing class
 */
var PropertyCache = {};

PB.Morph = function () {

	this.isRunning = false;

	this.cssText = '';

	this.color = new PB.Color();
};

PB.Morph.prototype = {

	colorProperties: ['backgroundColor', 'borderColor', 'color', 'outlineColor'],

	/* Skipping this part for a while
	groupedStyles: {

		border: ['borderWidth', 'borderStyle', 'borderColor'],
		boxShadow: []
	},*/

	decamalizeExp: /([A-Z])/g,

	reset: function ( element, options ) {

		if( this.isRunning === true ) {

			this.stop();
		}

		this.element = element;

		this.after = options.after;

		this.duration = options.duration * 1000;

		this.effect = PB.MorphEffects[options.effect] || PB.MorphEffects['ease'];

		this.styleInfo = {};

		this.cssText = (this.element.node.style.cssText || "").toLowerCase();

		if( this.cssText !== "" && this.cssText.charAt(this.cssText.length-1) !== ';' ) {

			this.cssText += ";";
		}

		Object.forEach(options.to, this.setStartEndStyles, this);

		return this;
	},

	setStartEndStyles: function ( value, styleName ) {

		var from = this.element.getStyle( styleName, true ),
			unit,
			diff = 0,
			realName = styleName.replace( this.decamalizeExp, "-$1" ).toLowerCase();

		if( typeof from === "undefined" ) {

			return;
		}

		if( typeof value === "number" ) {

			if( typeof from === "string" && from.charAt(from.length-1) === '%' ) {

				from = this.percentToPixel( styleName );
			}

			diff = value - from;
			unit = styleName === 'opacity' ? 0 : 'px';
		}
		else if ( value.charAt(value.length-1) === '%' ) {

			if( parseFloat(from) === from ) {

				from = this.pixelToPercent( styleName, from );
			} else {

				from = parseFloat( from );
			}

			value = parseFloat( value );

			diff = value - from;
			unit = '%';
		}
		else if ( this.colorProperties.indexOf( styleName ) >= 0 ) {

			if( from === "transparent" ) {

				from = "#FFFFFF";
			}

			value = new PB.Color( value );
			from = new PB.Color( from );
			diff = value.diff( from );

			unit = 'color';
		}

		this.styleInfo[styleName] = {

			to: value,
			from: from,
			diff: diff,
			unit: unit,
			realCssName: realName
		};

		if( this.cssText !== '' ) {

			this.removeFromCssText( realName );
		}
	},

	/**
	 * @todo add right, bottom, more?
	 */
	percentToPixel: function ( styleName ) {

		switch( styleName ) {

				case 'height':

					return this.element.height();

				case 'width':

					return this.element.width();

				case 'left':

					return this.element.offset().left;

				case 'top':

					return this.element.offset().top;
			}

			return 0;
	},

	pixelToPercent: function ( styleName, value ) {

		var element = this.element.parent(),
			body = document.body,
			percent = 0;

		while( element ) {

			if( element.getStyle("position") === "absolute" || element.node === body ) {

				break;
			}

			element = element.parent();
		}

		if( element.node === body ) {

			element = PB.get( document );
		}

		value = parseFloat( value );

		switch( styleName ) {

			case "left":
			case "right":
			case "width":
				return value / (element.width() / 100);

			case "top":
			case "bottom":
			case "height":
				return value / (element.height() / 100);

			default:
				return 0;
		}
	},

	removeFromCssText: function ( property ) {

		if( property === "opacity" && PB.Browser.IE && typeof PropertyCache.filter === "undefined" ) {

			PropertyCache.filter = new RegExp("((^|\\s)filter:\\s[a-z0-9\.\(\)=]+;)");
		}

		if( typeof PropertyCache[property] === "undefined" ) {

			PropertyCache[property] = new RegExp("((^|\\s)"+property+":\\s[a-z0-9\.\(\)=\\s,]+;)");
		}

		this.cssText = this.cssText.replace( PropertyCache[property], '' );
	},

	start: function () {

		this.isRunning = true;

		this.startedAt = Date.now();
		this.endsAt = this.startedAt + this.duration;

		PB.Animation_Observer.attach( this );

		return this;
	},

	/**
	 * Update method, invoked by PB.Animation_Observer
	 */
	update: function ( timestamp ) {

		var position = 1 - ((this.endsAt - timestamp) / this.duration );

		if( timestamp >= this.endsAt ) {

			this.render( 1 );
			this.stop();

			return;
		}

		this.render( position );
	},

	/**
	 * Stop morphing
	 *
	 * Todo: Remove this.element and this.after reference
	 */
	stop: function ( skipEnd ) {

		if( this.isRunning === false ) {

			return this;
		}

		var after = this.after,
			element = this.element;

		this.element = null;
		this.after = null;

		PB.Animation_Observer.detach( this );

		if( skipEnd === true ) {

			this.render( 1 );
		}

		this.isRunning = false;

		if( after ) {

			after( element );
		}

		return this;
	},

	render: function ( position ) {

		var styleNames = Object.keys( this.styleInfo ),		// Could be placed somewhere else
			cssText = this.cssText,
			length = styleNames.length,
			ref,
			t = this.effect( position ),
			value;

		while( length-- ) {

			ref = this.styleInfo[styleNames[length]];

			if( ref.unit === 'color' ) {

				value = ref.from.get();

				this.color.set(
						value.R + ( ref.diff.R * t ), //R
						value.G + ( ref.diff.G * t ), //G
						value.B + ( ref.diff.B * t )  //B
					);

				cssText += ref.realCssName+": "+this.color.toRGB()+"; ";
			} else {

				value = ref.from + (ref.diff * t);

				if( ref.realCssName === 'opacity' && PB.Browser.IE ) {

					cssText += "filter: alpha(opacity="+(value*100)+"); ";
				} else {

					cssText += ref.realCssName+": "+(value+ref.unit)+"; ";
				}
			}
		}

		this.element.node.style.cssText = cssText;

	}
};

PB.MorphEffects = {

	linear: function ( t ) {

		return t;
	},

	ease: function ( t ) {

		return t;
	},

	easeIn: function ( t ) {

		return t*t;
	},

	easeOut: function ( t ) {

		return -1*t*(t-2);
	},

	easeInOut: function ( t ) {

		return t;
	},

	bounce: function ( t ) {

		if (t < (1/2.75)) {

		      return (7.5625*t*t);
		  } else if (t < (2/2.75)) {

		      return (7.5625*(t-=(1.5/2.75))*t + .75);
		  } else if (t < (2.5/2.75)) {

		      return (7.5625*(t-=(2.25/2.75))*t + .9375);
		  } else {
		      return (7.5625*(t-=(2.625/2.75))*t + .984375);
		  }
	},

	backIn: function ( t ) {

		return (t)*t*((1.70158+1)*t - 1.70158);
	},

	backOut: function ( t ) {

		return ((t-=1)*t*((1.70158+1)*t + 1.70158) + 1);
	},

	elasticIn: function ( t ) {

		var p = 0.3,
			s = p/4,
			a = 1;

        if (t === 0) {

            return 0;
        }

        if ( t === 1 ) {

            return 1;
        }

        return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t-s)*(2*Math.PI)/p ));
	},

	elasticOut: function ( t ) {

		var p = 0.3,
			s = p/4,
			a = 1;

        if (t === 0) {

            return 0;
        }

        if ( t === 1 ) {

            return 1;
        }

        return a*Math.pow(2,-10*t) * Math.sin( (t-s)*(2*Math.PI)/p ) + 1;
	}
};

/**
 * Element visual
 */
Object.extend(PB.Element.prototype, {

	/**
	 *
	 *
	 * @param object to
	 * @param function after
	 * @param float duration
	 * @param string effect
	 */
	morph: function ( to/* after, duration, effect */ ) {

		var options = {

				to: to,
				duration: .4,	// Default duraton
				effect: "easeOut"
			},
			i = 1;

		if( typeof this._store.morph === "undefined" ) {

			this._store.morph = new PB.Morph();
		}

		for( ; i < arguments.length; i++ ) {

			switch( typeof arguments[i] ) {

				case "function":
					options.after = arguments[i];
					break;

				case "number":
					options.duration = arguments[i];
					break;

				case "string":
					options.effect = arguments[i];
					break;
			}
		}

		this._store.morph
			.reset( this, options )
			.start();
	},

	/**
	 * Stop morphing effect
	 *
	 * @param boolean skipEnd -> Default true
	 * @return PB.Element
	 */
	stopMorph: function ( skipEnd ) {

		if( typeof skipEnd !== 'boolean' ) {

			skipEnd = true;
		}

		if( typeof this._store.morph === "undefined" ) {

			return this;
		}

		this._store.morph.stop( skipEnd );

		return this;
	}
});

/**
 * Ajax
 */

(function(){

	var local = {

		'_callback_id': 0,

		'async': true
	};

	var JSONP = function ( options ) {

		if( typeof options.success === 'undefined' ) {

			throw new Error("No success function specified. Required for JSONP");
		}

		if( typeof options.callback === 'undefined' ) {

			options.callback = 'jsonp_'+(++local._callback_id)+Date.now();
		}

		options.data.callback = options.callback;

		this.options = options || {};
	};

	JSONP.prototype = {

		/**
		 *
		 */
		send: function () {

			var success = this.options.success,
				callback = this.options.callback,
				scriptTag = $(document.createElement('script')),
				url = this.options.url;

			if( typeof this.options.data !== 'undefined' ) {

				url += (url.indexOf('?') === -1 ? '?' : '&')+this.buildQueryString(this.options.data);
			}

			window[ callback ] = function ( json ){

				success( json );

				try {
					window[ callback ] = null;
					delete window[ callback ];
				} catch (e) {}

				scriptTag.remove();
			};

			scriptTag.attr('src', url).appendTo( document.getElementsByTagName('head')[0] );
		},

		buildQueryString: PB.Request.prototype.buildQueryString
	};

	PB.JSONP = {

		set: function ( key, value ) {

			if( local.hasOwnProperty(key) === false ) {

				throw new Error('Failed to set option `'+key+'` for JSONP');
			}

			local[key] = value;
		},

		get: function ( key ) {

			if( local.hasOwnProperty(key) === false ) {

				throw new Error('Failed to get option `'+key+'` for JSONP');
			}

			local[key] = value;
		},

		/**
		 * Return void, class should self destruct
		 */
		request: function ( options ) {

			var jsonp = new JSONP( options );

			jsonp.send();
		}
	}
})();


