Annotation of loncom/html/adm/countdown/jquery.countdown.js, revision 1.2

1.1       foxr        1: /* http://keith-wood.name/countdown.html
1.2     ! raeburn     2:    Countdown for jQuery v1.6.3.
1.1       foxr        3:    Written by Keith Wood (kbwood{at}iinet.com.au) January 2008.
1.2     ! raeburn     4:    Available under the MIT (https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) license. 
1.1       foxr        5:    Please attribute the author if you use it. */
                      6: 
                      7: /* Display a countdown timer.
                      8:    Attach it with options like:
                      9:    $('div selector').countdown(
                     10:        {until: new Date(2009, 1 - 1, 1, 0, 0, 0), onExpiry: happyNewYear}); */
                     11: 
                     12: (function($) { // Hide scope, no $ conflict
                     13: 
                     14: /* Countdown manager. */
                     15: function Countdown() {
                     16: 	this.regional = []; // Available regional settings, indexed by language code
                     17: 	this.regional[''] = { // Default regional settings
                     18: 		// The display texts for the counters
                     19: 		labels: ['Years', 'Months', 'Weeks', 'Days', 'Hours', 'Minutes', 'Seconds'],
                     20: 		// The display texts for the counters if only one
                     21: 		labels1: ['Year', 'Month', 'Week', 'Day', 'Hour', 'Minute', 'Second'],
                     22: 		compactLabels: ['y', 'm', 'w', 'd'], // The compact texts for the counters
                     23: 		whichLabels: null, // Function to determine which labels to use
1.2     ! raeburn    24: 		digits: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], // The digits to display
1.1       foxr       25: 		timeSeparator: ':', // Separator for time periods
                     26: 		isRTL: false // True for right-to-left languages, false for left-to-right
                     27: 	};
                     28: 	this._defaults = {
                     29: 		until: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count down to
                     30: 			// or numeric for seconds offset, or string for unit offset(s):
                     31: 			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
                     32: 		since: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count up from
                     33: 			// or numeric for seconds offset, or string for unit offset(s):
                     34: 			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
                     35: 		timezone: null, // The timezone (hours or minutes from GMT) for the target times,
                     36: 			// or null for client local
                     37: 		serverSync: null, // A function to retrieve the current server time for synchronisation
                     38: 		format: 'dHMS', // Format for display - upper case for always, lower case only if non-zero,
                     39: 			// 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
                     40: 		layout: '', // Build your own layout for the countdown
                     41: 		compact: false, // True to display in a compact format, false for an expanded one
                     42: 		significant: 0, // The number of periods with values to show, zero for all
                     43: 		description: '', // The description displayed for the countdown
                     44: 		expiryUrl: '', // A URL to load upon expiry, replacing the current page
                     45: 		expiryText: '', // Text to display upon expiry, replacing the countdown
                     46: 		alwaysExpire: false, // True to trigger onExpiry even if never counted down
                     47: 		onExpiry: null, // Callback when the countdown expires -
                     48: 			// receives no parameters and 'this' is the containing division
                     49: 		onTick: null, // Callback when the countdown is updated -
                     50: 			// receives int[7] being the breakdown by period (based on format)
                     51: 			// and 'this' is the containing division
                     52: 		tickInterval: 1 // Interval (seconds) between onTick callbacks
                     53: 	};
                     54: 	$.extend(this._defaults, this.regional['']);
                     55: 	this._serverSyncs = [];
1.2     ! raeburn    56: 	var now = (typeof Date.now == 'function' ? Date.now :
        !            57: 		function() { return new Date().getTime(); });
        !            58: 	var perfAvail = (window.performance && typeof window.performance.now == 'function');
1.1       foxr       59: 	// Shared timer for all countdowns
                     60: 	function timerCallBack(timestamp) {
1.2     ! raeburn    61: 		var drawStart = (timestamp < 1e12 ? // New HTML5 high resolution timer
        !            62: 			(perfAvail ? (performance.now() + performance.timing.navigationStart) : now()) :
        !            63: 			// Integer milliseconds since unix epoch
        !            64: 			timestamp || now());
1.1       foxr       65: 		if (drawStart - animationStartTime >= 1000) {
1.2     ! raeburn    66: 			plugin._updateTargets();
1.1       foxr       67: 			animationStartTime = drawStart;
                     68: 		}
                     69: 		requestAnimationFrame(timerCallBack);
                     70: 	}
1.2     ! raeburn    71: 	var requestAnimationFrame = window.requestAnimationFrame ||
        !            72: 		window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||
        !            73: 		window.oRequestAnimationFrame || window.msRequestAnimationFrame || null;
        !            74: 		// This is when we expect a fall-back to setInterval as it's much more fluid
1.1       foxr       75: 	var animationStartTime = 0;
1.2     ! raeburn    76: 	if (!requestAnimationFrame || $.noRequestAnimationFrame) {
        !            77: 		$.noRequestAnimationFrame = null;
        !            78: 		setInterval(function() { plugin._updateTargets(); }, 980); // Fall back to good old setInterval
1.1       foxr       79: 	}
                     80: 	else {
1.2     ! raeburn    81: 		animationStartTime = window.animationStartTime ||
        !            82: 			window.webkitAnimationStartTime || window.mozAnimationStartTime ||
        !            83: 			window.oAnimationStartTime || window.msAnimationStartTime || now();
1.1       foxr       84: 		requestAnimationFrame(timerCallBack);
                     85: 	}
                     86: }
                     87: 
                     88: var Y = 0; // Years
                     89: var O = 1; // Months
                     90: var W = 2; // Weeks
                     91: var D = 3; // Days
                     92: var H = 4; // Hours
                     93: var M = 5; // Minutes
                     94: var S = 6; // Seconds
                     95: 
                     96: $.extend(Countdown.prototype, {
                     97: 	/* Class name added to elements to indicate already configured with countdown. */
                     98: 	markerClassName: 'hasCountdown',
1.2     ! raeburn    99: 	/* Name of the data property for instance settings. */
        !           100: 	propertyName: 'countdown',
        !           101: 
        !           102: 	/* Class name for the right-to-left marker. */
        !           103: 	_rtlClass: 'countdown_rtl',
        !           104: 	/* Class name for the countdown section marker. */
        !           105: 	_sectionClass: 'countdown_section',
        !           106: 	/* Class name for the period amount marker. */
        !           107: 	_amountClass: 'countdown_amount',
        !           108: 	/* Class name for the countdown row marker. */
        !           109: 	_rowClass: 'countdown_row',
        !           110: 	/* Class name for the holding countdown marker. */
        !           111: 	_holdingClass: 'countdown_holding',
        !           112: 	/* Class name for the showing countdown marker. */
        !           113: 	_showClass: 'countdown_show',
        !           114: 	/* Class name for the description marker. */
        !           115: 	_descrClass: 'countdown_descr',
1.1       foxr      116: 
                    117: 	/* List of currently active countdown targets. */
                    118: 	_timerTargets: [],
                    119: 	
                    120: 	/* Override the default settings for all instances of the countdown widget.
                    121: 	   @param  options  (object) the new settings to use as defaults */
                    122: 	setDefaults: function(options) {
                    123: 		this._resetExtraLabels(this._defaults, options);
1.2     ! raeburn   124: 		$.extend(this._defaults, options || {});
1.1       foxr      125: 	},
                    126: 
                    127: 	/* Convert a date/time to UTC.
                    128: 	   @param  tz     (number) the hour or minute offset from GMT, e.g. +9, -360
                    129: 	   @param  year   (Date) the date/time in that timezone or
                    130: 	                  (number) the year in that timezone
                    131: 	   @param  month  (number, optional) the month (0 - 11) (omit if year is a Date)
                    132: 	   @param  day    (number, optional) the day (omit if year is a Date)
                    133: 	   @param  hours  (number, optional) the hour (omit if year is a Date)
                    134: 	   @param  mins   (number, optional) the minute (omit if year is a Date)
                    135: 	   @param  secs   (number, optional) the second (omit if year is a Date)
                    136: 	   @param  ms     (number, optional) the millisecond (omit if year is a Date)
                    137: 	   @return  (Date) the equivalent UTC date/time */
                    138: 	UTCDate: function(tz, year, month, day, hours, mins, secs, ms) {
                    139: 		if (typeof year == 'object' && year.constructor == Date) {
                    140: 			ms = year.getMilliseconds();
                    141: 			secs = year.getSeconds();
                    142: 			mins = year.getMinutes();
                    143: 			hours = year.getHours();
                    144: 			day = year.getDate();
                    145: 			month = year.getMonth();
                    146: 			year = year.getFullYear();
                    147: 		}
                    148: 		var d = new Date();
                    149: 		d.setUTCFullYear(year);
                    150: 		d.setUTCDate(1);
                    151: 		d.setUTCMonth(month || 0);
                    152: 		d.setUTCDate(day || 1);
                    153: 		d.setUTCHours(hours || 0);
                    154: 		d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz));
                    155: 		d.setUTCSeconds(secs || 0);
                    156: 		d.setUTCMilliseconds(ms || 0);
                    157: 		return d;
                    158: 	},
                    159: 
                    160: 	/* Convert a set of periods into seconds.
                    161: 	   Averaged for months and years.
                    162: 	   @param  periods  (number[7]) the periods per year/month/week/day/hour/minute/second
                    163: 	   @return  (number) the corresponding number of seconds */
                    164: 	periodsToSeconds: function(periods) {
                    165: 		return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 +
                    166: 			periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6];
                    167: 	},
                    168: 
                    169: 	/* Attach the countdown widget to a div.
                    170: 	   @param  target   (element) the containing division
                    171: 	   @param  options  (object) the initial settings for the countdown */
1.2     ! raeburn   172: 	_attachPlugin: function(target, options) {
        !           173: 		target = $(target);
        !           174: 		if (target.hasClass(this.markerClassName)) {
1.1       foxr      175: 			return;
                    176: 		}
1.2     ! raeburn   177: 		var inst = {options: $.extend({}, this._defaults), _periods: [0, 0, 0, 0, 0, 0, 0]};
        !           178: 		target.addClass(this.markerClassName).data(this.propertyName, inst);
        !           179: 		this._optionPlugin(target, options);
1.1       foxr      180: 	},
                    181: 
                    182: 	/* Add a target to the list of active ones.
                    183: 	   @param  target  (element) the countdown target */
                    184: 	_addTarget: function(target) {
                    185: 		if (!this._hasTarget(target)) {
                    186: 			this._timerTargets.push(target);
                    187: 		}
                    188: 	},
                    189: 
                    190: 	/* See if a target is in the list of active ones.
                    191: 	   @param  target  (element) the countdown target
                    192: 	   @return  (boolean) true if present, false if not */
                    193: 	_hasTarget: function(target) {
                    194: 		return ($.inArray(target, this._timerTargets) > -1);
                    195: 	},
                    196: 
                    197: 	/* Remove a target from the list of active ones.
                    198: 	   @param  target  (element) the countdown target */
                    199: 	_removeTarget: function(target) {
                    200: 		this._timerTargets = $.map(this._timerTargets,
                    201: 			function(value) { return (value == target ? null : value); }); // delete entry
                    202: 	},
                    203: 
                    204: 	/* Update each active timer target. */
                    205: 	_updateTargets: function() {
                    206: 		for (var i = this._timerTargets.length - 1; i >= 0; i--) {
                    207: 			this._updateCountdown(this._timerTargets[i]);
                    208: 		}
                    209: 	},
                    210: 
1.2     ! raeburn   211: 	/* Reconfigure the settings for a countdown div.
        !           212: 	   @param  target   (element) the control to affect
        !           213: 	   @param  options  (object) the new options for this instance or
        !           214: 	                    (string) an individual property name
        !           215: 	   @param  value    (any) the individual property value (omit if options
        !           216: 	                    is an object or to retrieve the value of a setting)
        !           217: 	   @return  (any) if retrieving a value */
        !           218: 	_optionPlugin: function(target, options, value) {
        !           219: 		target = $(target);
        !           220: 		var inst = target.data(this.propertyName);
        !           221: 		if (!options || (typeof options == 'string' && value == null)) { // Get option
        !           222: 			var name = options;
        !           223: 			options = (inst || {}).options;
        !           224: 			return (options && name ? options[name] : options);
        !           225: 		}
        !           226: 
        !           227: 		if (!target.hasClass(this.markerClassName)) {
        !           228: 			return;
        !           229: 		}
        !           230: 		options = options || {};
        !           231: 		if (typeof options == 'string') {
        !           232: 			var name = options;
        !           233: 			options = {};
        !           234: 			options[name] = value;
        !           235: 		}
        !           236: 		if (options.layout) {
        !           237: 			options.layout = options.layout.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
        !           238: 		}
        !           239: 		this._resetExtraLabels(inst.options, options);
        !           240: 		var timezoneChanged = (inst.options.timezone != options.timezone);
        !           241: 		$.extend(inst.options, options);
        !           242: 		this._adjustSettings(target, inst,
        !           243: 			options.until != null || options.since != null || timezoneChanged);
        !           244: 		var now = new Date();
        !           245: 		if ((inst._since && inst._since < now) || (inst._until && inst._until > now)) {
        !           246: 			this._addTarget(target[0]);
        !           247: 		}
        !           248: 		this._updateCountdown(target, inst);
        !           249: 	},
        !           250: 
1.1       foxr      251: 	/* Redisplay the countdown with an updated display.
                    252: 	   @param  target  (jQuery) the containing division
                    253: 	   @param  inst    (object) the current settings for this instance */
                    254: 	_updateCountdown: function(target, inst) {
                    255: 		var $target = $(target);
1.2     ! raeburn   256: 		inst = inst || $target.data(this.propertyName);
1.1       foxr      257: 		if (!inst) {
                    258: 			return;
                    259: 		}
1.2     ! raeburn   260: 		$target.html(this._generateHTML(inst)).toggleClass(this._rtlClass, inst.options.isRTL);
        !           261: 		if ($.isFunction(inst.options.onTick)) {
1.1       foxr      262: 			var periods = inst._hold != 'lap' ? inst._periods :
1.2     ! raeburn   263: 				this._calculatePeriods(inst, inst._show, inst.options.significant, new Date());
        !           264: 			if (inst.options.tickInterval == 1 ||
        !           265: 					this.periodsToSeconds(periods) % inst.options.tickInterval == 0) {
        !           266: 				inst.options.onTick.apply(target, [periods]);
1.1       foxr      267: 			}
                    268: 		}
                    269: 		var expired = inst._hold != 'pause' &&
                    270: 			(inst._since ? inst._now.getTime() < inst._since.getTime() :
                    271: 			inst._now.getTime() >= inst._until.getTime());
                    272: 		if (expired && !inst._expiring) {
                    273: 			inst._expiring = true;
1.2     ! raeburn   274: 			if (this._hasTarget(target) || inst.options.alwaysExpire) {
1.1       foxr      275: 				this._removeTarget(target);
1.2     ! raeburn   276: 				if ($.isFunction(inst.options.onExpiry)) {
        !           277: 					inst.options.onExpiry.apply(target, []);
        !           278: 				}
        !           279: 				if (inst.options.expiryText) {
        !           280: 					var layout = inst.options.layout;
        !           281: 					inst.options.layout = inst.options.expiryText;
1.1       foxr      282: 					this._updateCountdown(target, inst);
                    283: 					inst.options.layout = layout;
                    284: 				}
1.2     ! raeburn   285: 				if (inst.options.expiryUrl) {
        !           286: 					window.location = inst.options.expiryUrl;
1.1       foxr      287: 				}
                    288: 			}
                    289: 			inst._expiring = false;
                    290: 		}
                    291: 		else if (inst._hold == 'pause') {
                    292: 			this._removeTarget(target);
                    293: 		}
1.2     ! raeburn   294: 		$target.data(this.propertyName, inst);
1.1       foxr      295: 	},
                    296: 
                    297: 	/* Reset any extra labelsn and compactLabelsn entries if changing labels.
                    298: 	   @param  base     (object) the options to be updated
                    299: 	   @param  options  (object) the new option values */
                    300: 	_resetExtraLabels: function(base, options) {
                    301: 		var changingLabels = false;
                    302: 		for (var n in options) {
                    303: 			if (n != 'whichLabels' && n.match(/[Ll]abels/)) {
                    304: 				changingLabels = true;
                    305: 				break;
                    306: 			}
                    307: 		}
                    308: 		if (changingLabels) {
                    309: 			for (var n in base) { // Remove custom numbered labels
1.2     ! raeburn   310: 				if (n.match(/[Ll]abels[02-9]|compactLabels1/)) {
1.1       foxr      311: 					base[n] = null;
                    312: 				}
                    313: 			}
                    314: 		}
                    315: 	},
                    316: 	
                    317: 	/* Calculate interal settings for an instance.
                    318: 	   @param  target  (element) the containing division
1.2     ! raeburn   319: 	   @param  inst    (object) the current settings for this instance
        !           320: 	   @param  recalc  (boolean) true if until or since are set */
        !           321: 	_adjustSettings: function(target, inst, recalc) {
1.1       foxr      322: 		var now;
                    323: 		var serverOffset = 0;
                    324: 		var serverEntry = null;
                    325: 		for (var i = 0; i < this._serverSyncs.length; i++) {
1.2     ! raeburn   326: 			if (this._serverSyncs[i][0] == inst.options.serverSync) {
1.1       foxr      327: 				serverEntry = this._serverSyncs[i][1];
                    328: 				break;
                    329: 			}
                    330: 		}
                    331: 		if (serverEntry != null) {
1.2     ! raeburn   332: 			serverOffset = (inst.options.serverSync ? serverEntry : 0);
1.1       foxr      333: 			now = new Date();
                    334: 		}
                    335: 		else {
1.2     ! raeburn   336: 			var serverResult = ($.isFunction(inst.options.serverSync) ?
        !           337: 				inst.options.serverSync.apply(target, []) : null);
1.1       foxr      338: 			now = new Date();
                    339: 			serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
1.2     ! raeburn   340: 			this._serverSyncs.push([inst.options.serverSync, serverOffset]);
1.1       foxr      341: 		}
1.2     ! raeburn   342: 		var timezone = inst.options.timezone;
1.1       foxr      343: 		timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
1.2     ! raeburn   344: 		if (recalc || (!recalc && inst._until == null && inst._since == null)) {
        !           345: 			inst._since = inst.options.since;
        !           346: 			if (inst._since != null) {
        !           347: 				inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null));
        !           348: 				if (inst._since && serverOffset) {
        !           349: 					inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset);
        !           350: 				}
        !           351: 			}
        !           352: 			inst._until = this.UTCDate(timezone, this._determineTime(inst.options.until, now));
        !           353: 			if (serverOffset) {
        !           354: 				inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset);
1.1       foxr      355: 			}
                    356: 		}
                    357: 		inst._show = this._determineShow(inst);
                    358: 	},
                    359: 
                    360: 	/* Remove the countdown widget from a div.
                    361: 	   @param  target  (element) the containing division */
1.2     ! raeburn   362: 	_destroyPlugin: function(target) {
        !           363: 		target = $(target);
        !           364: 		if (!target.hasClass(this.markerClassName)) {
1.1       foxr      365: 			return;
                    366: 		}
1.2     ! raeburn   367: 		this._removeTarget(target[0]);
        !           368: 		target.removeClass(this.markerClassName).empty().removeData(this.propertyName);
1.1       foxr      369: 	},
                    370: 
                    371: 	/* Pause a countdown widget at the current time.
                    372: 	   Stop it running but remember and display the current time.
                    373: 	   @param  target  (element) the containing division */
1.2     ! raeburn   374: 	_pausePlugin: function(target) {
1.1       foxr      375: 		this._hold(target, 'pause');
                    376: 	},
                    377: 
                    378: 	/* Pause a countdown widget at the current time.
                    379: 	   Stop the display but keep the countdown running.
                    380: 	   @param  target  (element) the containing division */
1.2     ! raeburn   381: 	_lapPlugin: function(target) {
1.1       foxr      382: 		this._hold(target, 'lap');
                    383: 	},
                    384: 
                    385: 	/* Resume a paused countdown widget.
                    386: 	   @param  target  (element) the containing division */
1.2     ! raeburn   387: 	_resumePlugin: function(target) {
1.1       foxr      388: 		this._hold(target, null);
                    389: 	},
                    390: 
                    391: 	/* Pause or resume a countdown widget.
                    392: 	   @param  target  (element) the containing division
                    393: 	   @param  hold    (string) the new hold setting */
                    394: 	_hold: function(target, hold) {
1.2     ! raeburn   395: 		var inst = $.data(target, this.propertyName);
1.1       foxr      396: 		if (inst) {
                    397: 			if (inst._hold == 'pause' && !hold) {
                    398: 				inst._periods = inst._savePeriods;
                    399: 				var sign = (inst._since ? '-' : '+');
                    400: 				inst[inst._since ? '_since' : '_until'] =
                    401: 					this._determineTime(sign + inst._periods[0] + 'y' +
                    402: 						sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' +
                    403: 						sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' + 
                    404: 						sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's');
                    405: 				this._addTarget(target);
                    406: 			}
                    407: 			inst._hold = hold;
                    408: 			inst._savePeriods = (hold == 'pause' ? inst._periods : null);
1.2     ! raeburn   409: 			$.data(target, this.propertyName, inst);
1.1       foxr      410: 			this._updateCountdown(target, inst);
                    411: 		}
                    412: 	},
                    413: 
                    414: 	/* Return the current time periods.
                    415: 	   @param  target  (element) the containing division
                    416: 	   @return  (number[7]) the current periods for the countdown */
1.2     ! raeburn   417: 	_getTimesPlugin: function(target) {
        !           418: 		var inst = $.data(target, this.propertyName);
        !           419: 		return (!inst ? null : (inst._hold == 'pause' ? inst._savePeriods : (!inst._hold ? inst._periods :
        !           420: 			this._calculatePeriods(inst, inst._show, inst.options.significant, new Date()))));
1.1       foxr      421: 	},
                    422: 
                    423: 	/* A time may be specified as an exact value or a relative one.
                    424: 	   @param  setting      (string or number or Date) - the date/time value
                    425: 	                        as a relative or absolute value
                    426: 	   @param  defaultTime  (Date) the date/time to use if no other is supplied
                    427: 	   @return  (Date) the corresponding date/time */
                    428: 	_determineTime: function(setting, defaultTime) {
                    429: 		var offsetNumeric = function(offset) { // e.g. +300, -2
                    430: 			var time = new Date();
                    431: 			time.setTime(time.getTime() + offset * 1000);
                    432: 			return time;
                    433: 		};
                    434: 		var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m'
                    435: 			offset = offset.toLowerCase();
                    436: 			var time = new Date();
                    437: 			var year = time.getFullYear();
                    438: 			var month = time.getMonth();
                    439: 			var day = time.getDate();
                    440: 			var hour = time.getHours();
                    441: 			var minute = time.getMinutes();
                    442: 			var second = time.getSeconds();
                    443: 			var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g;
                    444: 			var matches = pattern.exec(offset);
                    445: 			while (matches) {
                    446: 				switch (matches[2] || 's') {
                    447: 					case 's': second += parseInt(matches[1], 10); break;
                    448: 					case 'm': minute += parseInt(matches[1], 10); break;
                    449: 					case 'h': hour += parseInt(matches[1], 10); break;
                    450: 					case 'd': day += parseInt(matches[1], 10); break;
                    451: 					case 'w': day += parseInt(matches[1], 10) * 7; break;
                    452: 					case 'o':
                    453: 						month += parseInt(matches[1], 10); 
1.2     ! raeburn   454: 						day = Math.min(day, plugin._getDaysInMonth(year, month));
1.1       foxr      455: 						break;
                    456: 					case 'y':
                    457: 						year += parseInt(matches[1], 10);
1.2     ! raeburn   458: 						day = Math.min(day, plugin._getDaysInMonth(year, month));
1.1       foxr      459: 						break;
                    460: 				}
                    461: 				matches = pattern.exec(offset);
                    462: 			}
                    463: 			return new Date(year, month, day, hour, minute, second, 0);
                    464: 		};
                    465: 		var time = (setting == null ? defaultTime :
                    466: 			(typeof setting == 'string' ? offsetString(setting) :
                    467: 			(typeof setting == 'number' ? offsetNumeric(setting) : setting)));
                    468: 		if (time) time.setMilliseconds(0);
                    469: 		return time;
                    470: 	},
                    471: 
                    472: 	/* Determine the number of days in a month.
                    473: 	   @param  year   (number) the year
                    474: 	   @param  month  (number) the month
                    475: 	   @return  (number) the days in that month */
                    476: 	_getDaysInMonth: function(year, month) {
                    477: 		return 32 - new Date(year, month, 32).getDate();
                    478: 	},
                    479: 
                    480: 	/* Determine which set of labels should be used for an amount.
                    481: 	   @param  num  (number) the amount to be displayed
                    482: 	   @return  (number) the set of labels to be used for this amount */
                    483: 	_normalLabels: function(num) {
                    484: 		return num;
                    485: 	},
                    486: 
                    487: 	/* Generate the HTML to display the countdown widget.
                    488: 	   @param  inst  (object) the current settings for this instance
                    489: 	   @return  (string) the new HTML for the countdown display */
                    490: 	_generateHTML: function(inst) {
1.2     ! raeburn   491: 		var self = this;
1.1       foxr      492: 		// Determine what to show
                    493: 		inst._periods = (inst._hold ? inst._periods :
1.2     ! raeburn   494: 			this._calculatePeriods(inst, inst._show, inst.options.significant, new Date()));
1.1       foxr      495: 		// Show all 'asNeeded' after first non-zero value
                    496: 		var shownNonZero = false;
                    497: 		var showCount = 0;
1.2     ! raeburn   498: 		var sigCount = inst.options.significant;
1.1       foxr      499: 		var show = $.extend({}, inst._show);
                    500: 		for (var period = Y; period <= S; period++) {
                    501: 			shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0);
                    502: 			show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]);
                    503: 			showCount += (show[period] ? 1 : 0);
                    504: 			sigCount -= (inst._periods[period] > 0 ? 1 : 0);
                    505: 		}
                    506: 		var showSignificant = [false, false, false, false, false, false, false];
                    507: 		for (var period = S; period >= Y; period--) { // Determine significant periods
                    508: 			if (inst._show[period]) {
                    509: 				if (inst._periods[period]) {
                    510: 					showSignificant[period] = true;
                    511: 				}
                    512: 				else {
                    513: 					showSignificant[period] = sigCount > 0;
                    514: 					sigCount--;
                    515: 				}
                    516: 			}
                    517: 		}
1.2     ! raeburn   518: 		var labels = (inst.options.compact ? inst.options.compactLabels : inst.options.labels);
        !           519: 		var whichLabels = inst.options.whichLabels || this._normalLabels;
1.1       foxr      520: 		var showCompact = function(period) {
1.2     ! raeburn   521: 			var labelsNum = inst.options['compactLabels' + whichLabels(inst._periods[period])];
        !           522: 			return (show[period] ? self._translateDigits(inst, inst._periods[period]) +
1.1       foxr      523: 				(labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
                    524: 		};
                    525: 		var showFull = function(period) {
1.2     ! raeburn   526: 			var labelsNum = inst.options['labels' + whichLabels(inst._periods[period])];
        !           527: 			return ((!inst.options.significant && show[period]) ||
        !           528: 				(inst.options.significant && showSignificant[period]) ?
        !           529: 				'<span class="' + plugin._sectionClass + '">' +
        !           530: 				'<span class="' + plugin._amountClass + '">' +
        !           531: 				self._translateDigits(inst, inst._periods[period]) + '</span><br/>' +
1.1       foxr      532: 				(labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
                    533: 		};
1.2     ! raeburn   534: 		return (inst.options.layout ? this._buildLayout(inst, show, inst.options.layout,
        !           535: 			inst.options.compact, inst.options.significant, showSignificant) :
        !           536: 			((inst.options.compact ? // Compact version
        !           537: 			'<span class="' + this._rowClass + ' ' + this._amountClass +
        !           538: 			(inst._hold ? ' ' + this._holdingClass : '') + '">' + 
1.1       foxr      539: 			showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) + 
1.2     ! raeburn   540: 			(show[H] ? this._minDigits(inst, inst._periods[H], 2) : '') +
        !           541: 			(show[M] ? (show[H] ? inst.options.timeSeparator : '') +
        !           542: 			this._minDigits(inst, inst._periods[M], 2) : '') +
        !           543: 			(show[S] ? (show[H] || show[M] ? inst.options.timeSeparator : '') +
        !           544: 			this._minDigits(inst, inst._periods[S], 2) : '') :
1.1       foxr      545: 			// Full version
1.2     ! raeburn   546: 			'<span class="' + this._rowClass + ' ' + this._showClass + (inst.options.significant || showCount) +
        !           547: 			(inst._hold ? ' ' + this._holdingClass : '') + '">' +
1.1       foxr      548: 			showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
                    549: 			showFull(H) + showFull(M) + showFull(S)) + '</span>' +
1.2     ! raeburn   550: 			(inst.options.description ? '<span class="' + this._rowClass + ' ' + this._descrClass + '">' +
        !           551: 			inst.options.description + '</span>' : '')));
1.1       foxr      552: 	},
                    553: 
                    554: 	/* Construct a custom layout.
                    555: 	   @param  inst             (object) the current settings for this instance
                    556: 	   @param  show             (string[7]) flags indicating which periods are requested
                    557: 	   @param  layout           (string) the customised layout
                    558: 	   @param  compact          (boolean) true if using compact labels
                    559: 	   @param  significant      (number) the number of periods with values to show, zero for all
                    560: 	   @param  showSignificant  (boolean[7]) other periods to show for significance
                    561: 	   @return  (string) the custom HTML */
                    562: 	_buildLayout: function(inst, show, layout, compact, significant, showSignificant) {
1.2     ! raeburn   563: 		var labels = inst.options[compact ? 'compactLabels' : 'labels'];
        !           564: 		var whichLabels = inst.options.whichLabels || this._normalLabels;
1.1       foxr      565: 		var labelFor = function(index) {
1.2     ! raeburn   566: 			return (inst.options[(compact ? 'compactLabels' : 'labels') +
        !           567: 				whichLabels(inst._periods[index])] || labels)[index];
1.1       foxr      568: 		};
                    569: 		var digit = function(value, position) {
1.2     ! raeburn   570: 			return inst.options.digits[Math.floor(value / position) % 10];
1.1       foxr      571: 		};
1.2     ! raeburn   572: 		var subs = {desc: inst.options.description, sep: inst.options.timeSeparator,
        !           573: 			yl: labelFor(Y), yn: this._minDigits(inst, inst._periods[Y], 1),
        !           574: 			ynn: this._minDigits(inst, inst._periods[Y], 2),
        !           575: 			ynnn: this._minDigits(inst, inst._periods[Y], 3), y1: digit(inst._periods[Y], 1),
1.1       foxr      576: 			y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
                    577: 			y1000: digit(inst._periods[Y], 1000),
1.2     ! raeburn   578: 			ol: labelFor(O), on: this._minDigits(inst, inst._periods[O], 1),
        !           579: 			onn: this._minDigits(inst, inst._periods[O], 2),
        !           580: 			onnn: this._minDigits(inst, inst._periods[O], 3), o1: digit(inst._periods[O], 1),
1.1       foxr      581: 			o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
                    582: 			o1000: digit(inst._periods[O], 1000),
1.2     ! raeburn   583: 			wl: labelFor(W), wn: this._minDigits(inst, inst._periods[W], 1),
        !           584: 			wnn: this._minDigits(inst, inst._periods[W], 2),
        !           585: 			wnnn: this._minDigits(inst, inst._periods[W], 3), w1: digit(inst._periods[W], 1),
1.1       foxr      586: 			w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
                    587: 			w1000: digit(inst._periods[W], 1000),
1.2     ! raeburn   588: 			dl: labelFor(D), dn: this._minDigits(inst, inst._periods[D], 1),
        !           589: 			dnn: this._minDigits(inst, inst._periods[D], 2),
        !           590: 			dnnn: this._minDigits(inst, inst._periods[D], 3), d1: digit(inst._periods[D], 1),
1.1       foxr      591: 			d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
                    592: 			d1000: digit(inst._periods[D], 1000),
1.2     ! raeburn   593: 			hl: labelFor(H), hn: this._minDigits(inst, inst._periods[H], 1),
        !           594: 			hnn: this._minDigits(inst, inst._periods[H], 2),
        !           595: 			hnnn: this._minDigits(inst, inst._periods[H], 3), h1: digit(inst._periods[H], 1),
1.1       foxr      596: 			h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
                    597: 			h1000: digit(inst._periods[H], 1000),
1.2     ! raeburn   598: 			ml: labelFor(M), mn: this._minDigits(inst, inst._periods[M], 1),
        !           599: 			mnn: this._minDigits(inst, inst._periods[M], 2),
        !           600: 			mnnn: this._minDigits(inst, inst._periods[M], 3), m1: digit(inst._periods[M], 1),
1.1       foxr      601: 			m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
                    602: 			m1000: digit(inst._periods[M], 1000),
1.2     ! raeburn   603: 			sl: labelFor(S), sn: this._minDigits(inst, inst._periods[S], 1),
        !           604: 			snn: this._minDigits(inst, inst._periods[S], 2),
        !           605: 			snnn: this._minDigits(inst, inst._periods[S], 3), s1: digit(inst._periods[S], 1),
1.1       foxr      606: 			s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100),
                    607: 			s1000: digit(inst._periods[S], 1000)};
                    608: 		var html = layout;
                    609: 		// Replace period containers: {p<}...{p>}
                    610: 		for (var i = Y; i <= S; i++) {
                    611: 			var period = 'yowdhms'.charAt(i);
1.2     ! raeburn   612: 			var re = new RegExp('\\{' + period + '<\\}([\\s\\S]*)\\{' + period + '>\\}', 'g');
1.1       foxr      613: 			html = html.replace(re, ((!significant && show[i]) ||
                    614: 				(significant && showSignificant[i]) ? '$1' : ''));
                    615: 		}
                    616: 		// Replace period values: {pn}
                    617: 		$.each(subs, function(n, v) {
                    618: 			var re = new RegExp('\\{' + n + '\\}', 'g');
                    619: 			html = html.replace(re, v);
                    620: 		});
                    621: 		return html;
                    622: 	},
                    623: 
                    624: 	/* Ensure a numeric value has at least n digits for display.
1.2     ! raeburn   625: 	   @param  inst   (object) the current settings for this instance
1.1       foxr      626: 	   @param  value  (number) the value to display
                    627: 	   @param  len    (number) the minimum length
                    628: 	   @return  (string) the display text */
1.2     ! raeburn   629: 	_minDigits: function(inst, value, len) {
1.1       foxr      630: 		value = '' + value;
                    631: 		if (value.length >= len) {
1.2     ! raeburn   632: 			return this._translateDigits(inst, value);
1.1       foxr      633: 		}
                    634: 		value = '0000000000' + value;
1.2     ! raeburn   635: 		return this._translateDigits(inst, value.substr(value.length - len));
        !           636: 	},
        !           637: 
        !           638: 	/* Translate digits into other representations.
        !           639: 	   @param  inst   (object) the current settings for this instance
        !           640: 	   @param  value  (string) the text to translate
        !           641: 	   @return  (string) the translated text */
        !           642: 	_translateDigits: function(inst, value) {
        !           643: 		return ('' + value).replace(/[0-9]/g, function(digit) {
        !           644: 				return inst.options.digits[digit];
        !           645: 			});
1.1       foxr      646: 	},
                    647: 
                    648: 	/* Translate the format into flags for each period.
                    649: 	   @param  inst  (object) the current settings for this instance
                    650: 	   @return  (string[7]) flags indicating which periods are requested (?) or
                    651: 	            required (!) by year, month, week, day, hour, minute, second */
                    652: 	_determineShow: function(inst) {
1.2     ! raeburn   653: 		var format = inst.options.format;
1.1       foxr      654: 		var show = [];
                    655: 		show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null));
                    656: 		show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null));
                    657: 		show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null));
                    658: 		show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null));
                    659: 		show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null));
                    660: 		show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null));
                    661: 		show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null));
                    662: 		return show;
                    663: 	},
                    664: 	
                    665: 	/* Calculate the requested periods between now and the target time.
                    666: 	   @param  inst         (object) the current settings for this instance
                    667: 	   @param  show         (string[7]) flags indicating which periods are requested/required
                    668: 	   @param  significant  (number) the number of periods with values to show, zero for all
                    669: 	   @param  now          (Date) the current date and time
                    670: 	   @return  (number[7]) the current time periods (always positive)
                    671: 	            by year, month, week, day, hour, minute, second */
                    672: 	_calculatePeriods: function(inst, show, significant, now) {
                    673: 		// Find endpoints
                    674: 		inst._now = now;
                    675: 		inst._now.setMilliseconds(0);
                    676: 		var until = new Date(inst._now.getTime());
                    677: 		if (inst._since) {
                    678: 			if (now.getTime() < inst._since.getTime()) {
                    679: 				inst._now = now = until;
                    680: 			}
                    681: 			else {
                    682: 				now = inst._since;
                    683: 			}
                    684: 		}
                    685: 		else {
                    686: 			until.setTime(inst._until.getTime());
                    687: 			if (now.getTime() > inst._until.getTime()) {
                    688: 				inst._now = now = until;
                    689: 			}
                    690: 		}
                    691: 		// Calculate differences by period
                    692: 		var periods = [0, 0, 0, 0, 0, 0, 0];
                    693: 		if (show[Y] || show[O]) {
                    694: 			// Treat end of months as the same
1.2     ! raeburn   695: 			var lastNow = plugin._getDaysInMonth(now.getFullYear(), now.getMonth());
        !           696: 			var lastUntil = plugin._getDaysInMonth(until.getFullYear(), until.getMonth());
1.1       foxr      697: 			var sameDay = (until.getDate() == now.getDate() ||
                    698: 				(until.getDate() >= Math.min(lastNow, lastUntil) &&
                    699: 				now.getDate() >= Math.min(lastNow, lastUntil)));
                    700: 			var getSecs = function(date) {
                    701: 				return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds();
                    702: 			};
                    703: 			var months = Math.max(0,
                    704: 				(until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() +
                    705: 				((until.getDate() < now.getDate() && !sameDay) ||
                    706: 				(sameDay && getSecs(until) < getSecs(now)) ? -1 : 0));
                    707: 			periods[Y] = (show[Y] ? Math.floor(months / 12) : 0);
                    708: 			periods[O] = (show[O] ? months - periods[Y] * 12 : 0);
                    709: 			// Adjust for months difference and end of month if necessary
                    710: 			now = new Date(now.getTime());
                    711: 			var wasLastDay = (now.getDate() == lastNow);
1.2     ! raeburn   712: 			var lastDay = plugin._getDaysInMonth(now.getFullYear() + periods[Y],
1.1       foxr      713: 				now.getMonth() + periods[O]);
                    714: 			if (now.getDate() > lastDay) {
                    715: 				now.setDate(lastDay);
                    716: 			}
                    717: 			now.setFullYear(now.getFullYear() + periods[Y]);
                    718: 			now.setMonth(now.getMonth() + periods[O]);
                    719: 			if (wasLastDay) {
                    720: 				now.setDate(lastDay);
                    721: 			}
                    722: 		}
                    723: 		var diff = Math.floor((until.getTime() - now.getTime()) / 1000);
                    724: 		var extractPeriod = function(period, numSecs) {
                    725: 			periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0);
                    726: 			diff -= periods[period] * numSecs;
                    727: 		};
                    728: 		extractPeriod(W, 604800);
                    729: 		extractPeriod(D, 86400);
                    730: 		extractPeriod(H, 3600);
                    731: 		extractPeriod(M, 60);
                    732: 		extractPeriod(S, 1);
                    733: 		if (diff > 0 && !inst._since) { // Round up if left overs
                    734: 			var multiplier = [1, 12, 4.3482, 7, 24, 60, 60];
                    735: 			var lastShown = S;
                    736: 			var max = 1;
                    737: 			for (var period = S; period >= Y; period--) {
                    738: 				if (show[period]) {
                    739: 					if (periods[lastShown] >= max) {
                    740: 						periods[lastShown] = 0;
                    741: 						diff = 1;
                    742: 					}
                    743: 					if (diff > 0) {
                    744: 						periods[period]++;
                    745: 						diff = 0;
                    746: 						lastShown = period;
                    747: 						max = 1;
                    748: 					}
                    749: 				}
                    750: 				max *= multiplier[period];
                    751: 			}
                    752: 		}
                    753: 		if (significant) { // Zero out insignificant periods
                    754: 			for (var period = Y; period <= S; period++) {
                    755: 				if (significant && periods[period]) {
                    756: 					significant--;
                    757: 				}
                    758: 				else if (!significant) {
                    759: 					periods[period] = 0;
                    760: 				}
                    761: 			}
                    762: 		}
                    763: 		return periods;
                    764: 	}
                    765: });
                    766: 
1.2     ! raeburn   767: // The list of commands that return values and don't permit chaining
        !           768: var getters = ['getTimes'];
        !           769: 
        !           770: /* Determine whether a command is a getter and doesn't permit chaining.
        !           771:    @param  command    (string, optional) the command to run
        !           772:    @param  otherArgs  ([], optional) any other arguments for the command
        !           773:    @return  true if the command is a getter, false if not */
        !           774: function isNotChained(command, otherArgs) {
        !           775: 	if (command == 'option' && (otherArgs.length == 0 ||
        !           776: 			(otherArgs.length == 1 && typeof otherArgs[0] == 'string'))) {
        !           777: 		return true;
1.1       foxr      778: 	}
1.2     ! raeburn   779: 	return $.inArray(command, getters) > -1;
1.1       foxr      780: }
                    781: 
                    782: /* Process the countdown functionality for a jQuery selection.
1.2     ! raeburn   783:    @param  options  (object) the new settings to use for these instances (optional) or
        !           784:                     (string) the command to run (optional)
        !           785:    @return  (jQuery) for chaining further calls or
        !           786:             (any) getter value */
1.1       foxr      787: $.fn.countdown = function(options) {
                    788: 	var otherArgs = Array.prototype.slice.call(arguments, 1);
1.2     ! raeburn   789: 	if (isNotChained(options, otherArgs)) {
        !           790: 		return plugin['_' + options + 'Plugin'].
        !           791: 			apply(plugin, [this[0]].concat(otherArgs));
1.1       foxr      792: 	}
                    793: 	return this.each(function() {
                    794: 		if (typeof options == 'string') {
1.2     ! raeburn   795: 			if (!plugin['_' + options + 'Plugin']) {
        !           796: 				throw 'Unknown command: ' + options;
        !           797: 			}
        !           798: 			plugin['_' + options + 'Plugin'].
        !           799: 				apply(plugin, [this].concat(otherArgs));
1.1       foxr      800: 		}
                    801: 		else {
1.2     ! raeburn   802: 			plugin._attachPlugin(this, options || {});
1.1       foxr      803: 		}
                    804: 	});
                    805: };
                    806: 
                    807: /* Initialise the countdown functionality. */
1.2     ! raeburn   808: var plugin = $.countdown = new Countdown(); // Singleton instance
1.1       foxr      809: 
                    810: })(jQuery);

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>