PageSourceSearch

https://craq.club/modules/mod_smartcountdown3/js/smartcountdown.js

js craq.club collected 2026-10-10 00:12:52 UTC 70,258 bytes, 1,858 lines download raw bytes

1/**
2 * @package Module Smart Countdown 3 for Joomla! 3.0
3 * @version 3.4.5
4 * @author Alex Polonski
5 * @copyright (C) 2012-2015 - Alex Polonski
6 * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
7 **/
8(function($) {
9	var MILLIS_IN_DAY = 86400000;
10	var MILLIS_IN_HOUR = 3600000;
11	var MILLIS_IN_MINUTE = 60000;
12	var MILLIS_IN_SECOND = 1000;
13	
14	var SECONDS_IN_DAY = 86400;
15	var SECONDS_IN_HOUR = 3600;
16	var SECONDS_IN_MINUTE = 60;
17	var MINUTES_IN_DAY = SECONDS_IN_DAY / 60;
18	var MINUTES_IN_HOUR = 60;
19	var HOURS_IN_DAY = 24;
20	var MONTHS_IN_YEAR = 12;
21	
22	var MIN_SUSPEND_THRESHOLD = 50; // standard threshold
23	var SUSPEND_THRESHOLD_RELAX_STEP = 100; // temporarly increment threshold by this value
24	var SUSPEND_THRESHOLD_RESTRICT_STEP = 50; // gradually decrease threshold by this value
25
26	var WINDOW_RESIZE_EVENT_DELAY = 500; // avoid massive resize events
27	var SCD_SAFE_ADJUST_PX = 10; // responsive adjust safe margin - make required width bigger
28	
29	// global container for smart countdown objects
30	scds_container = {
31		timer : {
32			id : false,
33			now : false,
34			offset : false,
35			awake_detect : false,
36			suspend_threshold : MIN_SUSPEND_THRESHOLD,
37			acc_correction : 0
38		},
39		instances : {},
40		add : function(options) {
41			// protection against adding the same instance more than once
42			if(typeof scds_container.instances[options.id] !== 'undefined') {
43				return;
44			}
45			// scd_counter is a generic object. We have to use a fresh copy
46			// each time we add a counter, so that scd_counter instance is
47			// always intact.
48			var working_copy = $.extend(true, {}, scd_counter);
49			
50			// call init method. The recently created counter will
51			// be added to scds_container after it's setup is complete
52			working_copy.init(options);
53			
54			// create the tick timer if not created yet
55			if(this.timer.id === false) {
56				this.timer.id = window.setTimeout(function() {
57					scds_container.fireAllCounters();
58				}, MILLIS_IN_SECOND);
59				
60				// avoid massive resize events
61				$(window).resize(function() {
62					clearTimeout($.data(this, 'resizeTimer'));
63					$.data(this, 'resizeTimer', setTimeout(function() {
64						scds_container.responsiveAdjust();
65					}, WINDOW_RESIZE_EVENT_DELAY));
66				});
67				// on the first run (timer hasn't been created yet) we
68				// add some easings to support existing animations
69				this.setupVelocityEasings();
70			}
71		},
72		remove : function(id) {
73			delete(scds_container.instances[id]);
74		},
75		updateInstance : function(id, instance) {
76			scds_container.instances[id] = instance;
77			scds_container.responsiveAdjust();
78		},
79		fireAllCounters : function() {
80			var now = new Date().getTime();
81			if(this.timer.awake_detect === false) {
82				this.timer.awake_detect = now - MILLIS_IN_SECOND;
83			}
84			var elapsed = now - this.timer.awake_detect;
85			this.timer.awake_detect = now;
86			
87			var bias = elapsed - MILLIS_IN_SECOND, timeout = MILLIS_IN_SECOND;
88			if(Math.abs(bias) < 20) {
89				// we can correct small timer fluctuations simply
90				// adjusting next timeout
91				timeout = MILLIS_IN_SECOND - bias;
92				this.timer.acc_correction -= bias;
93			}
94			// programm next tick right away
95			this.timer.id = window.setTimeout(function() {
96				scds_container.fireAllCounters();
97			}, timeout)
98			
99			// keep track of accumulated correction
100			this.timer.acc_correction += (elapsed - MILLIS_IN_SECOND);
101
102			var correction = 0;
103			if(this.timer.acc_correction >= this.timer.suspend_threshold) {
104				correction = this.timer.acc_correction;
105				this.timer.acc_correction = 0;
106				this.timer.suspend_threshold += SUSPEND_THRESHOLD_RELAX_STEP;
107			
108				// we are in suspend/resume correction and have to refresh current
109				// system time stored in this.timer object
110				this.getServerTime(true);
111			} else if(this.timer.suspend_threshold > MIN_SUSPEND_THRESHOLD) {
112				this.timer.suspend_threshold -= SUSPEND_THRESHOLD_RESTRICT_STEP;
113			}
114			
115			// update internal server now each tick
116			this.timer.now += MILLIS_IN_SECOND;
117			
118			$.each(this.instances, function() {
119				this.tick(true, correction);
120			});
121		},
122		setServerTime : function(ts) {
123			if(this.timer.offset === false) {
124				this.timer.offset = ts - new Date().getTime();
125				
126				// set internal now on init, later it will be updated on each
127				// timer tick, but we have to make it available before the
128				// timer is activated
129				this.timer.now = ts;
130			}
131		},
132		getServerTime : function(renew) {
133			if(renew) {
134				this.timer.now = new Date().getTime() + this.timer.offset;
135			}
136			return this.timer.now;
137		},
138		responsiveAdjust : function() {
139			$.each(this.instances, function(id, counter) {
140				/*var width = window.innerWidth
141					|| document.documentElement.clientWidth
142					|| document.body.clientWidth;*/
143				counter.responsiveAdjust(/*width*/);
144			});
145		},
146		setupVelocityEasings : function() {
147			var VelocityContainer = window.Velocity || $.Velocity;
148
149			$.extend(VelocityContainer.Easings, {
150			    Back: function( p ) {
151			        return p * p * ( 3 * p - 2 );
152			    },
153			    Bounce: function ( p ) {
154			        var pow2,
155			            bounce = 4;
156
157			        while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
158			        return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
159			    }
160			});
161			
162			$.each([ "Back", "Bounce" ], function( index, easeInName ) {
163
164			    var easeIn = VelocityContainer.Easings[easeInName];
165
166			    VelocityContainer.Easings[ "easeIn" + easeInName ] = easeIn;
167			    VelocityContainer.Easings[ "easeOut" + easeInName ] = function( p ) {
168			        return 1 - easeIn( 1 - p );
169			    };
170			    VelocityContainer.Easings[ "easeInOut" + easeInName ] = function( p ) {
171			        return p < 0.5 ?
172			            easeIn( p * 2 ) / 2 :
173			            1 - easeIn( p * -2 + 2 ) / 2;
174			    };
175			});
176		}
177	}
178	
179	var scd_counter = {
180		options : {
181			units : {
182				years : 1,
183				months : 1,
184				weeks : 1,
185				days : 1,
186				hours : 1,
187				minutes : 1,
188				seconds : 1
189			},
190			hide_lower_units : [],
191			limits : {
192				// default overflow limits for up mode. If some of related time units
193				// are not displayed these limits will be updated in getCounterValues()
194				seconds : SECONDS_IN_HOUR,
195				minutes : MINUTES_IN_HOUR,
196				hours : HOURS_IN_DAY
197			},
198			paddings : {
199				years : 1,
200				months : 1,
201				weeks : 1,
202				days : 2,
203				hours : 2,
204				minutes : 2,
205				seconds : 2
206			},
207			animations : {},
208			labels_vert_align : 'middle',
209			initDisplay : true,
210			allow_all_zeros : 0,
211			replace_lowest_zero : 1,
212			hide_highest_zeros : 0,
213			hide_countup_counter : 0,
214			shortcode : 0,
215			redirect_url : '',
216			click_url : '',
217			import_config : '',
218			base_font_size : 12,
219			counter_container : null,
220			
221			countup_limit : -1,
222			countdown_limit : -1,
223			countdown_query_limit : -1
224		},
225		current_values : {},
226		elements : {},
227		animation_timers : {},
228		
229		init : function(options) {
230			$.extend(true, this.options, options);
231			
232			// backup original event titles - we'll need them later
233			// for appending imported event titles
234			this.options.original_title_before_down = this.options.title_before_down;
235			this.options.original_title_before_up = this.options.title_before_up;
236			
237			this.options.original_title_after_down = this.options.title_after_down;
238			this.options.original_title_after_up = this.options.title_after_up;
239			
240			// backup countdown limit. We will need these
241			// values when requesting next event. limits will change
242			// during counter life to indicate the next query interval
243			this.options.original_countdown_limit = this.options.countdown_limit;
244			
245			// here we cannot rely on module container div class - different templates use different markup
246			// $$$ we should add an advanced option - "container class". For now, use some popular
247			// classes and fall back to our module content wrapper if outer container is not found.
248			this.options.counter_container = $('#' + this.options.id).closest('.moduletable, .module-inner');
249			if(this.options.counter_container.length == 0) {
250				this.options.counter_container = $('#' + this.options.id);
251			}
252			
253			this.queryNextEvent(true);
254		},
255		tick : function(from_timer, correction) {
256			var delta = this.options.mode == 'up' ? 1 : -1;
257			
258			if(typeof correction !== 'undefined' && correction != 0) {
259				var diff = this.diff + (correction + MILLIS_IN_SECOND ) * delta;
260				if(this.options.mode == 'down' && diff <= 0) {
261					// deadline reached while suspended
262					this.deadlineReached();
263				} else {
264					// recalculate counter values
265					// when browser is resumed, values queue can contain
266					// values which are not sequential. We pass "resumed"
267					// parameter here (reserved)
268					this.softInitCounter(true);
269				}
270				// on resume we do not set initDisplay because it incurres a significant
271				// workload on script (units visibility checks, etc.)
272				return;
273			}
274			// check for counter mode limits every tick - we do it before incrementing
275			// diff, so we react to countup limit reached on time
276			this.applyCounterLimits();
277			
278			// advance current diff
279			if(from_timer) {
280				this.diff += delta * MILLIS_IN_SECOND;
281			}
282			
283			// copy current values to new_values
284			var new_values = $.extend({},this.current_values);
285			
286			// always advance seconds, even if they are not displayed
287			new_values.seconds += delta;
288			
289			// update time units on seconds limit according to counter display
290			// settings
291			
292			// 'up' mode
293			if(new_values.seconds >= this.options.limits.seconds) {
294				// if seconds value reaches 24h threshhold we always recalculate
295				// counter values
296				if(new_values.seconds >= SECONDS_IN_DAY) {
297					this.softInitCounter();
298					return;
299				}
300				// normal overflow, increment next higher displayed unit
301				new_values.seconds = 0;
302				if(this.options.units.minutes == 1) {
303					// minutes displayed
304					new_values.minutes++;
305					if(new_values.minutes >= this.options.limits.minutes) {
306						new_values.minutes = 0;
307						if(this.options.units.hours == 1) {
308							// minutes and hours displayed
309							new_values.hours++;
310							if(new_values.hours >= this.options.limits.hours) {
311								this.softInitCounter();
312								return;
313							}
314						} else {
315							// hours are not displayed but minutes have reached the
316							// MINUTES_IN_DAY limit, recalculate counter values
317							this.softInitCounter();
318							return;
319						}
320					}
321				} else if(this.options.units.hours == 1) {
322					// only hours are displayed
323					new_values.hours++;
324					if(new_values.hours >= this.options.limits.hours) {
325						this.softInitCounter();
326						return;
327					}
328				}
329				// if neither minutes nor hours are displayed, we shouldn't
330				// ever get to this point. Counter values recalculation method
331				// should have been already called		
332			}
333			
334			// 'down' mode
335			if(new_values.seconds < 0) {
336				// check for deadline
337				if(this.diff <= 0) {
338					return this.deadlineReached();
339				}
340				new_values.seconds = this.options.limits.seconds - 1;
341				if(this.options.units.minutes == 1) {
342					// minutes displayed
343					new_values.minutes--;
344					if(new_values.minutes < 0) {
345						new_values.minutes = this.options.limits.minutes - 1;
346						if(this.options.units.hours == 1) {
347							// minutes and hours displayed
348							new_values.hours--;
349							if(new_values.hours < 0) {
350								this.softInitCounter();
351								return;
352							}
353						} else {
354							// hours are not displayed but minutes have crossed zero,
355							// recalculate counter values
356							this.softInitCounter();
357							return;
358						}
359					}
360				} else if(this.options.units.hours == 1) {
361					// only hours are displayed
362					new_values.hours--;
363					if(new_values.hours < 0) {
364						// recalculate counter values
365						this.softInitCounter();
366						return;
367					}
368				} else {
369					// neither minutes nor hours are displayed.
370					// recalculate counter values
371					this.softInitCounter();
372					return;
373				}
374			}
375			
376			this.display(new_values);
377		},
378		/**
379		 * A set of calls for soft counter init:
380		 * - Recalulate the counter values
381		 * - If "resumed" param is set, set validate queue mode
382		 */
383		softInitCounter : function(resumed) {
384			if(resumed) {
385				// additional actions on resume. Reserved ***
386			}
387			this.updateCounter(this.getCounterValues());
388		},
389		deadlineReached : function() {
390			// redirect if set so in options. We do it immediately when
391			// this method is called, because more actions at this point
392			// can cause counter flicker and / or orphan values. Anyway,
393			// changing window.location will result in page refresh, thus
394			// all required calculations will be done when the refreshed
395			// page is generated.
396			if(this.options.redirect_url != '') {
397				this.options.counter_container.hide();
398				
399				// we hide the module first, then if user doesn't confirm redirection
400				// we show the module again
401				if(this.doRedirect(this.options.redirect_url, false) === false) {
402					this.options.counter_container.show();
403				}
404			}
405			
406			// New feature: countdown_query_limit for support complex events overlapping
407			if(this.options.countdown_query_limit == 0) {
408				this.options.counter_container.hide();
409				this.queryNextEvent();
410				this.options.countup_limit = -1;
411				return;
412			}
413			
414			var new_values = this.getCounterValues();
415			
416			// in countdown-only and countdown-to-end modes "deadlineReached" requires
417			// immediate query for the next event (call to getCounterValues() method has
418			// already changed counter mode to "up")
419			this.applyCounterLimits();
420			
421			// Force animations re-init in new mode - add mode_changed flag
422			this.updateCounter(new_values, true);
423
424			// update units visibilty, new_diff may be far from zero if the
425			// deadline was reached while suspended, so we must counter
426			// visibility here
427			this.setCounterUnitsVisibility(new_values);
428			
429			this.applyCounterClickable();
430		},
431		/*
432		 * This method performs the main work of calculating counter values
433		 * It uses current module instance display options along with event deadline
434		 * (target date and time) to set correct values relative to "now"
435		 * argument. On module initialization "now" is read from options
436		 * (system time) but later, e.g. on suspend/resume event this value
437		 * will be corrected by suspend time, so that a valid "now" will not
438		 * be requested from server - can be useful in case of short and repeated
439		 * suspend periods
440		 */
441		getCounterValues : function() {
442			var now_ts = scds_container.getServerTime();
443			
444			// init Date objects from this.options.deadline and now
445			var t, dateFrom, dateTo;
446			
447			dateFrom = new Date(now_ts);
448			
449			// we expect ISO format here
450			var dateTo = new Date(this.options.deadline);
451			if(isNaN(dateTo.getTime())) {
452				// panic fallback
453				dateTo = new Date();
454			}
455			
456			/** TEST UNIT ***
457			var testFrom = '2012-02-28';
458			var testTo = '2015-04-01';
459			
460			if(testFrom.length == 10) testFrom = testFrom + ' 00:00:00';
461			if(testTo.length == 10) testTo = testTo + ' 00:00:00';
462			
463			dateTo = new Date(testTo.replace(' ', 'T') + '+00:00');
464			dateFrom = new Date(testFrom.replace(' ', 'T') + '+00:00');
465			*** TEST UNIT END ***/
466			
467			// swap dateTo and dateFrom for 'up' mode and
468			// set surrent counter mode ('down'/'up')
469			this.diff = dateTo - dateFrom;
470			
471			if(this.diff <= 0) {
472				this.options.mode = 'up';
473				var tmp = dateFrom;
474				dateFrom = dateTo;
475				dateTo = tmp;
476				this.diff = this.diff * -1;
477				
478				// do not allow 0 seconds diff in count up mode
479				if(this.diff < MILLIS_IN_SECOND) {
480					this.diff = MILLIS_IN_SECOND;
481				}
482			} else {
483				this.options.mode = 'down';
484			}
485			
486			// get dates components as properties for faster access
487			var target = dateToObject(dateTo);
488			var now = dateToObject(dateFrom);
489			
490			// calculate this.difference in units (years, months, days, hours,
491			// minutes and seconds)
492
493			var yearsDiff = target.year - now.year;
494			var monthsDiff = target.month - now.month;
495			var daysDiff = target.day - now.day;
496			var hoursDiff = target.hours - now.hours;
497			var minutesDiff = target.minutes - now.minutes;
498			var secondsDiff = target.seconds - now.seconds;
499			
500			if(secondsDiff < 0) {
501				minutesDiff--;
502				secondsDiff += SECONDS_IN_MINUTE;
503			}
504			if(minutesDiff < 0) {
505				hoursDiff--;
506				minutesDiff += MINUTES_IN_HOUR;
507			}
508			if(hoursDiff < 0) {
509				daysDiff--;
510				hoursDiff += HOURS_IN_DAY;
511			}
512			if(daysDiff < 0) {
513				monthsDiff--;
514				// tricky: if we get negative days diff we must calculate correct effective days
515				// diff basing on the number of days in "now" month and in the month previous to
516				// "target" and choose the month with more days in it for correct calculation
517				var daysInNowMonth = daysInThisMonth(now.year, now.month);
518				var daysInPrevTargetMonth = daysInPrevMonth(target.year, target.month);
519				// correct daysDiff to get effective positive value
520				daysDiff += (daysInNowMonth > daysInPrevTargetMonth ? daysInNowMonth : daysInPrevTargetMonth);
521			}
522			if(monthsDiff < 0) {
523				yearsDiff--;
524				monthsDiff += MONTHS_IN_YEAR;
525			}
526			// Year diff must be always >= 0: if initial now is greated than initial target we swap them!
527			
528			// Set counter values according to display settigns
529			
530			// days-hours-seconds part of the interval
531			var timeSpan = (hoursDiff * SECONDS_IN_HOUR + minutesDiff * SECONDS_IN_MINUTE + secondsDiff) * MILLIS_IN_SECOND;
532
533			// get years interval part end date
534			var yearsEnd = new Date(Date.UTC(now.year + yearsDiff, now.month - 1, (now.month == 2 && now.day == 29 && new Date(now.year + yearsDiff, 1, 29).getMonth() != 1 ? 28 : now.day), now.hours, now.minutes, now.seconds));
535			var yearsSpan = yearsEnd.valueOf() - dateFrom.valueOf();
536			
537			// get months interval part end date
538			var monthsEnd = new Date(dateTo.valueOf() - daysDiff * MILLIS_IN_DAY - timeSpan);
539			var monthsSpan = monthsEnd.valueOf() - yearsEnd.valueOf();
540			
541			// construct resulting values
542			var result = {
543					/*
544					years : null,
545					months : null,
546					weeks : null,
547					days : null,
548					hours : null,
549					minutes : null,
550					seconds : null
551					*/
552			};
553			var restDiff = this.diff;
554			
555			// tricky cases
556			
557			// if years or months are displayed we have to subtract yearsSpan
558			// from restDiff
559			if(this.options.units.years == 1 || this.options.units.months == 1) {
560				restDiff = restDiff - yearsSpan;
561			}
562			if(this.options.units.years == 1) {
563				result.years = yearsDiff;
564			} else if(this.options.units.months == 1) {
565				// no years but months present: adjust monthsDiff
566				monthsDiff = monthsDiff + yearsDiff * MONTHS_IN_YEAR;
567			}
568			if(this.options.units.months == 1) {
569				// show months value, monthsDiff could have already been adjusted if
570				// years are hidden
571				restDiff = restDiff - monthsSpan;
572				result.months = monthsDiff;
573			} else {
574				// no month display. We can rely on restDiff to calculate remainig
575				// days value (restDiff could be already adjusted due to year and/or
576				// month display settings
577				daysDiff = Math.floor(restDiff / MILLIS_IN_DAY);
578			}
579			// easy cases: starting from weeks unit and lower we can use simple
580			// division to calculate values. No days-in-month and leap years stuff.
581			// We chain restDiff subtraction on each unit that is displayed, so that
582			// next lower unit showes correct value
583			if(this.options.units.weeks == 1) {
584				var weeksDif = Math.floor(daysDiff / 7); // entire weeks
585				daysDiff = daysDiff % 7; // days left
586				restDiff = restDiff - weeksDif * 7 * MILLIS_IN_DAY;
587				result.weeks = weeksDif;
588			}
589			if(this.options.units.days == 1) {
590				result.days = Math.floor(restDiff / MILLIS_IN_DAY);
591				restDiff = restDiff - daysDiff * MILLIS_IN_DAY;
592			}
593			if(this.options.units.hours == 1) {
594				result.hours = Math.floor(restDiff / MILLIS_IN_HOUR);
595				restDiff = restDiff - result.hours * MILLIS_IN_HOUR;
596			}
597			if(this.options.units.minutes == 1) {
598				result.minutes = Math.floor(restDiff / MILLIS_IN_MINUTE);
599				restDiff = restDiff - result.minutes * MILLIS_IN_MINUTE;
600			}
601			// always include seconds in result. Even if seconds are not displayed on
602			// screen according to widget configuration, they will be rendered as hidden.
603			// Also seconds must be there for the "easy inc/dec" method to work -
604			// performance optimization to avoid heavy calculations in tick() method
605			result.seconds = Math.floor(restDiff / MILLIS_IN_SECOND);
606			
607			// set overflow limits
608			if(this.options.units.minutes == 1) {
609				this.options.limits.seconds = SECONDS_IN_MINUTE;
610			} else {
611				if(this.options.units.hours == 1) {
612					this.options.limits.seconds = SECONDS_IN_HOUR;
613				} else {
614					this.options.limits.seconds = SECONDS_IN_DAY;
615				}
616			}
617			if(this.options.units.hours == 1) {
618				this.options.limits.minutes = MINUTES_IN_HOUR;
619			} else {
620				this.options.limits.minutes = MINUTES_IN_DAY;
621			}
622			
623			// we have to check here if visible units count cound change, if so we
624			// set initDisplay flag, so that on display the module layout will be checked
625			// (this is important for suspend/resume cases - now we do not force initDisplay
626			// on each resume - for the sake of performance, but we are aware of the fact
627			// that if device is suspended for a long time, visible units count can change)
628			for(asset in this.current_values) {
629				if(asset != 'seconds' && !this.current_values[asset] != !result[asset]) {
630					this.initDisplay = true;
631					break;
632				}
633			}
634			
635			return result;
636			
637			// Helper functions
638			function dateToObject(date) {
639				return {
640					year : date.getUTCFullYear(),
641					month : date.getUTCMonth() + 1,
642					day : date.getUTCDate(),
643					hours : date.getUTCHours(),
644					minutes : date.getUTCMinutes(),
645					seconds : date.getUTCSeconds()
646				}
647			}
648			function daysInThisMonth(year, month) {
649				// js Date implements 0-based months (0-11), we need next month for current
650				// calculation, so passing 1-based month will do the trick, except DEC (12) -
651				// we convert it to month 0 of the next year
652				if(month == 12) {
653					year++;
654					month = 0;
655				}
656				// day 0 means the last hour of previous day, so we get here the date
657				// of the last day of month-1
658				return new Date(Date.UTC(year, month, 0)).getUTCDate();
659			}
660			function daysInPrevMonth(year, month) {
661				month = month - 2; // -1 for JS Date month notaion (0-11) - 1 for previous month
662				if(month < 0) {
663					year--;
664				}
665				return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
666			}
667		},
668		updateCounter : function(new_values, mode_changed) {
669			if(mode_changed === true) {
670				// reset all digits and elements, so that they are recreated
671				// for new animation (down/up modes can use different animations)
672				$('#' + this.options.id + ' .scd-digit').remove();
673				this.elements = {};
674			}
675			
676			this.display(new_values, mode_changed);
677			this.displayTexts();
677 // *** this call is required only on mode down/up change
678			// e.g. texts are dirty
679		},
680		display : function(new_values, mode_changed) {
681			var prev, next;
682			if(typeof this.current_values.seconds === 'undefined') {
683				// first hard init case. Make this logic better!!! ***
684				this.initDisplay = true;
685				this.current_values = new_values;
686			}
687			// in initDisplay mode we have to abort all running and queued animations
688			if(this.initDisplay) {
689				for(key in this.animation_timers) {
690					window.clearTimeout(this.animation_timers[key]);
691				}
692				this.animation_timers = {};
693			}
694			prev = this.current_values;
695			next = new_values;
696			
697			// Update counter output
698			var i, self = this, updateUnitsWidth = false;
699			$.each(this.options.units, function(asset, display) {
700				if(display == 0 || (!self.initDisplay && !mode_changed && next[asset] == prev[asset])) {
701					return; // no display or unit has not changed - continue loop
702				}
703				// only update on init or if the value actually changed
704				if(self.updateCounterUnit(asset, prev[asset], next[asset], self.initDisplay) === true) {
705					// if number of digits displayed has changed for a counter unit,
706					// updateCounterUnit() will return true
707					updateUnitsWidth = true;
708				}
709			});
710			
711			// Update digits width if required
712			if(updateUnitsWidth) {
713				this.setRowVerticalAlign();
714				this.responsiveAdjust();
715			}
716			
717			if(this.initDisplay ||
718					(this.options.mode == 'down' && next.seconds == this.options.limits.seconds - 1) || 
719					(this.options.mode == 'up' && next.seconds == 0)) {
720				this.setCounterUnitsVisibility(next);
721			}
722			
723			this.initDisplay = false;
724			
725			this.current_values = new_values;
726		},
727		displayTexts : function() {
728			if(this.options.mode == 'up') {
729				$('#' + this.options.id + '-title-before').html(this.options.title_before_up);
730				$('#' + this.options.id + '-title-after').html(this.options.title_after_up);
731			} else {
732				$('#' + this.options.id + '-title-before').html(this.options.title_before_down);
733				$('#' + this.options.id + '-title-after').html(this.options.title_after_down);
734			}
735		},
736		/**
737		 * Update counter unit - digits and label
738		 */
739		updateCounterUnit : function(asset, old_value, new_value, initDisplay) {
740			old_value = this.padLeft(old_value, this.options.paddings[asset]);
741			new_value = this.padLeft(new_value, this.options.paddings[asset]);
742			
743			$('#' + this.options.id + '-' + asset + '-label').text(this.getLabel(asset, new_value));
744			
745			var wrapper = $('#' + this.options.id + '-' + asset + '-digits'), count = wrapper.children('.scd-digit').length;
746			var updateDigitsWidth = false, new_digits;
747			
748			if(new_value.length != count) {
749				new_digits = this.updateDigitsCount(asset, count, old_value, new_value, wrapper);
750				old_value = new_digits.old_value;
751				new_value = new_digits.new_value;
752				updateDigitsWidth = true;
753			}
754			
755			// we have to split the values by digits to check which one actually must
756			// be updated
757			var values = {
758					'prev' : old_value.split(''),
759					'next' : new_value.split('')
760			};
761
762			for(i = 0; i < values['next'].length; i++) {
763				if(values['prev'][i] != values['next'][i] || initDisplay) {
764					// pass both old and new digit value, also scalar new unit value for
765					// margin digits calculation basing on current counter display options
766					// and the unit in question (asset parameter)
767					this.updateCounterDigit(asset, $(wrapper).children('.scd-digit').eq(i), values['next'].length - i - 1, values['prev'][i], values['next'][i], old_value, new_value, initDisplay);
768				}
769			}
770			
771			return updateDigitsWidth;
772		},
773		// left pad with zero helper
774		padLeft : function(value, len) {
775			value = value + '';
776			if(value.length < len) {
777				value = Array(len + 1 - value.length).join('0') + value;
778			}
779			return value;
780		},
781		
782		updateDigitsCount : function(asset, current_count, old_value, new_value, wrapper) {
783			var i;
784			if(new_value.length < current_count) {
785				// remove unused high digit(s)
786				for(i = current_count - 1; i >= new_value.length; i--) {
787					delete(this.elements[asset + i]);
788					wrapper.children('.scd-digit').first().remove();
789				}
790				old_value = old_value.slice(old_value.length - new_value.length);
791				return { old_value : old_value, new_value : new_value };
792			} else {
793				// initialize digits or add missing high digit(s)
794				var new_digit, guess_prev_value, config_index, config;
795				
796				// we have to prepend new digits starting from the lower one
797				var digits_to_add = new_value.slice(0, new_value.length - current_count);
798				// reverse digits
799				digits_to_add = digits_to_add.split('').reverse().join('');
800				// pad old value to the same length as new value
801				old_value = this.padLeft(old_value, new_value.length);
802				
803				// we must reverse old digits to get correct prev values
804				var digits_from = old_value.slice(0, old_value.length - current_count);
805				digits_from = digits_from.split('').reverse().join('');
806				
807				for(i = 0; i < digits_to_add.length; i++) {
808					new_digit = $('<div class="scd-digit"></div>');
809					wrapper.prepend(new_digit);
810					// in init mode we use old_value as previous digit value, otherwise we are
811					// processing a "digits count changed" case, so we assume that previous
812					// digit value is always "0" (as if the old value were padded by zeros)
813					guess_prev_value = current_count == 0 ? digits_from[i] : '0';
814					
815					// When adding a new digit we look for custom digit configuration. Custom digit positions must be expressed 
816					// in asset+index form (less index = lower digit) but should be used with caution because the number of digits in
817					// real counters may differ due to options.unts setting
818					config_index = asset + (i + current_count);
819					config = typeof this.options.animations.digits[config_index] === 'undefined' ? this.options.animations.digits['*'] : this.options.animations.digits[config_index];
820					
821					this.setElements(new_digit, { 'prev' : guess_prev_value, 'next' : digits_to_add[i] }, asset + (i + current_count), config);
822				}
823				return { old_value : old_value, new_value : new_value };
824			}
825		},
826		/**
827		 * Method to actually update a counter digit. All digit transition and animation must be
828		 * performed here.
829		 */
830		updateCounterDigit : function(asset, wrapper, index, old_digit, new_digit, old_unit_value, new_unit_value, initDisplay) {
831			var hash_prefix = asset + index;
832			
833			var values = {
834					'prev' : old_digit,
835					'next' : new_digit
836			}
837			// test-only not a bullet-proof guess! ***
838			if(this.options.animations.uses_margin_values == 1) {
839				if(this.options.mode == 'down') {
840					values['pre-prev'] = this.guessIncrValue(asset, index, old_digit, old_unit_value);
841					values['post-next'] = this.guessDecrValue(asset, index, new_digit, new_unit_value);
842				} else {
843					values['pre-prev'] = this.guessDecrValue(asset, index, old_digit, old_unit_value);
844					values['post-next'] = this.guessIncrValue(asset, index, new_digit, new_unit_value);
845				}
846				if(initDisplay) {
847					// on init "next" and "prev" are equal (this is a design flaw)
848					// calculated "pre-prev" will never be visible, so we can use it
849					// as simple "prev" (*** WORKAROUND, not beautiful at all...)
850					values['prev'] = values['pre-prev'];
851				}
852			}
853			
854			var config_index = asset + index;
855			var config = typeof this.options.animations.digits[config_index] === 'undefined' ? this.options.animations.digits['*'] : this.options.animations.digits[config_index];
856			var groups = config[this.options.mode];
857			
858			if(initDisplay) {
859				this.setElements(wrapper, values, hash_prefix, config);
860			}
861			if(values.prev != values.next) {
862				// only proceed with animation if the values differ
863				this.animateElements(values, hash_prefix, groups);
864			}
865		},
866		guessIncrValue : function(asset, index, digit_value, unit_value) {
867			var limit = this.guessDigitLimit(asset, index, digit_value, unit_value);
868			if(++digit_value > limit) {
869				digit_value = 0;
870			}
871			return digit_value;
872		},
873		guessDecrValue : function(asset, index, digit_value, unit_value) {
874			var limit = this.guessDigitLimit(asset, index, digit_value, unit_value);
875			if(--digit_value < 0) {
876				digit_value = limit;
877			}
878			return digit_value;
879		},
880		guessDigitLimit : function(asset, index, digit_value, unit_value) {
881			var limit = 9;
882			if(asset == 'seconds') {
883				if(this.options.units['minutes'] == 1) {
884					if(index != 0) {
885						limit = 5
886					}
887				}
888			} else if(asset == 'minutes') {
889				if(this.options.units['hours'] == 1) {
890					if(index != 0) {
891						limit = 5
892					}
893				}
894			} else if(asset == 'hours' && this.options.units['days'] == 1) {
895				if(index == 1) {
896					limit = 2;
897				} else if(index == 0 && unit_value >= 20) {
898					limit = 3;
899				}
900			}
901			return limit;
902		},
903		
904		setElements : function(wrapper, values, prefix, config) {
905			wrapper.empty();
906			this.elements[prefix] = {};
907			
908			wrapper.css(config.style);
909			var groups = config[this.options.mode];
910			
911			var i, group;
912			for(i = 0; i < groups.length; i++) {
913				group = groups[i];
914				var els = group.elements, j, el, $el, hash;
915				for(j = 0; j < els.length; j++) {
916					el = els[j];
917					hash = this.getElementHash(el);
918					$el = this.createElement(el, values);
919					
920					if(el.content_type == 'static-bg') {
921						$el.attr('src', this.options.animations.images_folder + el.filename_base + el.filename_ext);
922					}
923					
924					// we have to add created element to collection by unique hash,
925					// so that element duplicates are not appended to wrapper, but can
926					// be referenced without ambiguity when we prodeed with animations
927					// *** store only first unique occurrence
928					if(typeof this.elements[prefix][hash] === 'undefined') {
929						this.elements[prefix][hash] = $el;
930					}
931				}
932			}
933			
934			$.each(this.elements[prefix], function(hash, el) {
935				wrapper.append(el);
936			});
937		},
938		
939		animateElements : function(values, prefix, groups) {
940			var i, group;
941			
942			for(i = 0; i < groups.length; i++) {
943				group = groups[i];
944				var j, el, $el, value;
945				for(j = 0; j < group.elements.length; j++) {
946					el = group.elements[j];
947					
948					if(el.content_type == 'static-bg') {
949						// no animation for static background
950						continue;
951					}
952					
953					$el = this.elements[prefix][this.getElementHash(el)];
954					
955					// resore original style(s) for all elements before we start the real animation
956					$el.css(el.styles);
957				}
958			}
959			
960			// animate first group, next group animation (if any) will be launched after the
961			// previous group's animation is finished
962			this.animateGroup(values, 0, prefix, groups);
963		},
964		animateGroup : function(values, group_index, prefix, groups) {
965			if(group_index >= groups.length) {
966				return;
967			}
968			var i, group = groups[group_index], el, $el, styles, self = this;
969			var duration = this.initDisplay ? 0 : +group.duration; // in initDisplay mode we make all animations instant, i.e. duration = 0
970			var transition = group.transition;
971			var elements_count = group.elements.length, cur_element_index = 0, group_processed = false;
972			
973			for(i = 0; i < group.elements.length; i++) {
974				// get stored element
975				el = group.elements[i];
976				
977				if(el.content_type == 'static-bg') {
978					// no animation for static background
979					elements_count--; // the element is not included in group for
980					// animation, adjust elements_count, so that we can detect
981					// when all elements in group are processed
982					continue;
983				}
984				
985				$el = this.elements[prefix][this.getElementHash(el)];
986				
987				// apply tween.from styles
988				$el.css(el.tweens.from);
989				
990				value = el.content_type == 'uni-img' ? '' : values[el.value_type];
991				if(el.content_type == 'txt') {
992					$el.text(value);
993				} else {
994					var src = this.options.animations.images_folder + el.filename_base + value + el.filename_ext;
995					$el.attr('src', src);
996				}
997				
998				// We are sure that at least 1 element qualified for animation was found in the group
999				group_processed = true;
1000				
1001				if(el.tweens.to.length === 0) { // objects have length undefined, so only empty array
1002					// will pass this condition
1003					// if tweens are empty we have to simulate animation duration. Of course,
1004					// it is possible to use a "trivial" tween hack, e.g. <width>100,100</width>,
1005					// but using native setTimeout() shoud be better.
1006					if(duration > 0) {
1007						// even timeout zero causes animations on init, so if group duration = 0
1008						// we increment cur_element_index directly and proceed to next group if
1009						// required. Store timeout system id, so that it can be cleared explicitly
1010						// in "initDisplay" mode
1011						this.animation_timers[prefix + group_index] = window.setTimeout(function() {
1012							cur_element_index++;
1013							if(cur_element_index >= elements_count) {
1014								self.animateGroup(values, group_index + 1, prefix, groups);
1015							}
1016						}, duration);
1017					} else {
1018						cur_element_index++;
1019						if(cur_element_index >= elements_count) {
1020							this.animateGroup(values, group_index + 1, prefix, groups);
1021						}
1022					}
1023				} else {
1024					// we have tweens defined. Proceed with animation. Stop running animation - prevent
1025					// animations mess up on tab switch back and resume in some browsers
1026					$el.velocity('stop');
1027					
1028					// actually start animation
1029					$el.velocity(el.tweens.to, duration, transition, function() {
1030						cur_element_index++;
1031						if(cur_element_index >= elements_count) {
1032							self.animateGroup(values, group_index + 1, prefix, groups);
1033						}
1034					});
1035				}
1036			}
1037			
1038			if(!group_processed) {
1039				// empty group or a group containing only 'static-bg' elements.
1040				// In some animations this kind of a group serves as a pause in animations queue
1041				// Store timeout system id, so that it can be cleared explicitly
1042				// in "initDisplay" mode
1043				this.animation_timers[prefix + group_index] = window.setTimeout(function() {
1044					self.animateGroup(values, group_index + 1, prefix, groups);
1045				}, duration);
1046			}
1047		},
1048		getElementHash : function(el) {
1049			return [el.content_type, el.value_type, el.filename_base, el.filename_ext].join('::');
1050		},
1051		/*
1052		 * Create DOM element using jQuery, basing on "el" object properties (content_type, tag, etc...)
1053		 */
1054		createElement : function(el, values) {
1055			var $el;
1056			if(el.content_type == 'txt') {
1057				$el = $('<' + el.tag + '/>');
1058				$el.text(values[el.value_type]);
1059			} else {
1060				var value = el.content_type == 'img' ? values[el.value_type] : '', src;
1061				src = this.options.animations.images_folder + el.filename_base + value + el.filename_ext;
1062				
1063				$el = $('<' + el.tag + '/>');
1064				$el.attr('src', src);
1065			}
1066			$el.css(el.styles);
1067			return $el;
1068		},
1069		getLabel : function(asset, value) {
1070			var labels = {
1071				years : 'Y',
1072				months : 'M',
1073				weeks : 'W',
1074				days : 'D',
1075				hours : 'H',
1076				minutes : 'Min',
1077				seconds : 'Sec'
1078			};
1079			
1080			var suffix = smartcountdown_plural(value);
1081			return this.options.label_strings[asset + suffix] || labels[asset];
1082		},
1083		/**
1084		 * Set counter units visibility according to units display configuration and counter values.
1085		 * Related settings:
1086		 * - allow_all_zeros: show zero in the lowest counter unit, if not allowed, the unit with zero
1087		 * value will be replaced by a lower non-zero unit and later replaced with original unit once
1088		 * its value is grater than zero
1089		 * - hide_highest_zeros: hide highest counter units until a non-zero unit is found, even if these
1090		 * high units are set up as "displayed" in units configuration
1091		 */
1092		setCounterUnitsVisibility : function(new_values) {
1093			// restore original hide_lower_units from configuration
1094			var hide_units = $.extend([], this.options.hide_lower_units);
1095			var i, assets = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];
1096			
1097			// allow_all_zeros feature
1098			if(this.options.allow_all_zeros == 0 && $.inArray('seconds', hide_units) != -1) {
1099				var index;
1100				var lowest_displayed_unit = -1, is_non_zero = 0;
1101				for(i = 0; i < assets.length; i++) {
1102					index = $.inArray(assets[i], hide_units);
1103					if(index == -1) {
1104						is_non_zero = is_non_zero + (new_values[assets[i]] || 0);
1105						lowest_displayed_unit = i;
1106					}
1107				}
1108				// only apply "allow_all_zeros disabled" effect if all displayed counter units are zeros
1109				if(!is_non_zero && lowest_displayed_unit >= 0 && new_values[assets[lowest_displayed_unit]] == 0) {
1110					for(i = lowest_displayed_unit; i < assets.length; i++) {
1111						if(new_values[assets[i]] > 0 || assets[i] == 'seconds') {
1112							index = $.inArray(assets[i], hide_units);
1113							if(index > -1) {
1114								hide_units.splice(index, 1);
1115								// replace unit with zero values with a non-zero lower one
1116								if(this.options.replace_lowest_zero == 1) {
1117									hide_units.push(assets[lowest_displayed_unit]);
1118								}
1119								break;
1120							}
1121						}
1122					}
1123				}
1124			}
1125			
1126			// hide_highest_zeros feature
1127			// 0 - disable feature
1128			// 1 - hide highest zeros only
1129			// 2 - hide all zeros (except seconds)
1130			if(this.options.hide_highest_zeros == 2) {
1131				// hide all zeros
1132				for(i = 0; i < assets.length; i++) {
1133					if(this.options.units[assets[i]] == 0) {
1134						continue;
1135					}
1136					if((new_values[assets[i]] || 0) == 0) {
1137						if($.inArray(assets[i], hide_units) == -1) {
1138							hide_units.push(assets[i]);
1139						}
1140					}
1141				}
1142			} else if(this.options.hide_highest_zeros == 1) {
1143				// hide highest only (default)
1144				var lowest_displayed_unit = -1;
1145				for(i = 0; i < assets.length; i++) {
1146					if(this.options.units[assets[i]] == 0) {
1147						continue;
1148					}
1149					lowest_displayed_unit = i;
1150					if((new_values[assets[i]] || 0) == 0) {
1151						if($.inArray(assets[i], hide_units) == -1) {
1152							hide_units.push(assets[i]);
1153						}
1154					} else {
1155						break;
1156					}
1157				}
1158				
1159				// if lowest zero unit found, remove it from hide_units
1160				if(lowest_displayed_unit > -1 && new_values[assets[lowest_displayed_unit]] == 0) {
1161					hide_units.splice(lowest_displayed_unit, 1);
1162				}
1163			}
1164			
1165			// make sure that we have at least one visible unit - some configurations
1166			// (e.g. allow_all_zeros=1 && hide_highest_zeros=1) may result in "all units
1167			// selected for display in module settings are hidden" effect
1168			var visible_unit_found = false;
1169			$.each(this.options.units, function(asset, display) {
1170				if(display && $.inArray(asset, hide_units) == -1) {
1171					visible_unit_found = true;
1172					return false; // break the loop
1173				}
1174			});
1175			// no visible units, show the lowest one
1176			if(!visible_unit_found) {
1177				hide_units.pop();
1178			}
1179			
1180			// optimize performance: only call responsiveAdjust of counter
1181			// layout actually changes
1182			var unitsChanged = false;
1183			
1184			// apply calculated hide_units configuration
1185			var self = this;
1186			$.each(this.options.units, function(asset, display) {
1187				var unit_wrapper = $('#' + self.options.id + '-' + asset);
1188				if(display == 1 && $.inArray(asset, hide_units) == -1) {
1189					if(!unit_wrapper.is(':visible')) {
1190						// hidden => visible
1191						unitsChanged = true;
1192					}
1193					unit_wrapper.show();
1194				} else {
1195					if(unit_wrapper.is(':visible')) {
1196						// visible => hidden
1197						unitsChanged = true;
1198					}
1199					unit_wrapper.hide();
1200				}
1201			});
1202			
1203			if(unitsChanged) {
1204				// only apply responsive if counter units set has changed
1205				this.setRowVerticalAlign();
1206				this.responsiveAdjust();
1207			}
1208			
1209			var counter_container = this.options.counter_container;
1210
1211			// For count up mode we implement an option to completely hide
1212			// counter digits block after the event time is reached
1213			if(this.options.mode == 'up') {
1214				if(this.options.hide_countup_counter == 1) {
1215					counter_container.find('.scd-counter').hide();
1216					counter_container.show();
1217				} else {
1218					counter_container.find('.scd-counter').show();
1219				}
1220			} else {
1221				// in "down" mode we always show counter block. The whole
1222				// module will be hidden if required in applyCounterLimits()
1223				counter_container.find('.scd-counter').show();
1224			}
1225		},
1226		
1227		applyCounterLimits : function() {
1228			// show module before/after event limits
1229			if(this.options.mode == 'down') {
1230				if(this.options.countdown_limit >= 0 && this.diff >= this.options.countdown_limit * MILLIS_IN_SECOND) {
1231					this.options.counter_container.hide();
1232				} else {
1233					this.options.counter_container.show();
1234				}
1235				
1236				// New feature: countdown_query_limit for support complex events overlapping
1237				if(this.options.countdown_query_limit > 0 && this.diff < this.options.countdown_query_limit  * MILLIS_IN_SECOND) {
1238					this.queryNextEvent();
1239					this.options.countup_limit = -1;
1240				}
1241			}
1242			if(this.options.mode == 'up') {
1243				if(this.options.countup_limit >= 0 && this.diff >= this.options.countup_limit * MILLIS_IN_SECOND) {
1244					this.queryNextEvent();
1245					
1246					// disable countup limit temporarly, so that no more queryNextEvent() are
1247					// done. Correct countup limit will be set when the event request is done.
1248					// *** we do countup limit reset after calling queryNextEvent(), so that
1249					// actual limit value can be used inside queryNextEvent() method. Anyway,
1250					// resetting countup limit here will guarantee that no more queries will
1251					// be done while current one is in progress
1252					this.options.countup_limit = -1;
1253				}
1254			}
1255		},
1256		
1257		/*
1258		 * TODO: check if we can optimize calls to this method. For now it is called twice
1259		 * in some cases. This is not a big problem - the function is quite fast and at most
1260		 * called each minute, but for the sake of code cleanness...
1261		 */
1262		setRowVerticalAlign : function() {
1263			// calculate labels maximum width(s) for all singular/plural forms
1264			if(typeof this.label_min_widths === 'undefined') {
1265				// widths are calculated in em, so it has to be done only once
1266				if(this.updateMinLabelsWidth() === false) {
1267					// updateMinLabelsWidth failed, e.g. no visible labels - do nothing
1268					return;
1269				}
1270			}
1271			
1272			// align digits - for counter column layout only
1273			var digits = $('#' + this.options.id + ' .scd-unit-vert .scd-digits-row:visible');
1274			if(digits.length > 0) {
1275				var maxWidth = 0, width;
1276				digits.css('min-width', '');
1277				digits.each(function() {
1278					width = $(this).width();
1279					if(width > maxWidth) {
1280						maxWidth = width;
1281					}
1282				});
1283				digits.css('min-width', maxWidth);
1284			}
1285			
1286			// Labels min-widths are expressed in 'em'.
1287			
1288			// all row labels for vertical counter layout must have the same min-width =
1289			// maximum width of all labels/all forms
1290			var labels = $('#' + this.options.id + ' .scd-unit-vert .scd-label-row:visible');
1291			labels.css('min-width', this.labels_max_width + 'em');
1292			
1293			// for horizontal counter layout we set:
1294			// - column labels: to maximum width of all labels/all forms
1295			// - row labels: to maximum width of all forms for this label only
1296			labels = $('#' + this.options.id + ' .scd-unit-horz .scd-label:visible');
1297			if(labels.length > 0) {
1298				var self = this;
1299				labels.each(function() {
1300					var $this = $(this);
1301					width = $this.width();
1302					if($this.hasClass('scd-label-col')) {
1303						// same width for all
1304						$this.css('min-width', self.labels_max_width + 'em');
1305					} else {
1306						// maximum width for this label (singular/plural forms)
1307						$this.css('min-width', self.label_min_widths[$this.attr('id')] + 'em');
1308					}
1309				});
1310			}
1311			this.setLabelsPosition();
1312		},
1313		
1314		updateMinLabelsWidth : function() {
1315			// use the fisrt visible label as test element for measuring. This method is called only once per
1316			// script life-time and on early stage, so we can rely on visible pseudo-selector: counter visibilty
1317			// is not updated yet, so all labels are visible. This can be a design flaw if program logic changes
1318			var test_label = $('#' + this.options.id + ' .scd-label:visible').first(), unit, label_id, width, max_width = 0;
1319			if(test_label.length == 0) {
1320				// panic
1321				return false;
1322			}
1323			// backup existing text
1324			var the_text = test_label.text();
1325			// reset measurement
1326			test_label.css('min-width', '');
1327			this.label_min_widths = {};
1328			// loop through all label strings injected in js script
1329			for(label_key in this.options.label_strings) {
1330				// get unit name for object key
1331				unit = label_key.split('_', 1)[0];
1332				if(!unit) {
1333					unit = label_key;
1334				}
1335				// constuct label id - it will be used as label_min_widths key and for fast
1336				// access to label attributes
1337				label_id = this.options.id + '-' + unit + '-label';
1338				
1339				if(typeof this.label_min_widths[label_id] === 'undefined') {
1340					// on first iteration initialize the value
1341					this.label_min_widths[label_id] = 0;
1342				}
1343				// set test label text
1344				test_label.text(this.options.label_strings[label_key]);
1345				// do the measurement in 'em' based on labels size
1346				width = test_label.width() / this.options.labels_size;
1347				// update unit maximum
1348				if(this.label_min_widths[label_id] < width) {
1349					this.label_min_widths[label_id] = width;
1350				}
1351				// keep track of the maximum-for-all width
1352				if(max_width < width) {
1353					max_width = width;
1354				}
1355				// all widths are expressed in em, but stored as float numbers -
1356				// make sure we append 'em' to these values when applying as css
1357			}
1358			// store maximum-for-all width
1359			this.labels_max_width = max_width;
1360			// clean up - restore original text of the test label
1361			test_label.text(the_text);
1362		},
1363		
1364		// update labels vertical position for left or right labels placement
1365		setLabelsPosition : function() {
1366			// reset position for column lables
1367			$('#' + this.options.id + ' .scd-label-col:visible').css('position', '');
1368			
1369			// adjust labels position if neeed (if vertical label position is set)
1370			var labels = $('#' + this.options.id + ' .scd-label-row:visible');
1371			if(labels.length > 0) {
1372				var digitsDiv, digitsHeight, labelHeight, top, self = this;
1373				labels.each(function() {
1374					var $this = $(this);
1375					digitsDiv = $this.siblings('.scd-digits-row');
1376					digitsHeight = digitsDiv.height();
1377					labelHeight = $this.height();
1378					
1379					// check if digits and label are horizontally overlapping
1380					var a = digitsDiv[0].getBoundingClientRect();
1381					var b = this.getBoundingClientRect();
1382					if(!( // we allow adjacent divs
1383							b.left > a.right ||
1384							b.right < a.left ||
1385							b.top > a.bottom ||
1386							b.bottom < a.top
1387					)) {
1388						$this.css('position', '');
1389						$this.css('top', '');
1390						return true; // continue iteration
1391					}
1392					// continue with position (normal flow)
1393					$this.css('position', 'relative');
1394					switch(self.options.labels_vert_align) {
1395						case 'top' :
1396							top = 0;
1397							break;
1398						case 'bottom' :
1399							top = digitsHeight - labelHeight;
1400							break;
1401						case 'high' :
1402							top = labelHeight * 0.5;
1403							break;
1404						case 'low' :
1405							top = digitsHeight - labelHeight * 1.5;
1406							break;
1407						case 'superscript' :
1408							top = labelHeight * -0.5;
1409							break;
1410						case 'subscript' :
1411							top = digitsHeight - labelHeight / 2;
1412							break;
1413						default: // middle
1414							top = digitsHeight / 2 - labelHeight / 2;
1415					}
1416					$this.css('top', top);
1417				});
1418			}
1419		},
1420		
1421		resetAlignments : function(counter_container) {
1422			counter_container.find('.scd-label').css({ position : '', top : '' });
1423			counter_container.find('.scd-label, .scd-digits').css('min-width', '');
1424		},
1425		
1426		responsiveAdjust : function(/*width*/) {
1427			var responsive = this.options.responsive;
1428			var counter_container = $('#' + this.options.id), i, scale = 1.0, self = this;
1429			
1430			// page layout changes during responsive adjust procedure and in some browsers
1431			// can cause accidental scroll to window top. We fix the height of the module
1432			// as a workaround. Later we reset fixed height - IMPORTANT: height rule in
1433			// module style(s) setting will be discarded. Check if this is an important limitation...
1434			
1435			var height = counter_container.height();
1436			counter_container.css('height', height);
1437			
1438			if(responsive && responsive.length > 0) {
1439				// responsive behavior
1440
1441				// we have to reset all existing labels and digits alignment before proceeding with
1442				// responsive adjust
1443				this.resetAlignments(counter_container);
1444				
1445				// reset container font before starting measurments
1446				counter_container.css('font-size', this.options.base_font_size + 'px');
1447				
1448				// restore original layout before measurement
1449				var scale_preset = responsive[responsive.length - 1];
1450				this.applyLayout(counter_container, scale_preset.alt_classes);
1451				
1452				// update layout for new font size for correct measurement
1453				this.setRowVerticalAlign();
1454				
1455				// check if horizontal-layout units wrap
1456				var is_wrapping = this.checkWrapping(counter_container.find('.scd-unit-horz:visible, .scd-title-row'));
1457				
1458				if(is_wrapping.wrapped_width > 0) {
1459					var container_width = counter_container.width();
1460					var required_width = is_wrapping.row_width + is_wrapping.wrapped_width + SCD_SAFE_ADJUST_PX;
1461
1462					scale = container_width / required_width;
1463				}
1464
1465				// check extreme case - wrapping digits in unit, do not allow this
1466				var digit_groups = counter_container.find('.scd-digits:visible'), is_wrapped = false;
1467				digit_groups.each(function() {
1468					var $this = $(this);
1469					var is_wrapping = self.checkWrapping($this.find('.scd-digit'));
1470
1471					if(is_wrapping.wrapped_width > 0) {
1472						var container_width = $this.width();
1473						var required_width = is_wrapping.row_width + is_wrapping.wrapped_width + SCD_SAFE_ADJUST_PX;
1474						
1475						var this_scale = container_width / required_width;
1476						// only update current effective scale if we get a lower value here
1477						if(this_scale < scale) {
1478							scale = this_scale;
1479						}
1480						is_wrapped = true;
1481					}
1482				});
1483				
1484				// we have suggested scale at this point
1485				// Look at scale alternative layouts in responsive settings
1486				
1487				// IMPORTANT: scale nodes must be sorted ascending, otherwise responsive behavior will be
1488				// unpredictable
1489				for(i = 0; i < responsive.length; i++) {
1490					var scale_preset = responsive[i];
1491					if(scale <= scale_preset.scale) {
1492						// apply additional classes (if any)
1493						this.applyLayout(counter_container, scale_preset.alt_classes);
1494						
1495						// we are done - no need to continue looping
1496						break;
1497					}
1498				}
1499				// we have alternative layout applied
1500				
1501				// prepare to repeat measurement with updated layout - reset base font
1502				scale = 1.0;
1503				counter_container.css('font-size', this.options.base_font_size + 'px');
1504				
1505				// realign labels and measure units (for column layouts)
1506				this.setRowVerticalAlign();
1507				
1508				// check if horizontal-layout units wrap
1509				var is_wrapping = this.checkWrapping(counter_container.find('.scd-unit-horz:visible, .scd-title-row'));
1510				
1511				if(is_wrapping.wrapped_width > 0) {
1512					var container_width = counter_container.width();
1513					var required_width = is_wrapping.row_width + is_wrapping.wrapped_width + SCD_SAFE_ADJUST_PX;
1514
1515					scale = container_width / required_width;
1516					counter_container.css('font-size', (this.options.base_font_size * scale) + 'px');
1517					
1518					// update layout for new base font size
1519					this.setRowVerticalAlign();
1520				}			
1521			} else {
1522				// prepare for next measurement if responsive feature is disabled
1523				counter_container.css('font-size', this.options.base_font_size + 'px');
1524			}
1525			
1526			// even if responsive feature is disabled we always check for digits wrapping and
1527			// scale the module so that all digits in units are placed on the same line
1528			var digit_groups = counter_container.find('.scd-digits:visible'), is_wrapped = false;
1529			
1530			digit_groups.each(function() {
1531				var $this = $(this);
1532				var is_wrapping = self.checkWrapping($this.find('.scd-digit'));
1533
1534				if(is_wrapping.wrapped_width > 0) {
1535					var container_width = $this.width();
1536					var required_width = is_wrapping.row_width + is_wrapping.wrapped_width + SCD_SAFE_ADJUST_PX;
1537					
1538					var this_scale = container_width / required_width;
1539					// only update current effective scale if we get a lower value here
1540					if(this_scale < scale) {
1541						scale = this_scale;
1542					}
1543					is_wrapped = true;
1544				}
1545			});
1546			
1547			if(is_wrapped) {
1548				// apply new calulated scale
1549				counter_container.css('font-size', (this.options.base_font_size * scale) + 'px');
1550			}
1551			// update layout for new base font size
1552			this.setRowVerticalAlign();
1553			
1554			// reset fixed height (accidental scroll workaround)
1555			counter_container.css('height', '');
1556		},
1557		
1558		checkWrapping : function(units) {
1559			var row_width = 0, wrapped_width = 0, top = null;
1560			units.each(function() {
1561				var $this = $(this), unit_position = $this.position();
1562				if(top === null) {
1563					top = unit_position.top;
1564				}
1565				if(top != unit_position.top) {
1566					wrapped_width += $this.outerWidth(true);
1567				} else {
1568					row_width += $this.outerWidth(true);
1569				}
1570			});
1571			return {
1572				row_width : row_width, // top row width (non-wrapped units)
1573				wrapped_width : wrapped_width // all other rows (wrapped units)
1574			};
1575		},
1576		
1577		applyLayout : function(counter_container, alt_classes) {
1578			if(alt_classes.length > 0) {
1579				$.each(alt_classes, function(index, classes) {
1580					if($.isArray(classes)) {
1581						$.each(classes, function(ci, c) {
1582							counter_container.find(c.selector).removeClass(c.remove).addClass(c.add);
1583						});
1584					} else {
1585						counter_container.find(classes.selector).removeClass(classes.remove).addClass(classes.add);
1586					}
1587				});
1588			}
1589		},
1590		
1591		applyCounterClickable : function(is_countdown_to_end) {
1592			// if the counter is clickable, set the handler
1593			if(this.options.click_url != '') {
1594				if(this.options.counter_clickable == 1 ||
1595						(this.options.counter_clickable == 2 && this.options.mode == 'down') ||
1596						(this.options.counter_clickable == 3 && (this.options.mode == 'up' || is_countdown_to_end == 1))
1597				) {
1598					this.options.counter_container.css('cursor', 'pointer');
1599					this.options.counter_container.off('click');
1600					var self = this;
1601					this.options.counter_container.on('click', function() {
1602						self.doRedirect(self.options.click_url, true);
1603					});
1604					
1605					// counter is clickable. We are done
1606					return;
1607				}
1608			}
1609			// we get to this point only if one of the conditions or making counter
1610			// clickable was not met. Disable counter link (click functionality)
1611			this.options.counter_container.css('cursor', 'default');
1612			this.options.counter_container.off('click');
1613		},
1614		
1615		doRedirect : function(url, new_window) {
1616			var confirmed;
1617			if(this.options.confirm_redirect == 1 && !new_window) {
1618				// we try to guess if there are partially filled-in forms on the page:
1619				// - discard hidden inputs
1620				// - discard checkboxes
1621				var inputs = $('form input, form textarea, form select').not(':checkbox').not(':hidden');
1622				if(!!inputs.filter(function() {
1623					return $(this).val();
1624				}).length) {
1625					// user might lose data entered, ask permission
1626					confirmed = confirm(Joomla.JText._('MOD_SMARTCOUNTDOWN3_REDIRECT_CONFIRM_HINT'));
1627				} else {
1628					// no form data found
1629					confirmed = true;
1630				}
1631			} else {
1632				// "confirm redirect" not enabled - implicit redirect permission
1633				confirmed = true;
1634			}
1635			if(confirmed) {
1636				if(new_window) {
1637					window.open(url, '_blank');
1638				} else {
1639					window.location = url;
1640				}
1641			} else {
1642				// return false to indicate that the user refused redirection. Sometimes we
1643				// must show the module (in most cases the module is hidden just before starting
1644				// redirection to avoid wrong display during page refresh).
1645				return false;
1646			}
1647		},
1648		
1649		queryNextEvent : function(isNew) {
1650			var self = this;
1651			
1652			// show spinner
1653			$('#' + self.options.id + '-loading').show();
1654			
1655			$('#' + self.options.id + ' .scd-all-wrapper').hide();
1656			
1657			var queryData = {
1658				option : 'com_smartcountdown3',
1659				task : 'event.getevent',
1660				scd_module_id : this.options.module_id,
1661				format : 'json',
1662				prevent_cache : new Date().getTime()
1663			};
1664			
1665			$.getJSON(
1666					this.options.ajaxurl,
1667					queryData,
1668					function(response) {
1669						// hide spinner
1670						$('#' + self.options.id + '-loading').hide();
1671						
1672						$('#' + self.options.id + ' .scd-all-wrapper').show();
1673						
1674						if(typeof response !== 'undefined' && response.err_code == 0) {
1675							// Actually we have "self" object already initialized and
1676							// setup with options
1677							
1678							// we have to add actual "deadline" here, it
1679							// should work for plain counters (caching workaround) and
1680							// also for events import plugins
1681							
1682							self.options.deadline = response.options.deadline;
1683							
1684							// we append imported event title (if any) to counter titles
1685							// or insert imported title if a placeholder found in original
1686							
1687							// now we support plugins which provide different titles for down and up modes,
1688							// e.g. if link event option is set differently for events in fiture or events
1689							// in progress.
1690							var imported_title_down = response.options.imported_title_down;
1691							var imported_title_up = response.options.imported_title_up;
1692							
1693							// restore original titles for down mode (could be replaced during cout
1693ner life with
1694							// countdown-to-end titles
1695							self.options.title_before_down = self.options.original_title_before_down;
1696							self.options.title_after_down = self.options.original_title_after_down;
1697							
1698							if(typeof imported_title_down !== 'undefined') {
1699								if(self.options.original_title_after_down != '' && self.options.original_title_after_down.indexOf('%imported%') != -1) {
1700									// replace placeholder with imported title
1701									self.options.title_after_down = self.options.original_title_after_down.replace('%imported%', imported_title_down);
1702								} else if(self.options.original_title_before_down != '') { // only modify title before if imported title was not already inserted
1703									if(self.options.original_title_before_down.indexOf('%imported%') != -1) {
1704										// replace placeholder with imported title
1705										self.options.title_before_down = self.options.original_title_before_down.replace('%imported%', imported_title_down);
1706									} else {
1707										// no placeholder - append imported title
1708										self.options.title_before_down = self.options.original_title_before_down + ' ' + imported_title_down;
1709									}
1710								} else {
1711									// generic title empty - use imported title as is
1712									self.options.title_before_down = imported_title_down;
1713								}
1714							} else {
1715								// just in case - remove "imported" placeholders
1716								self.options.title_before_down = self.options.original_title_before_down.replace('%imported%', '');								
1717								self.options.title_after_down = self.options.original_title_after_down.replace('%imported%', '');
1718							}
1719							
1720							if(typeof imported_title_up !== 'undefined') {
1721								if(self.options.original_title_after_up != '' && self.options.original_title_after_up.indexOf('%imported%') != -1) {
1722									// replace placeholder with imported title
1723									self.options.title_after_up = self.options.original_title_after_up.replace('%imported%', imported_title_up);
1724								} else if(self.options.original_title_before_up != '') { // only modify title before if imported title was not already inserted
1725									if(self.options.original_title_before_up.indexOf('%imported%') != -1) {
1726										// replace placeholder with imported title
1727										self.options.title_before_up = self.options.original_title_before_up.replace('%imported%', imported_title_up);
1728									} else {
1729										// no placeholder - append imported title
1730										self.options.title_before_up = self.options.original_title_before_up + ' ' + imported_title_up;
1731									}
1732								} else {
1733									// generic title empty - use imported title as is
1734									self.options.title_before_up = imported_title_up;
1735								}
1736							} else {
1737								// just in case - remove "imported" placeholders
1738								self.options.title_before_up = self.options.original_title_before_up.replace('%imported%', '');								
1739								self.options.title_after_up = self.options.original_title_after_up.replace('%imported%', '');
1740							}
1741
1742							// some event import plugins may set is_countdown_to_end flag indicating that
1743							// event date and time are actually end time for an event, so that we should display
1744							// a countdown but with "up" title
1745							if(response.options.is_countdown_to_end == 1) {
1746								self.options.title_before_down = self.options.title_before_up;
1747								self.options.title_after_down = self.options.title_after_up;
1748								// in "countdown to end" mode we release countdown limit to allow
1749								// mode "0:-2". We have a certain limitation here: it is not possible
1750								// to configure CTE counter to appear N seconds before event end.
1751								// only 2 options are supported - only show CTE mode or "auto" + CTE,
1752								// i.e. "3600:-2" kind of modes are not implemented
1753								self.options.countdown_limit = -1;
1754							} else {
1755								self.options.countdown_limit = self.options.original_countdown_limit;
1756							}
1757							self.options.countup_limit = response.options.countup_limit;
1758							// New feature: countdown_query_limit for support complex events overlapping
1759							self.options.countdown_query_limit = typeof response.options.countdown_query_limit !== 'undefined'
1760								? response.options.countdown_query_limit : -1; // for those who updated the module but didn't update the component
1761							
1762							if(response.options.deadline == '') {
1763								// no next events. TODO: display message(?), etc.
1764								
1765								// detach counter from container - this is a definitive
1766								// shut-down for this counter instance, as there are no future events
1767								scds_container.remove(self.options.id);
1768								
1769								self.options.counter_container.hide();
1770								return;
1771							}
1772							
1773							// event import plugins may provide custom redirect and click URLS,
1774							// if so, update those in initial module options
1775							if(typeof response.options.redirect_url !== 'undefined') {
1776								self.options.redirect_url = response.options.redirect_url;
1777							}
1778							if(typeof response.options.click_url !== 'undefined') {
1779								self.options.click_url = response.options.click_url;
1780							}
1781							
1782							// convert deadline to javascript Date (compatibility with IE8)
1783							self.options.deadline = new Date(self.tsFromISO(self.options.deadline)).toString();
1784							scds_container.setServerTime(response.options.now);
1785							self.updateCounter(self.getCounterValues());
1786							
1787							// We have to set counter mode limits after getting new event
1788							self.applyCounterLimits();
1789							
1790							self.applyCounterClickable(response.options.is_countdown_to_end);
1791							
1792							// module instance registration in container is
1793							// required only for the first event query, switching
1794							// to next event in a running instance doesn't require
1795							// adding module instance to container because it is already there
1796							if(isNew) {
1797								scds_container.updateInstance(self.options.id, self);
1798							} else if(self.options.redirect_url != '' && self.options.mode == 'up') {
1799								// when the counter is running and queryNextEvent is called on
1800								// deadline-reached event, the event in response might have already
1801								// started (overlapping events). So we have to simulate automatic
1802								// redirection on countdown zero if set so in response - this event
1803								// has actually started, but its auto-redirect eas not run!
1804								// We are on the safe side with redirect loop hell because on new
1805								// pages this code will never fire. If you reload the page right after 
1806								// this implicit redirect it will not execute auto-redirect
1807								self.options.counter_container.hide();
1808								
1809								// we hide the module first, then if user doesn't confirm redirection
1810								// we show the module again
1811								if(self.doRedirect(self.options.redirect_url, false) === false) {
1812									self.options.counter_container.show();
1813								}
1814							}
1815							
1816							// We have to set units display after getting new event
1817							self.setCounterUnitsVisibility(self.current_values);
1818						} else if(response.err_code == 101 && response.err_msg) {
1819							// 101 code are errors from event import plugins, we display them as alerts
1820							alert(response.err_msg);
1821						}
1822					}
1823				).fail(function(jqxhr, textStatus, error) {
1824					$('#' + self.options.id + '-loading').hide();
1825					$('#' + self.options.id + ' .scd-all-wrapper').show();
1826					//alert(textStatus + ", " + error);
1827				});
1828		},
1829		// parse date for older browsers
1830		tsFromISO : function(s) {
1831			var D = new Date(s);
1832			if(isNaN(D.getTime())) {
1833				var day, tz,
1834	            rx=/^(\d{4}\-\d\d\-\d\d([tT ][\d:\.]*)?)([zZ]|([+\-])(\d\d):(\d\d))?$/,
1835	            p= rx.exec(s) || [];
1836	            if(p[1]){
1837	                day= p[1].split(/\D/);
1838	                for(var i= 0, L= day.length; i<L; i++){
1839	                    day[i]= parseInt(day[i], 10) || 0;
1840	                };
1841	                day[1]-= 1;
1842	                day= new Date(Date.UTC.apply(Date, day));
1843	                if(!day.getDate()) return NaN;
1844	                if(p[5]){
1845	                    tz= (parseInt(p[5], 10)*60);
1846	                    if(p[6]) tz+= parseInt(p[6], 10);
1847	                    if(p[4]== '+') tz*= -1;
1848	                    if(tz) day.setUTCMinutes(day.getUTCMinutes()+ tz);
1849	                }
1850	                return day.getTime();
1851	            }
1852	            return NaN;
1853			} else {
1854				return D.getTime();
1855			}
1856		}
1857	}
1858})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.