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.