File:  [LON-CAPA] / loncom / html / adm / countdown / jquery.countdown.js
Revision 1.1: download - view: text, annotated - select for diffs
Sun Mar 18 13:51:11 2012 UTC (12 years, 5 months ago) by foxr
Branches: MAIN
CVS tags: version_2_11_0_RC1, HEAD
Add jquery countdown widget to the repository (BZ 5891 resolution will use
this for the timer).

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

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