atreides 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. data/LICENSE +1 -1
  2. data/app/assets/javascripts/atreides/admin_base.js.erb +11 -16
  3. data/app/assets/stylesheets/atreides/admin_base.css.erb +2 -3
  4. data/app/controllers/atreides/admin_controller.rb +14 -0
  5. data/app/models/atreides/{preferences.rb → preference.rb} +1 -1
  6. data/app/views/admin/atreides/common/_sidebar_publish.html.haml +16 -0
  7. data/app/views/admin/atreides/features/_form.html.haml +11 -18
  8. data/app/views/admin/atreides/pages/_form.html.haml +1 -4
  9. data/app/views/admin/atreides/posts/_form_sidebar.html.haml +1 -8
  10. data/app/views/admin/common/_list_table.html.haml +30 -0
  11. data/app/views/admin/common/_list_table_row.html.haml +11 -0
  12. data/app/views/admin/common/_pagination.html.haml +1 -0
  13. data/app/views/admin/common/_search_form.html.haml +4 -0
  14. data/lib/assets/flash/atreides/player.swf +0 -0
  15. data/lib/assets/flash/atreides/slideshowpro.swf +0 -0
  16. data/lib/assets/flash/atreides/swfupload.swf +0 -0
  17. data/lib/assets/images/atreides/share/aim_16.png +0 -0
  18. data/lib/assets/images/atreides/share/apple_16.png +0 -0
  19. data/lib/assets/images/atreides/share/bebo_16.png +0 -0
  20. data/lib/assets/images/atreides/share/blogger_16.png +0 -0
  21. data/lib/assets/images/atreides/share/brightkite_16.png +0 -0
  22. data/lib/assets/images/atreides/share/cargo_16.png +0 -0
  23. data/lib/assets/images/atreides/share/delicious_16.png +0 -0
  24. data/lib/assets/images/atreides/share/designfloat_16.png +0 -0
  25. data/lib/assets/images/atreides/share/designmoo_16.png +0 -0
  26. data/lib/assets/images/atreides/share/deviantart_16.png +0 -0
  27. data/lib/assets/images/atreides/share/digg_16.png +0 -0
  28. data/lib/assets/images/atreides/share/digg_alt_16.png +0 -0
  29. data/lib/assets/images/atreides/share/dopplr_16.png +0 -0
  30. data/lib/assets/images/atreides/share/dribbble_16.png +0 -0
  31. data/lib/assets/images/atreides/share/email_16.png +0 -0
  32. data/lib/assets/images/atreides/share/ember_16.png +0 -0
  33. data/lib/assets/images/atreides/share/evernote_16.png +0 -0
  34. data/lib/assets/images/atreides/share/facebook_16.png +0 -0
  35. data/lib/assets/images/atreides/share/flickr_16.png +0 -0
  36. data/lib/assets/images/atreides/share/friendfeed_16.png +0 -0
  37. data/lib/assets/images/atreides/share/gamespot_16.png +0 -0
  38. data/lib/assets/images/atreides/share/google_16.png +0 -0
  39. data/lib/assets/images/atreides/share/google_voice_16.png +0 -0
  40. data/lib/assets/images/atreides/share/google_wave_16.png +0 -0
  41. data/lib/assets/images/atreides/share/googletalk_16.png +0 -0
  42. data/lib/assets/images/atreides/share/gowalla_16.png +0 -0
  43. data/lib/assets/images/atreides/share/grooveshark_16.png +0 -0
  44. data/lib/assets/images/atreides/share/ilike_16.png +0 -0
  45. data/lib/assets/images/atreides/share/komodomedia_azure_16.png +0 -0
  46. data/lib/assets/images/atreides/share/komodomedia_wood_16.png +0 -0
  47. data/lib/assets/images/atreides/share/lastfm_16.png +0 -0
  48. data/lib/assets/images/atreides/share/license.txt +9 -0
  49. data/lib/assets/images/atreides/share/linkedin_16.png +0 -0
  50. data/lib/assets/images/atreides/share/mixx_16.png +0 -0
  51. data/lib/assets/images/atreides/share/mobileme_16.png +0 -0
  52. data/lib/assets/images/atreides/share/mynameise_16.png +0 -0
  53. data/lib/assets/images/atreides/share/myspace_16.png +0 -0
  54. data/lib/assets/images/atreides/share/netvibes_16.png +0 -0
  55. data/lib/assets/images/atreides/share/newsvine_16.png +0 -0
  56. data/lib/assets/images/atreides/share/openid_16.png +0 -0
  57. data/lib/assets/images/atreides/share/orkut_16.png +0 -0
  58. data/lib/assets/images/atreides/share/pandora_16.png +0 -0
  59. data/lib/assets/images/atreides/share/paypal_16.png +0 -0
  60. data/lib/assets/images/atreides/share/picasa_16.png +0 -0
  61. data/lib/assets/images/atreides/share/plurk_16.png +0 -0
  62. data/lib/assets/images/atreides/share/posterous_16.png +0 -0
  63. data/lib/assets/images/atreides/share/qik_16.png +0 -0
  64. data/lib/assets/images/atreides/share/readernaut_16.png +0 -0
  65. data/lib/assets/images/atreides/share/reddit_16.png +0 -0
  66. data/lib/assets/images/atreides/share/roboto_16.png +0 -0
  67. data/lib/assets/images/atreides/share/rss_16.png +0 -0
  68. data/lib/assets/images/atreides/share/sharethis_16.png +0 -0
  69. data/lib/assets/images/atreides/share/skype_16.png +0 -0
  70. data/lib/assets/images/atreides/share/stumbleupon_16.png +0 -0
  71. data/lib/assets/images/atreides/share/technorati_16.png +0 -0
  72. data/lib/assets/images/atreides/share/tumblr_16.png +0 -0
  73. data/lib/assets/images/atreides/share/twitter_16.png +0 -0
  74. data/lib/assets/images/atreides/share/viddler_16.png +0 -0
  75. data/lib/assets/images/atreides/share/vimeo_16.png +0 -0
  76. data/lib/assets/images/atreides/share/virb_16.png +0 -0
  77. data/lib/assets/images/atreides/share/windows_16.png +0 -0
  78. data/lib/assets/images/atreides/share/wordpress_16.png +0 -0
  79. data/lib/assets/images/atreides/share/xing_16.png +0 -0
  80. data/lib/assets/images/atreides/share/yahoo_16.png +0 -0
  81. data/lib/assets/images/atreides/share/yahoobuzz_16.png +0 -0
  82. data/lib/assets/images/atreides/share/yelp_16.png +0 -0
  83. data/lib/assets/images/atreides/share/youtube_16.png +0 -0
  84. data/lib/assets/javascripts/atreides/facebox.js +309 -0
  85. data/lib/assets/javascripts/atreides/fileuploader.js +1250 -0
  86. data/lib/assets/javascripts/atreides/galleria.js +2 -0
  87. data/lib/assets/javascripts/atreides/galleria/src/galleria.js +3816 -0
  88. data/lib/assets/javascripts/atreides/galleria/src/plugins/galleria.flickr.js +221 -0
  89. data/lib/assets/javascripts/atreides/galleria/src/plugins/galleria.history.js +602 -0
  90. data/lib/assets/javascripts/atreides/galleria/src/themes/classic/classic-loader.gif +0 -0
  91. data/lib/assets/javascripts/atreides/galleria/src/themes/classic/classic-map.png +0 -0
  92. data/lib/assets/javascripts/atreides/galleria/src/themes/classic/galleria.classic.css +202 -0
  93. data/lib/assets/javascripts/atreides/galleria/src/themes/classic/galleria.classic.js +92 -0
  94. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/b.png +0 -0
  95. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/down.gif +0 -0
  96. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/fix.gif +0 -0
  97. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/fullscreen-demo.html +34 -0
  98. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/galleria.fullscreen.css +46 -0
  99. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/galleria.fullscreen.js +193 -0
  100. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/i.png +0 -0
  101. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/l.gif +0 -0
  102. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/l2.png +0 -0
  103. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/loader.gif +0 -0
  104. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/n.gif +0 -0
  105. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/p.gif +0 -0
  106. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/r.gif +0 -0
  107. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/r2.png +0 -0
  108. data/lib/assets/javascripts/atreides/galleria/src/themes/fullscreen/up.gif +0 -0
  109. data/lib/assets/javascripts/atreides/jquery.autogrow.js +132 -0
  110. data/lib/assets/javascripts/atreides/jquery.cookie.js +96 -0
  111. data/lib/assets/javascripts/atreides/jquery.countdown.js +759 -0
  112. data/lib/assets/javascripts/atreides/jquery.cycle.js +918 -0
  113. data/lib/assets/javascripts/atreides/jquery.easing.1.3.js +205 -0
  114. data/lib/assets/javascripts/atreides/jquery.flash.js +288 -0
  115. data/lib/assets/javascripts/atreides/jquery.iphone-checkboxes.js +214 -0
  116. data/lib/assets/javascripts/atreides/jquery.jBreadCrumb.1.1.js +240 -0
  117. data/lib/assets/javascripts/atreides/jquery.multipleselectbox-min.js +15 -0
  118. data/lib/assets/javascripts/atreides/jquery.multipleselectbox.js +446 -0
  119. data/lib/assets/javascripts/atreides/jquery.swfupload.js +64 -0
  120. data/lib/assets/javascripts/atreides/jquery.tipsy.js +104 -0
  121. data/lib/assets/javascripts/atreides/jquery.uniform.js +461 -0
  122. data/lib/assets/javascripts/atreides/jtags.js +125 -0
  123. data/lib/assets/javascripts/atreides/raphael-min.js +7 -0
  124. data/lib/assets/javascripts/atreides/raphael.analytics.js +205 -0
  125. data/lib/assets/javascripts/atreides/raphael.path.methods.js +53 -0
  126. data/lib/assets/javascripts/atreides/raphael.pie.js +44 -0
  127. data/lib/assets/javascripts/atreides/sifr.js +18 -0
  128. data/lib/assets/javascripts/atreides/swfobject.js +4 -0
  129. data/lib/assets/javascripts/atreides/swfupload/handlers.js +290 -0
  130. data/lib/assets/javascripts/atreides/swfupload/swfupload.cookies.js +53 -0
  131. data/lib/assets/javascripts/atreides/swfupload/swfupload.js +980 -0
  132. data/lib/assets/javascripts/atreides/swfupload/swfupload.queue.js +98 -0
  133. data/lib/assets/javascripts/atreides/swfupload/swfupload.speed.js +342 -0
  134. data/lib/assets/javascripts/atreides/swfupload/swfupload.swfobject.js +111 -0
  135. data/lib/assets/javascripts/atreides/underscore-1.1.6.js +807 -0
  136. data/lib/assets/stylesheets/atreides/admin_grid.css +258 -0
  137. data/lib/assets/stylesheets/atreides/facebox.css +80 -0
  138. data/lib/assets/stylesheets/atreides/formtastic.css +144 -0
  139. data/lib/assets/stylesheets/atreides/galleria.css +24 -0
  140. data/lib/assets/stylesheets/atreides/ie.css +35 -0
  141. data/lib/assets/stylesheets/atreides/images/bg_fallback.png +0 -0
  142. data/lib/assets/stylesheets/atreides/images/icon_sprite.png +0 -0
  143. data/lib/assets/stylesheets/atreides/images/progress_bar.gif +0 -0
  144. data/lib/assets/stylesheets/atreides/images/slider_handles.png +0 -0
  145. data/lib/assets/stylesheets/atreides/images/ui-icons_222222_256x240.png +0 -0
  146. data/lib/assets/stylesheets/atreides/images/ui-icons_454545_256x240.png +0 -0
  147. data/lib/assets/stylesheets/atreides/jquery-ui.css +738 -0
  148. data/lib/assets/stylesheets/atreides/multipleselectbox.css +33 -0
  149. data/lib/assets/stylesheets/atreides/print.css +29 -0
  150. data/lib/assets/stylesheets/atreides/public_grid.css +258 -0
  151. data/lib/assets/stylesheets/atreides/tipsy.css +7 -0
  152. data/lib/assets/stylesheets/atreides/uniform.default.css +476 -0
  153. data/lib/atreides.rb +4 -0
  154. data/lib/atreides/schema.rb +53 -0
  155. data/lib/atreides/time_zone.rb +6 -0
  156. data/lib/atreides/version.rb +1 -1
  157. data/lib/generators/templates/create_pages.rb +1 -2
  158. data/lib/generators/templates/create_posts.rb +1 -2
  159. metadata +155 -13
  160. data/app/views/admin/common/_list_table.html.erb +0 -37
  161. data/app/views/admin/common/_list_table_row.html.erb +0 -18
  162. data/app/views/admin/common/_pagination.html.erb +0 -1
  163. data/app/views/admin/common/_search_form.html.erb +0 -6
@@ -0,0 +1,132 @@
1
+ /*
2
+ * Auto Expanding Text Area (1.2.2)
3
+ * by Chrys Bader (www.chrysbader.com)
4
+ * chrysb@gmail.com
5
+ *
6
+ * Special thanks to:
7
+ * Jake Chapa - jake@hybridstudio.com
8
+ * John Resig - jeresig@gmail.com
9
+ *
10
+ * Copyright (c) 2008 Chrys Bader (www.chrysbader.com)
11
+ * Licensed under the GPL (GPL-LICENSE.txt) license.
12
+ *
13
+ *
14
+ * NOTE: This script requires jQuery to work. Download jQuery at www.jquery.com
15
+ *
16
+ */
17
+
18
+ (function(jQuery) {
19
+
20
+ var self = null;
21
+
22
+ jQuery.fn.autogrow = function(o)
23
+ {
24
+ return this.each(function() {
25
+ new jQuery.autogrow(this, o);
26
+ });
27
+ };
28
+
29
+
30
+ /**
31
+ * The autogrow object.
32
+ *
33
+ * @constructor
34
+ * @name jQuery.autogrow
35
+ * @param Object e The textarea to create the autogrow for.
36
+ * @param Hash o A set of key/value pairs to set as configuration properties.
37
+ * @cat Plugins/autogrow
38
+ */
39
+
40
+ jQuery.autogrow = function (e, o)
41
+ {
42
+ this.options = o || {};
43
+ this.dummy = null;
44
+ this.interval = null;
45
+ this.line_height = this.options.lineHeight || parseInt(jQuery(e).css('line-height'));
46
+ this.min_height = this.options.minHeight || parseInt(jQuery(e).css('min-height'));
47
+ this.max_height = this.options.maxHeight || parseInt(jQuery(e).css('max-height'));;
48
+ this.textarea = jQuery(e);
49
+
50
+ if(this.line_height == NaN)
51
+ this.line_height = 0;
52
+
53
+ // Only one textarea activated at a time, the one being used
54
+ this.init();
55
+ };
56
+
57
+ jQuery.autogrow.fn = jQuery.autogrow.prototype = {
58
+ autogrow: '1.2.2'
59
+ };
60
+
61
+ jQuery.autogrow.fn.extend = jQuery.autogrow.extend = jQuery.extend;
62
+
63
+ jQuery.autogrow.fn.extend({
64
+
65
+ init: function() {
66
+ var self = this;
67
+ this.textarea.css({overflow: 'hidden', display: 'block'});
68
+ this.textarea.bind('focus', function() { self.startExpand() } ).bind('blur', function() { self.stopExpand() });
69
+ this.checkExpand();
70
+ },
71
+
72
+ startExpand: function() {
73
+ var self = this;
74
+ this.interval = window.setInterval(function() {self.checkExpand()}, 400);
75
+ },
76
+
77
+ stopExpand: function() {
78
+ clearInterval(this.interval);
79
+ },
80
+
81
+ checkExpand: function() {
82
+
83
+ if (this.dummy == null)
84
+ {
85
+ this.dummy = jQuery('<div></div>');
86
+ this.dummy.css({
87
+ 'font-size' : this.textarea.css('font-size'),
88
+ 'font-family': this.textarea.css('font-family'),
89
+ 'width' : this.textarea.css('width'),
90
+ 'padding' : this.textarea.css('padding'),
91
+ 'line-height': this.line_height + 'px',
92
+ 'overflow-x' : 'hidden',
93
+ 'position' : 'absolute',
94
+ 'top' : 0,
95
+ 'left' : -9999
96
+ }).appendTo('body');
97
+ }
98
+
99
+ // Strip HTML tags
100
+ var html = this.textarea.val().replace(/(<|>)/g, '');
101
+
102
+ // IE is different, as per usual
103
+ if ($.browser.msie)
104
+ {
105
+ html = html.replace(/\n/g, '<BR>new');
106
+ }
107
+ else
108
+ {
109
+ html = html.replace(/\n/g, '<br>new');
110
+ }
111
+
112
+ if (this.dummy.html() != html)
113
+ {
114
+ this.dummy.html(html);
115
+
116
+ if (this.max_height > 0 && (this.dummy.height() + this.line_height > this.max_height))
117
+ {
118
+ this.textarea.css('overflow-y', 'auto');
119
+ }
120
+ else
121
+ {
122
+ this.textarea.css('overflow-y', 'hidden');
123
+ if (this.textarea.height() < this.dummy.height() + this.line_height || (this.dummy.height() < this.textarea.height()))
124
+ {
125
+ this.textarea.animate({height: (this.dummy.height() + this.line_height) + 'px'}, 100);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ });
132
+ })(jQuery);
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Cookie plugin
3
+ *
4
+ * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
5
+ * Dual licensed under the MIT and GPL licenses:
6
+ * http://www.opensource.org/licenses/mit-license.php
7
+ * http://www.gnu.org/licenses/gpl.html
8
+ *
9
+ */
10
+
11
+ /**
12
+ * Create a cookie with the given name and value and other optional parameters.
13
+ *
14
+ * @example $.cookie('the_cookie', 'the_value');
15
+ * @desc Set the value of a cookie.
16
+ * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
17
+ * @desc Create a cookie with all available options.
18
+ * @example $.cookie('the_cookie', 'the_value');
19
+ * @desc Create a session cookie.
20
+ * @example $.cookie('the_cookie', null);
21
+ * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
22
+ * used when the cookie was set.
23
+ *
24
+ * @param String name The name of the cookie.
25
+ * @param String value The value of the cookie.
26
+ * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
27
+ * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
28
+ * If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
29
+ * If set to null or omitted, the cookie will be a session cookie and will not be retained
30
+ * when the the browser exits.
31
+ * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
32
+ * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
33
+ * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
34
+ * require a secure protocol (like HTTPS).
35
+ * @type undefined
36
+ *
37
+ * @name $.cookie
38
+ * @cat Plugins/Cookie
39
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
40
+ */
41
+
42
+ /**
43
+ * Get the value of a cookie with the given name.
44
+ *
45
+ * @example $.cookie('the_cookie');
46
+ * @desc Get the value of a cookie.
47
+ *
48
+ * @param String name The name of the cookie.
49
+ * @return The value of the cookie.
50
+ * @type String
51
+ *
52
+ * @name $.cookie
53
+ * @cat Plugins/Cookie
54
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
55
+ */
56
+ jQuery.cookie = function(name, value, options) {
57
+ if (typeof value != 'undefined') { // name and value given, set cookie
58
+ options = options || {};
59
+ if (value === null) {
60
+ value = '';
61
+ options.expires = -1;
62
+ }
63
+ var expires = '';
64
+ if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
65
+ var date;
66
+ if (typeof options.expires == 'number') {
67
+ date = new Date();
68
+ date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
69
+ } else {
70
+ date = options.expires;
71
+ }
72
+ expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
73
+ }
74
+ // CAUTION: Needed to parenthesize options.path and options.domain
75
+ // in the following expressions, otherwise they evaluate to undefined
76
+ // in the packed version for some reason...
77
+ var path = options.path ? '; path=' + (options.path) : '';
78
+ var domain = options.domain ? '; domain=' + (options.domain) : '';
79
+ var secure = options.secure ? '; secure' : '';
80
+ document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
81
+ } else { // only name given, get cookie
82
+ var cookieValue = null;
83
+ if (document.cookie && document.cookie != '') {
84
+ var cookies = document.cookie.split(';');
85
+ for (var i = 0; i < cookies.length; i++) {
86
+ var cookie = jQuery.trim(cookies[i]);
87
+ // Does this cookie string begin with the name we want?
88
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
89
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ return cookieValue;
95
+ }
96
+ };
@@ -0,0 +1,759 @@
1
+ /* http://keith-wood.name/countdown.html
2
+ Countdown for jQuery v1.5.8.
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
+ }
57
+
58
+ var PROP_NAME = 'countdown';
59
+
60
+ var Y = 0; // Years
61
+ var O = 1; // Months
62
+ var W = 2; // Weeks
63
+ var D = 3; // Days
64
+ var H = 4; // Hours
65
+ var M = 5; // Minutes
66
+ var S = 6; // Seconds
67
+
68
+ $.extend(Countdown.prototype, {
69
+ /* Class name added to elements to indicate already configured with countdown. */
70
+ markerClassName: 'hasCountdown',
71
+
72
+ /* Shared timer for all countdowns. */
73
+ _timer: setInterval(function() { $.countdown._updateTargets(); }, 980),
74
+ /* List of currently active countdown targets. */
75
+ _timerTargets: [],
76
+
77
+ /* Override the default settings for all instances of the countdown widget.
78
+ @param options (object) the new settings to use as defaults */
79
+ setDefaults: function(options) {
80
+ this._resetExtraLabels(this._defaults, options);
81
+ extendRemove(this._defaults, options || {});
82
+ },
83
+
84
+ /* Convert a date/time to UTC.
85
+ @param tz (number) the hour or minute offset from GMT, e.g. +9, -360
86
+ @param year (Date) the date/time in that timezone or
87
+ (number) the year in that timezone
88
+ @param month (number, optional) the month (0 - 11) (omit if year is a Date)
89
+ @param day (number, optional) the day (omit if year is a Date)
90
+ @param hours (number, optional) the hour (omit if year is a Date)
91
+ @param mins (number, optional) the minute (omit if year is a Date)
92
+ @param secs (number, optional) the second (omit if year is a Date)
93
+ @param ms (number, optional) the millisecond (omit if year is a Date)
94
+ @return (Date) the equivalent UTC date/time */
95
+ UTCDate: function(tz, year, month, day, hours, mins, secs, ms) {
96
+ if (typeof year == 'object' && year.constructor == Date) {
97
+ ms = year.getMilliseconds();
98
+ secs = year.getSeconds();
99
+ mins = year.getMinutes();
100
+ hours = year.getHours();
101
+ day = year.getDate();
102
+ month = year.getMonth();
103
+ year = year.getFullYear();
104
+ }
105
+ var d = new Date();
106
+ d.setUTCFullYear(year);
107
+ d.setUTCDate(1);
108
+ d.setUTCMonth(month || 0);
109
+ d.setUTCDate(day || 1);
110
+ d.setUTCHours(hours || 0);
111
+ d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz));
112
+ d.setUTCSeconds(secs || 0);
113
+ d.setUTCMilliseconds(ms || 0);
114
+ return d;
115
+ },
116
+
117
+ /* Convert a set of periods into seconds.
118
+ Averaged for months and years.
119
+ @param periods (number[7]) the periods per year/month/week/day/hour/minute/second
120
+ @return (number) the corresponding number of seconds */
121
+ periodsToSeconds: function(periods) {
122
+ return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 +
123
+ periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6];
124
+ },
125
+
126
+ /* Retrieve one or more settings values.
127
+ @param name (string, optional) the name of the setting to retrieve
128
+ or 'all' for all instance settings or omit for all default settings
129
+ @return (any) the requested setting(s) */
130
+ _settingsCountdown: function(target, name) {
131
+ if (!name) {
132
+ return $.countdown._defaults;
133
+ }
134
+ var inst = $.data(target, PROP_NAME);
135
+ return (name == 'all' ? inst.options : inst.options[name]);
136
+ },
137
+
138
+ /* Attach the countdown widget to a div.
139
+ @param target (element) the containing division
140
+ @param options (object) the initial settings for the countdown */
141
+ _attachCountdown: function(target, options) {
142
+ var $target = $(target);
143
+ if ($target.hasClass(this.markerClassName)) {
144
+ return;
145
+ }
146
+ $target.addClass(this.markerClassName);
147
+ var inst = {options: $.extend({}, options),
148
+ _periods: [0, 0, 0, 0, 0, 0, 0]};
149
+ $.data(target, PROP_NAME, inst);
150
+ this._changeCountdown(target);
151
+ },
152
+
153
+ /* Add a target to the list of active ones.
154
+ @param target (element) the countdown target */
155
+ _addTarget: function(target) {
156
+ if (!this._hasTarget(target)) {
157
+ this._timerTargets.push(target);
158
+ }
159
+ },
160
+
161
+ /* See if a target is in the list of active ones.
162
+ @param target (element) the countdown target
163
+ @return (boolean) true if present, false if not */
164
+ _hasTarget: function(target) {
165
+ return ($.inArray(target, this._timerTargets) > -1);
166
+ },
167
+
168
+ /* Remove a target from the list of active ones.
169
+ @param target (element) the countdown target */
170
+ _removeTarget: function(target) {
171
+ this._timerTargets = $.map(this._timerTargets,
172
+ function(value) { return (value == target ? null : value); }); // delete entry
173
+ },
174
+
175
+ /* Update each active timer target. */
176
+ _updateTargets: function() {
177
+ for (var i = this._timerTargets.length - 1; i >= 0; i--) {
178
+ this._updateCountdown(this._timerTargets[i]);
179
+ }
180
+ },
181
+
182
+ /* Redisplay the countdown with an updated display.
183
+ @param target (jQuery) the containing division
184
+ @param inst (object) the current settings for this instance */
185
+ _updateCountdown: function(target, inst) {
186
+ var $target = $(target);
187
+ inst = inst || $.data(target, PROP_NAME);
188
+ if (!inst) {
189
+ return;
190
+ }
191
+ $target.html(this._generateHTML(inst));
192
+ $target[(this._get(inst, 'isRTL') ? 'add' : 'remove') + 'Class']('countdown_rtl');
193
+ var onTick = this._get(inst, 'onTick');
194
+ if (onTick) {
195
+ var periods = inst._hold != 'lap' ? inst._periods :
196
+ this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date());
197
+ var tickInterval = this._get(inst, 'tickInterval');
198
+ if (tickInterval == 1 || this.periodsToSeconds(periods) % tickInterval == 0) {
199
+ onTick.apply(target, [periods]);
200
+ }
201
+ }
202
+ var expired = inst._hold != 'pause' &&
203
+ (inst._since ? inst._now.getTime() < inst._since.getTime() :
204
+ inst._now.getTime() >= inst._until.getTime());
205
+ if (expired && !inst._expiring) {
206
+ inst._expiring = true;
207
+ if (this._hasTarget(target) || this._get(inst, 'alwaysExpire')) {
208
+ this._removeTarget(target);
209
+ var onExpiry = this._get(inst, 'onExpiry');
210
+ if (onExpiry) {
211
+ onExpiry.apply(target, []);
212
+ }
213
+ var expiryText = this._get(inst, 'expiryText');
214
+ if (expiryText) {
215
+ var layout = this._get(inst, 'layout');
216
+ inst.options.layout = expiryText;
217
+ this._updateCountdown(target, inst);
218
+ inst.options.layout = layout;
219
+ }
220
+ var expiryUrl = this._get(inst, 'expiryUrl');
221
+ if (expiryUrl) {
222
+ window.location = expiryUrl;
223
+ }
224
+ }
225
+ inst._expiring = false;
226
+ }
227
+ else if (inst._hold == 'pause') {
228
+ this._removeTarget(target);
229
+ }
230
+ $.data(target, PROP_NAME, inst);
231
+ },
232
+
233
+ /* Reconfigure the settings for a countdown div.
234
+ @param target (element) the containing division
235
+ @param options (object) the new settings for the countdown or
236
+ (string) an individual property name
237
+ @param value (any) the individual property value
238
+ (omit if options is an object) */
239
+ _changeCountdown: function(target, options, value) {
240
+ options = options || {};
241
+ if (typeof options == 'string') {
242
+ var name = options;
243
+ options = {};
244
+ options[name] = value;
245
+ }
246
+ var inst = $.data(target, PROP_NAME);
247
+ if (inst) {
248
+ this._resetExtraLabels(inst.options, options);
249
+ extendRemove(inst.options, options);
250
+ this._adjustSettings(target, inst);
251
+ $.data(target, PROP_NAME, inst);
252
+ var now = new Date();
253
+ if ((inst._since && inst._since < now) ||
254
+ (inst._until && inst._until > now)) {
255
+ this._addTarget(target);
256
+ }
257
+ this._updateCountdown(target, inst);
258
+ }
259
+ },
260
+
261
+ /* Reset any extra labelsn and compactLabelsn entries if changing labels.
262
+ @param base (object) the options to be updated
263
+ @param options (object) the new option values */
264
+ _resetExtraLabels: function(base, options) {
265
+ var changingLabels = false;
266
+ for (var n in options) {
267
+ if (n != 'whichLabels' && n.match(/[Ll]abels/)) {
268
+ changingLabels = true;
269
+ break;
270
+ }
271
+ }
272
+ if (changingLabels) {
273
+ for (var n in base) { // Remove custom numbered labels
274
+ if (n.match(/[Ll]abels[0-9]/)) {
275
+ base[n] = null;
276
+ }
277
+ }
278
+ }
279
+ },
280
+
281
+ /* Calculate interal settings for an instance.
282
+ @param target (element) the containing division
283
+ @param inst (object) the current settings for this instance */
284
+ _adjustSettings: function(target, inst) {
285
+ var now;
286
+ var serverSync = this._get(inst, 'serverSync');
287
+ var serverOffset = 0;
288
+ var serverEntry = null;
289
+ for (var i = 0; i < this._serverSyncs.length; i++) {
290
+ if (this._serverSyncs[i][0] == serverSync) {
291
+ serverEntry = this._serverSyncs[i][1];
292
+ break;
293
+ }
294
+ }
295
+ if (serverEntry != null) {
296
+ serverOffset = (serverSync ? serverEntry : 0);
297
+ now = new Date();
298
+ }
299
+ else {
300
+ var serverResult = (serverSync ? serverSync.apply(target, []) : null);
301
+ now = new Date();
302
+ serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
303
+ this._serverSyncs.push([serverSync, serverOffset]);
304
+ }
305
+ var timezone = this._get(inst, 'timezone');
306
+ timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
307
+ inst._since = this._get(inst, 'since');
308
+ if (inst._since != null) {
309
+ inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null));
310
+ if (inst._since && serverOffset) {
311
+ inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset);
312
+ }
313
+ }
314
+ inst._until = this.UTCDate(timezone, this._determineTime(this._get(inst, 'until'), now));
315
+ if (serverOffset) {
316
+ inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset);
317
+ }
318
+ inst._show = this._determineShow(inst);
319
+ },
320
+
321
+ /* Remove the countdown widget from a div.
322
+ @param target (element) the containing division */
323
+ _destroyCountdown: function(target) {
324
+ var $target = $(target);
325
+ if (!$target.hasClass(this.markerClassName)) {
326
+ return;
327
+ }
328
+ this._removeTarget(target);
329
+ $target.removeClass(this.markerClassName).empty();
330
+ $.removeData(target, PROP_NAME);
331
+ },
332
+
333
+ /* Pause a countdown widget at the current time.
334
+ Stop it running but remember and display the current time.
335
+ @param target (element) the containing division */
336
+ _pauseCountdown: function(target) {
337
+ this._hold(target, 'pause');
338
+ },
339
+
340
+ /* Pause a countdown widget at the current time.
341
+ Stop the display but keep the countdown running.
342
+ @param target (element) the containing division */
343
+ _lapCountdown: function(target) {
344
+ this._hold(target, 'lap');
345
+ },
346
+
347
+ /* Resume a paused countdown widget.
348
+ @param target (element) the containing division */
349
+ _resumeCountdown: function(target) {
350
+ this._hold(target, null);
351
+ },
352
+
353
+ /* Pause or resume a countdown widget.
354
+ @param target (element) the containing division
355
+ @param hold (string) the new hold setting */
356
+ _hold: function(target, hold) {
357
+ var inst = $.data(target, PROP_NAME);
358
+ if (inst) {
359
+ if (inst._hold == 'pause' && !hold) {
360
+ inst._periods = inst._savePeriods;
361
+ var sign = (inst._since ? '-' : '+');
362
+ inst[inst._since ? '_since' : '_until'] =
363
+ this._determineTime(sign + inst._periods[0] + 'y' +
364
+ sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' +
365
+ sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' +
366
+ sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's');
367
+ this._addTarget(target);
368
+ }
369
+ inst._hold = hold;
370
+ inst._savePeriods = (hold == 'pause' ? inst._periods : null);
371
+ $.data(target, PROP_NAME, inst);
372
+ this._updateCountdown(target, inst);
373
+ }
374
+ },
375
+
376
+ /* Return the current time periods.
377
+ @param target (element) the containing division
378
+ @return (number[7]) the current periods for the countdown */
379
+ _getTimesCountdown: function(target) {
380
+ var inst = $.data(target, PROP_NAME);
381
+ return (!inst ? null : (!inst._hold ? inst._periods :
382
+ this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date())));
383
+ },
384
+
385
+ /* Get a setting value, defaulting if necessary.
386
+ @param inst (object) the current settings for this instance
387
+ @param name (string) the name of the required setting
388
+ @return (any) the setting's value or a default if not overridden */
389
+ _get: function(inst, name) {
390
+ return (inst.options[name] != null ?
391
+ inst.options[name] : $.countdown._defaults[name]);
392
+ },
393
+
394
+ /* A time may be specified as an exact value or a relative one.
395
+ @param setting (string or number or Date) - the date/time value
396
+ as a relative or absolute value
397
+ @param defaultTime (Date) the date/time to use if no other is supplied
398
+ @return (Date) the corresponding date/time */
399
+ _determineTime: function(setting, defaultTime) {
400
+ var offsetNumeric = function(offset) { // e.g. +300, -2
401
+ var time = new Date();
402
+ time.setTime(time.getTime() + offset * 1000);
403
+ return time;
404
+ };
405
+ var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m'
406
+ offset = offset.toLowerCase();
407
+ var time = new Date();
408
+ var year = time.getFullYear();
409
+ var month = time.getMonth();
410
+ var day = time.getDate();
411
+ var hour = time.getHours();
412
+ var minute = time.getMinutes();
413
+ var second = time.getSeconds();
414
+ var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g;
415
+ var matches = pattern.exec(offset);
416
+ while (matches) {
417
+ switch (matches[2] || 's') {
418
+ case 's': second += parseInt(matches[1], 10); break;
419
+ case 'm': minute += parseInt(matches[1], 10); break;
420
+ case 'h': hour += parseInt(matches[1], 10); break;
421
+ case 'd': day += parseInt(matches[1], 10); break;
422
+ case 'w': day += parseInt(matches[1], 10) * 7; break;
423
+ case 'o':
424
+ month += parseInt(matches[1], 10);
425
+ day = Math.min(day, $.countdown._getDaysInMonth(year, month));
426
+ break;
427
+ case 'y':
428
+ year += parseInt(matches[1], 10);
429
+ day = Math.min(day, $.countdown._getDaysInMonth(year, month));
430
+ break;
431
+ }
432
+ matches = pattern.exec(offset);
433
+ }
434
+ return new Date(year, month, day, hour, minute, second, 0);
435
+ };
436
+ var time = (setting == null ? defaultTime :
437
+ (typeof setting == 'string' ? offsetString(setting) :
438
+ (typeof setting == 'number' ? offsetNumeric(setting) : setting)));
439
+ if (time) time.setMilliseconds(0);
440
+ return time;
441
+ },
442
+
443
+ /* Determine the number of days in a month.
444
+ @param year (number) the year
445
+ @param month (number) the month
446
+ @return (number) the days in that month */
447
+ _getDaysInMonth: function(year, month) {
448
+ return 32 - new Date(year, month, 32).getDate();
449
+ },
450
+
451
+ /* Determine which set of labels should be used for an amount.
452
+ @param num (number) the amount to be displayed
453
+ @return (number) the set of labels to be used for this amount */
454
+ _normalLabels: function(num) {
455
+ return num;
456
+ },
457
+
458
+ /* Generate the HTML to display the countdown widget.
459
+ @param inst (object) the current settings for this instance
460
+ @return (string) the new HTML for the countdown display */
461
+ _generateHTML: function(inst) {
462
+ // Determine what to show
463
+ var significant = this._get(inst, 'significant');
464
+ inst._periods = (inst._hold ? inst._periods :
465
+ this._calculatePeriods(inst, inst._show, significant, new Date()));
466
+ // Show all 'asNeeded' after first non-zero value
467
+ var shownNonZero = false;
468
+ var showCount = 0;
469
+ var sigCount = significant;
470
+ var show = $.extend({}, inst._show);
471
+ for (var period = Y; period <= S; period++) {
472
+ shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0);
473
+ show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]);
474
+ showCount += (show[period] ? 1 : 0);
475
+ sigCount -= (inst._periods[period] > 0 ? 1 : 0);
476
+ }
477
+ var showSignificant = [false, false, false, false, false, false, false];
478
+ for (var period = S; period >= Y; period--) { // Determine significant periods
479
+ if (inst._show[period]) {
480
+ if (inst._periods[period]) {
481
+ showSignificant[period] = true;
482
+ }
483
+ else {
484
+ showSignificant[period] = sigCount > 0;
485
+ sigCount--;
486
+ }
487
+ }
488
+ }
489
+ var compact = this._get(inst, 'compact');
490
+ var layout = this._get(inst, 'layout');
491
+ var labels = (compact ? this._get(inst, 'compactLabels') : this._get(inst, 'labels'));
492
+ var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
493
+ var timeSeparator = this._get(inst, 'timeSeparator');
494
+ var description = this._get(inst, 'description') || '';
495
+ var showCompact = function(period) {
496
+ var labelsNum = $.countdown._get(inst,
497
+ 'compactLabels' + whichLabels(inst._periods[period]));
498
+ return (show[period] ? inst._periods[period] +
499
+ (labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
500
+ };
501
+ var showFull = function(period) {
502
+ var labelsNum = $.countdown._get(inst, 'labels' + whichLabels(inst._periods[period]));
503
+ return ((!significant && show[period]) || (significant && showSignificant[period]) ?
504
+ '<span class="countdown_section"><span class="countdown_amount">' +
505
+ inst._periods[period] + '</span><br/>' +
506
+ (labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
507
+ };
508
+ return (layout ? this._buildLayout(inst, show, layout, compact, significant, showSignificant) :
509
+ ((compact ? // Compact version
510
+ '<span class="countdown_row countdown_amount' +
511
+ (inst._hold ? ' countdown_holding' : '') + '">' +
512
+ showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) +
513
+ (show[H] ? this._minDigits(inst._periods[H], 2) : '') +
514
+ (show[M] ? (show[H] ? timeSeparator : '') +
515
+ this._minDigits(inst._periods[M], 2) : '') +
516
+ (show[S] ? (show[H] || show[M] ? timeSeparator : '') +
517
+ this._minDigits(inst._periods[S], 2) : '') :
518
+ // Full version
519
+ '<span class="countdown_row countdown_show' + (significant || showCount) +
520
+ (inst._hold ? ' countdown_holding' : '') + '">' +
521
+ showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
522
+ showFull(H) + showFull(M) + showFull(S)) + '</span>' +
523
+ (description ? '<span class="countdown_row countdown_descr">' + description + '</span>' : '')));
524
+ },
525
+
526
+ /* Construct a custom layout.
527
+ @param inst (object) the current settings for this instance
528
+ @param show (string[7]) flags indicating which periods are requested
529
+ @param layout (string) the customised layout
530
+ @param compact (boolean) true if using compact labels
531
+ @param significant (number) the number of periods with values to show, zero for all
532
+ @param showSignificant (boolean[7]) other periods to show for significance
533
+ @return (string) the custom HTML */
534
+ _buildLayout: function(inst, show, layout, compact, significant, showSignificant) {
535
+ var labels = this._get(inst, (compact ? 'compactLabels' : 'labels'));
536
+ var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
537
+ var labelFor = function(index) {
538
+ return ($.countdown._get(inst,
539
+ (compact ? 'compactLabels' : 'labels') + whichLabels(inst._periods[index])) ||
540
+ labels)[index];
541
+ };
542
+ var digit = function(value, position) {
543
+ return Math.floor(value / position) % 10;
544
+ };
545
+ var subs = {desc: this._get(inst, 'description'), sep: this._get(inst, 'timeSeparator'),
546
+ yl: labelFor(Y), yn: inst._periods[Y], ynn: this._minDigits(inst._periods[Y], 2),
547
+ ynnn: this._minDigits(inst._periods[Y], 3), y1: digit(inst._periods[Y], 1),
548
+ y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
549
+ y1000: digit(inst._periods[Y], 1000),
550
+ ol: labelFor(O), on: inst._periods[O], onn: this._minDigits(inst._periods[O], 2),
551
+ onnn: this._minDigits(inst._periods[O], 3), o1: digit(inst._periods[O], 1),
552
+ o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
553
+ o1000: digit(inst._periods[O], 1000),
554
+ wl: labelFor(W), wn: inst._periods[W], wnn: this._minDigits(inst._periods[W], 2),
555
+ wnnn: this._minDigits(inst._periods[W], 3), w1: digit(inst._periods[W], 1),
556
+ w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
557
+ w1000: digit(inst._periods[W], 1000),
558
+ dl: labelFor(D), dn: inst._periods[D], dnn: this._minDigits(inst._periods[D], 2),
559
+ dnnn: this._minDigits(inst._periods[D], 3), d1: digit(inst._periods[D], 1),
560
+ d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
561
+ d1000: digit(inst._periods[D], 1000),
562
+ hl: labelFor(H), hn: inst._periods[H], hnn: this._minDigits(inst._periods[H], 2),
563
+ hnnn: this._minDigits(inst._periods[H], 3), h1: digit(inst._periods[H], 1),
564
+ h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
565
+ h1000: digit(inst._periods[H], 1000),
566
+ ml: labelFor(M), mn: inst._periods[M], mnn: this._minDigits(inst._periods[M], 2),
567
+ mnnn: this._minDigits(inst._periods[M], 3), m1: digit(inst._periods[M], 1),
568
+ m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
569
+ m1000: digit(inst._periods[M], 1000),
570
+ sl: labelFor(S), sn: inst._periods[S], snn: this._minDigits(inst._periods[S], 2),
571
+ snnn: this._minDigits(inst._periods[S], 3), s1: digit(inst._periods[S], 1),
572
+ s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100),
573
+ s1000: digit(inst._periods[S], 1000)};
574
+ var html = layout;
575
+ // Replace period containers: {p<}...{p>}
576
+ for (var i = Y; i <= S; i++) {
577
+ var period = 'yowdhms'.charAt(i);
578
+ var re = new RegExp('\\{' + period + '<\\}(.*)\\{' + period + '>\\}', 'g');
579
+ html = html.replace(re, ((!significant && show[i]) ||
580
+ (significant && showSignificant[i]) ? '$1' : ''));
581
+ }
582
+ // Replace period values: {pn}
583
+ $.each(subs, function(n, v) {
584
+ var re = new RegExp('\\{' + n + '\\}', 'g');
585
+ html = html.replace(re, v);
586
+ });
587
+ return html;
588
+ },
589
+
590
+ /* Ensure a numeric value has at least n digits for display.
591
+ @param value (number) the value to display
592
+ @param len (number) the minimum length
593
+ @return (string) the display text */
594
+ _minDigits: function(value, len) {
595
+ value = '' + value;
596
+ if (value.length >= len) {
597
+ return value;
598
+ }
599
+ value = '0000000000' + value;
600
+ return value.substr(value.length - len);
601
+ },
602
+
603
+ /* Translate the format into flags for each period.
604
+ @param inst (object) the current settings for this instance
605
+ @return (string[7]) flags indicating which periods are requested (?) or
606
+ required (!) by year, month, week, day, hour, minute, second */
607
+ _determineShow: function(inst) {
608
+ var format = this._get(inst, 'format');
609
+ var show = [];
610
+ show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null));
611
+ show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null));
612
+ show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null));
613
+ show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null));
614
+ show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null));
615
+ show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null));
616
+ show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null));
617
+ return show;
618
+ },
619
+
620
+ /* Calculate the requested periods between now and the target time.
621
+ @param inst (object) the current settings for this instance
622
+ @param show (string[7]) flags indicating which periods are requested/required
623
+ @param significant (number) the number of periods with values to show, zero for all
624
+ @param now (Date) the current date and time
625
+ @return (number[7]) the current time periods (always positive)
626
+ by year, month, week, day, hour, minute, second */
627
+ _calculatePeriods: function(inst, show, significant, now) {
628
+ // Find endpoints
629
+ inst._now = now;
630
+ inst._now.setMilliseconds(0);
631
+ var until = new Date(inst._now.getTime());
632
+ if (inst._since) {
633
+ if (now.getTime() < inst._since.getTime()) {
634
+ inst._now = now = until;
635
+ }
636
+ else {
637
+ now = inst._since;
638
+ }
639
+ }
640
+ else {
641
+ until.setTime(inst._until.getTime());
642
+ if (now.getTime() > inst._until.getTime()) {
643
+ inst._now = now = until;
644
+ }
645
+ }
646
+ // Calculate differences by period
647
+ var periods = [0, 0, 0, 0, 0, 0, 0];
648
+ if (show[Y] || show[O]) {
649
+ // Treat end of months as the same
650
+ var lastNow = $.countdown._getDaysInMonth(now.getFullYear(), now.getMonth());
651
+ var lastUntil = $.countdown._getDaysInMonth(until.getFullYear(), until.getMonth());
652
+ var sameDay = (until.getDate() == now.getDate() ||
653
+ (until.getDate() >= Math.min(lastNow, lastUntil) &&
654
+ now.getDate() >= Math.min(lastNow, lastUntil)));
655
+ var getSecs = function(date) {
656
+ return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds();
657
+ };
658
+ var months = Math.max(0,
659
+ (until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() +
660
+ ((until.getDate() < now.getDate() && !sameDay) ||
661
+ (sameDay && getSecs(until) < getSecs(now)) ? -1 : 0));
662
+ periods[Y] = (show[Y] ? Math.floor(months / 12) : 0);
663
+ periods[O] = (show[O] ? months - periods[Y] * 12 : 0);
664
+ // Adjust for months difference and end of month if necessary
665
+ now = new Date(now.getTime());
666
+ var wasLastDay = (now.getDate() == lastNow);
667
+ var lastDay = $.countdown._getDaysInMonth(now.getFullYear() + periods[Y],
668
+ now.getMonth() + periods[O]);
669
+ if (now.getDate() > lastDay) {
670
+ now.setDate(lastDay);
671
+ }
672
+ now.setFullYear(now.getFullYear() + periods[Y]);
673
+ now.setMonth(now.getMonth() + periods[O]);
674
+ if (wasLastDay) {
675
+ now.setDate(lastDay);
676
+ }
677
+ }
678
+ var diff = Math.floor((until.getTime() - now.getTime()) / 1000);
679
+ var extractPeriod = function(period, numSecs) {
680
+ periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0);
681
+ diff -= periods[period] * numSecs;
682
+ };
683
+ extractPeriod(W, 604800);
684
+ extractPeriod(D, 86400);
685
+ extractPeriod(H, 3600);
686
+ extractPeriod(M, 60);
687
+ extractPeriod(S, 1);
688
+ if (diff > 0 && !inst._since) { // Round up if left overs
689
+ var multiplier = [1, 12, 4.3482, 7, 24, 60, 60];
690
+ var lastShown = S;
691
+ var max = 1;
692
+ for (var period = S; period >= Y; period--) {
693
+ if (show[period]) {
694
+ if (periods[lastShown] >= max) {
695
+ periods[lastShown] = 0;
696
+ diff = 1;
697
+ }
698
+ if (diff > 0) {
699
+ periods[period]++;
700
+ diff = 0;
701
+ lastShown = period;
702
+ max = 1;
703
+ }
704
+ }
705
+ max *= multiplier[period];
706
+ }
707
+ }
708
+ if (significant) { // Zero out insignificant periods
709
+ for (var period = Y; period <= S; period++) {
710
+ if (significant && periods[period]) {
711
+ significant--;
712
+ }
713
+ else if (!significant) {
714
+ periods[period] = 0;
715
+ }
716
+ }
717
+ }
718
+ return periods;
719
+ }
720
+ });
721
+
722
+ /* jQuery extend now ignores nulls!
723
+ @param target (object) the object to update
724
+ @param props (object) the new settings
725
+ @return (object) the updated object */
726
+ function extendRemove(target, props) {
727
+ $.extend(target, props);
728
+ for (var name in props) {
729
+ if (props[name] == null) {
730
+ target[name] = null;
731
+ }
732
+ }
733
+ return target;
734
+ }
735
+
736
+ /* Process the countdown functionality for a jQuery selection.
737
+ @param command (string) the command to run (optional, default 'attach')
738
+ @param options (object) the new settings to use for these countdown instances
739
+ @return (jQuery) for chaining further calls */
740
+ $.fn.countdown = function(options) {
741
+ var otherArgs = Array.prototype.slice.call(arguments, 1);
742
+ if (options == 'getTimes' || options == 'settings') {
743
+ return $.countdown['_' + options + 'Countdown'].
744
+ apply($.countdown, [this[0]].concat(otherArgs));
745
+ }
746
+ return this.each(function() {
747
+ if (typeof options == 'string') {
748
+ $.countdown['_' + options + 'Countdown'].apply($.countdown, [this].concat(otherArgs));
749
+ }
750
+ else {
751
+ $.countdown._attachCountdown(this, options);
752
+ }
753
+ });
754
+ };
755
+
756
+ /* Initialise the countdown functionality. */
757
+ $.countdown = new Countdown(); // singleton instance
758
+
759
+ })(jQuery);