File:  [LON-CAPA] / loncom / html / adm / countdown / jquery.countdown.js
Revision 1.2: download - view: text, annotated - select for diffs
Wed Jan 15 14:45:11 2014 UTC (10 years, 6 months ago) by raeburn
Branches: MAIN
CVS tags: version_2_12_X, version_2_11_X, version_2_11_5, version_2_11_4_uiuc, version_2_11_4_msu, version_2_11_4, version_2_11_3_uiuc, version_2_11_3_msu, version_2_11_3, version_2_11_2_uiuc, version_2_11_2_msu, version_2_11_2_educog, version_2_11_2, version_2_11_1, version_2_11_0_RC3, version_2_11_0_RC2, version_2_11_0, HEAD
- Update jquery.countdown to 1.6.3

    1: /* http://keith-wood.name/countdown.html
    2:    Countdown for jQuery v1.6.3.
    3:    Written by Keith Wood (kbwood{at}iinet.com.au) January 2008.
    4:    Available under the MIT (https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) license. 
    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
   24: 		digits: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], // The digits to display
   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: 	var now = (typeof Date.now == 'function' ? Date.now :
   57: 		function() { return new Date().getTime(); });
   58: 	var perfAvail = (window.performance && typeof window.performance.now == 'function');
   59: 	// Shared timer for all countdowns
   60: 	function timerCallBack(timestamp) {
   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());
   65: 		if (drawStart - animationStartTime >= 1000) {
   66: 			plugin._updateTargets();
   67: 			animationStartTime = drawStart;
   68: 		}
   69: 		requestAnimationFrame(timerCallBack);
   70: 	}
   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
   75: 	var animationStartTime = 0;
   76: 	if (!requestAnimationFrame || $.noRequestAnimationFrame) {
   77: 		$.noRequestAnimationFrame = null;
   78: 		setInterval(function() { plugin._updateTargets(); }, 980); // Fall back to good old setInterval
   79: 	}
   80: 	else {
   81: 		animationStartTime = window.animationStartTime ||
   82: 			window.webkitAnimationStartTime || window.mozAnimationStartTime ||
   83: 			window.oAnimationStartTime || window.msAnimationStartTime || now();
   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',
   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',
  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);
  124: 		$.extend(this._defaults, options || {});
  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 */
  172: 	_attachPlugin: function(target, options) {
  173: 		target = $(target);
  174: 		if (target.hasClass(this.markerClassName)) {
  175: 			return;
  176: 		}
  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);
  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: 
  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: 
  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);
  256: 		inst = inst || $target.data(this.propertyName);
  257: 		if (!inst) {
  258: 			return;
  259: 		}
  260: 		$target.html(this._generateHTML(inst)).toggleClass(this._rtlClass, inst.options.isRTL);
  261: 		if ($.isFunction(inst.options.onTick)) {
  262: 			var periods = inst._hold != 'lap' ? inst._periods :
  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]);
  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;
  274: 			if (this._hasTarget(target) || inst.options.alwaysExpire) {
  275: 				this._removeTarget(target);
  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;
  282: 					this._updateCountdown(target, inst);
  283: 					inst.options.layout = layout;
  284: 				}
  285: 				if (inst.options.expiryUrl) {
  286: 					window.location = inst.options.expiryUrl;
  287: 				}
  288: 			}
  289: 			inst._expiring = false;
  290: 		}
  291: 		else if (inst._hold == 'pause') {
  292: 			this._removeTarget(target);
  293: 		}
  294: 		$target.data(this.propertyName, inst);
  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
  310: 				if (n.match(/[Ll]abels[02-9]|compactLabels1/)) {
  311: 					base[n] = null;
  312: 				}
  313: 			}
  314: 		}
  315: 	},
  316: 	
  317: 	/* Calculate interal settings for an instance.
  318: 	   @param  target  (element) the containing division
  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) {
  322: 		var now;
  323: 		var serverOffset = 0;
  324: 		var serverEntry = null;
  325: 		for (var i = 0; i < this._serverSyncs.length; i++) {
  326: 			if (this._serverSyncs[i][0] == inst.options.serverSync) {
  327: 				serverEntry = this._serverSyncs[i][1];
  328: 				break;
  329: 			}
  330: 		}
  331: 		if (serverEntry != null) {
  332: 			serverOffset = (inst.options.serverSync ? serverEntry : 0);
  333: 			now = new Date();
  334: 		}
  335: 		else {
  336: 			var serverResult = ($.isFunction(inst.options.serverSync) ?
  337: 				inst.options.serverSync.apply(target, []) : null);
  338: 			now = new Date();
  339: 			serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
  340: 			this._serverSyncs.push([inst.options.serverSync, serverOffset]);
  341: 		}
  342: 		var timezone = inst.options.timezone;
  343: 		timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
  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);
  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 */
  362: 	_destroyPlugin: function(target) {
  363: 		target = $(target);
  364: 		if (!target.hasClass(this.markerClassName)) {
  365: 			return;
  366: 		}
  367: 		this._removeTarget(target[0]);
  368: 		target.removeClass(this.markerClassName).empty().removeData(this.propertyName);
  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 */
  374: 	_pausePlugin: function(target) {
  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 */
  381: 	_lapPlugin: function(target) {
  382: 		this._hold(target, 'lap');
  383: 	},
  384: 
  385: 	/* Resume a paused countdown widget.
  386: 	   @param  target  (element) the containing division */
  387: 	_resumePlugin: function(target) {
  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) {
  395: 		var inst = $.data(target, this.propertyName);
  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);
  409: 			$.data(target, this.propertyName, inst);
  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 */
  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()))));
  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); 
  454: 						day = Math.min(day, plugin._getDaysInMonth(year, month));
  455: 						break;
  456: 					case 'y':
  457: 						year += parseInt(matches[1], 10);
  458: 						day = Math.min(day, plugin._getDaysInMonth(year, month));
  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) {
  491: 		var self = this;
  492: 		// Determine what to show
  493: 		inst._periods = (inst._hold ? inst._periods :
  494: 			this._calculatePeriods(inst, inst._show, inst.options.significant, new Date()));
  495: 		// Show all 'asNeeded' after first non-zero value
  496: 		var shownNonZero = false;
  497: 		var showCount = 0;
  498: 		var sigCount = inst.options.significant;
  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: 		}
  518: 		var labels = (inst.options.compact ? inst.options.compactLabels : inst.options.labels);
  519: 		var whichLabels = inst.options.whichLabels || this._normalLabels;
  520: 		var showCompact = function(period) {
  521: 			var labelsNum = inst.options['compactLabels' + whichLabels(inst._periods[period])];
  522: 			return (show[period] ? self._translateDigits(inst, inst._periods[period]) +
  523: 				(labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
  524: 		};
  525: 		var showFull = function(period) {
  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/>' +
  532: 				(labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
  533: 		};
  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 : '') + '">' + 
  539: 			showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) + 
  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) : '') :
  545: 			// Full version
  546: 			'<span class="' + this._rowClass + ' ' + this._showClass + (inst.options.significant || showCount) +
  547: 			(inst._hold ? ' ' + this._holdingClass : '') + '">' +
  548: 			showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
  549: 			showFull(H) + showFull(M) + showFull(S)) + '</span>' +
  550: 			(inst.options.description ? '<span class="' + this._rowClass + ' ' + this._descrClass + '">' +
  551: 			inst.options.description + '</span>' : '')));
  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) {
  563: 		var labels = inst.options[compact ? 'compactLabels' : 'labels'];
  564: 		var whichLabels = inst.options.whichLabels || this._normalLabels;
  565: 		var labelFor = function(index) {
  566: 			return (inst.options[(compact ? 'compactLabels' : 'labels') +
  567: 				whichLabels(inst._periods[index])] || labels)[index];
  568: 		};
  569: 		var digit = function(value, position) {
  570: 			return inst.options.digits[Math.floor(value / position) % 10];
  571: 		};
  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),
  576: 			y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
  577: 			y1000: digit(inst._periods[Y], 1000),
  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),
  581: 			o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
  582: 			o1000: digit(inst._periods[O], 1000),
  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),
  586: 			w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
  587: 			w1000: digit(inst._periods[W], 1000),
  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),
  591: 			d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
  592: 			d1000: digit(inst._periods[D], 1000),
  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),
  596: 			h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
  597: 			h1000: digit(inst._periods[H], 1000),
  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),
  601: 			m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
  602: 			m1000: digit(inst._periods[M], 1000),
  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),
  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);
  612: 			var re = new RegExp('\\{' + period + '<\\}([\\s\\S]*)\\{' + period + '>\\}', 'g');
  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.
  625: 	   @param  inst   (object) the current settings for this instance
  626: 	   @param  value  (number) the value to display
  627: 	   @param  len    (number) the minimum length
  628: 	   @return  (string) the display text */
  629: 	_minDigits: function(inst, value, len) {
  630: 		value = '' + value;
  631: 		if (value.length >= len) {
  632: 			return this._translateDigits(inst, value);
  633: 		}
  634: 		value = '0000000000' + value;
  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: 			});
  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) {
  653: 		var format = inst.options.format;
  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
  695: 			var lastNow = plugin._getDaysInMonth(now.getFullYear(), now.getMonth());
  696: 			var lastUntil = plugin._getDaysInMonth(until.getFullYear(), until.getMonth());
  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);
  712: 			var lastDay = plugin._getDaysInMonth(now.getFullYear() + periods[Y],
  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: 
  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;
  778: 	}
  779: 	return $.inArray(command, getters) > -1;
  780: }
  781: 
  782: /* Process the countdown functionality for a jQuery selection.
  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 */
  787: $.fn.countdown = function(options) {
  788: 	var otherArgs = Array.prototype.slice.call(arguments, 1);
  789: 	if (isNotChained(options, otherArgs)) {
  790: 		return plugin['_' + options + 'Plugin'].
  791: 			apply(plugin, [this[0]].concat(otherArgs));
  792: 	}
  793: 	return this.each(function() {
  794: 		if (typeof options == 'string') {
  795: 			if (!plugin['_' + options + 'Plugin']) {
  796: 				throw 'Unknown command: ' + options;
  797: 			}
  798: 			plugin['_' + options + 'Plugin'].
  799: 				apply(plugin, [this].concat(otherArgs));
  800: 		}
  801: 		else {
  802: 			plugin._attachPlugin(this, options || {});
  803: 		}
  804: 	});
  805: };
  806: 
  807: /* Initialise the countdown functionality. */
  808: var plugin = $.countdown = new Countdown(); // Singleton instance
  809: 
  810: })(jQuery);

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