1'use strict'; 2/* 3* jLiveTime 4* jQuery plugin for live timestamps, countdowns, time-ago, and timers. 5*/ 6(function($){ 7 8 var lt = $.livetime = $.livetime || {}; 9 lt.version = '0.0.13'; 10 11 lt.localTimeOffset = null; 12 13 var defaultOptions = { 14 datetimeSelector: '[datetime]', 15 datetimeAttribute: 'datetime', 16 durationAttribute: 'data-duration', 17 dateLabelSelector: '[data-time-label]', 18 triggerRefreshComplete: true, 19 serverTimeUrl: null, 20 autostart: false 21 }; 22 var defaultFormats = { 23 _default: [ 24 [-24*3600*30*11, 'yyyy MMMM d at H:mm'], 25 [-24*3600*30, 'in td_M months d_d days, MMMM d at H:mm'], 26 [-24*3600*7, 'in td_d days, MMMM d at H:mm'], 27 [-24*3600*2, 'next eeee, MMMM d at H:mm'], 28 [-2*3600, 'in td_h hours d_m minutes, at H:mm'], 29 [-60*5, 'in td_m minutes, at H:mm:ss'], 30 [-60, 'in td_m minutes d_s seconds, at H:mm:ss'], 31 [0, 'in td_s seconds, at H:mm:ss'], 32 [60, 'td_s seconds ago, at H:mm:ss'], 33 [60*5, 'td_m minutes d_s seconds ago, at H:mm:ss'], 34 [2*3600, 'td_m minutes ago, at H:mm:ss'], 35 [24*3600*2 , 'td_h hours d_m minutes ago, at H:mm'], 36 [24*3600*7 , 'last eeee, MMMM d at H:mm'], 37 [24*3600*30, 'MMMM d at H:mm'], 38 ['yyyy MMMM d at H:mm'] 39 ], 40 seconds: [ 41 [0, 'in td_hh:d_mm:d_ss'], 42 ['td_hh:d_mm:d_ss'] 43 ], 44 milliseconds: [ 45 [0, 'in td_hh:d_mm:d_ss.d_fff'], 46 ['td_hh:d_mm:d_ss.d_fff'] 47 ], 48 humanized: [ 49 [-360*24*3600, 'MMMM d, yyyy'], 50 ['daystart-'+7*24*3600, 'MMMM d at h:mm tt'], 51 ['daystart-'+24*3600, 'next eeee at h:mm tt'], 52 ['daystart', 'tomorrow at h:mm tt'], 53 [-7200, 'today at h:mm tt'], 54 [-3600, 'in about an hour'], 55 [-120, 'in td_m minutes'], 56 [-60, 'in about a minute'], 57 [0, 'in a few seconds'], 58 [60, 'a few seconds ago'], 59 [120, 'about a minute ago'], 60 [3600, 'td_m minutes ago'], 61 [7200, 'about an hour ago'], 62 [5*3600, 'td_h hours ago'], 63 ['dayend', 'today at h:mm tt'], 64 ['dayend+'+24*3600, 'yesterday at h:mm tt'], 65 ['dayend+'+7*24*3600, 'last eeee at h:mm tt'], 66 [360*24*3600, 'MMMM d at h:mm tt'], 67 ['MMMM d, yyyy'] 68 ], 69 fulldate: 'eee MMM d yyyy at h:mm:ss tt', 70 shortdate: 'MMM d yyyy', 71 _default_tooltip: '#fulldate', 72 _in: [[0,'in'],['']], 73 remaining: [[0,'remaining'],['']], 74 ago: [[0,''],['ago']], 75 elapsed: [[0,''],['elapsed']] 76 }; 77 78 var options = lt.options = $.extend(true, defaultOptions, lt.options); 79 options.formats = $.extend(defaultFormats, options.formats); 80 81 var padLeft = function(num, digits) { 82 if (digits < 2) { 83 return num; 84 } 85 var str = num+''; 86 while (str.length < digits) { 87 str = '0' + str; 88 } 89 return str; 90 }; 91 92 var timeUnits = { 93 // years 94 y: 1000 * 3600 * 24 * 365.25, 95 // months 96 M: 1000 * 3600 * 24 * 30, 97 // weeks 98 w: 1000 * 3600 * 24 * 7, 99 // days (from month) 100 d: 1000 * 3600 * 24, 101 // days (from week) 102 e: 1000 * 3600 * 24, 103 // hours 104 h: 1000 * 3600, 105 // minutes 106 m: 1000 * 60, 107 // seconds 108 s: 1000, 109 // milliseconds 110 f: 1 111 }; 112 113 var parentTimeUnits = { 114 M: 'y', 115 w: 'M', 116 d: 'M', 117 e: 'w', 118 h: 'd', 119 m: 'h', 120 s: 'm', 121 f: 's' 122 }; 123 124 var unitNames = { 125 M: ['January','February','March','April','May','June','July','August','September','October','November','December'], 126 e: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'] 127 }; 128 129 var log = (window.console && typeof window.console.log == 'function') ? 130 function(){ console.log.call(console, arguments); } : 131 function(){}; 132 133 var datetimeRegex = /^(\d{4})\-(\d{2})\-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(.*))?$/; 134 var timezoneRegex = /^([+\-]?)(\d{2}):(\d{2})$/; 135 136 // alternative strin date parsers, the first supported by browser will be used 137 var dateStringParsers = [ 138 // use native constructor (fastest) 139 function(str) { 140 return new Date(str); 141 }, 142 // use native constructor, allowing space instead of T 143 function(str) { 144 return new Date(str.slice(10,11)!==' ' ? str : str.slice(0,10) + 'T' + str.slice(11)); 145 }, 146 // use regex (slowest, but safe) 147 function(str) { 148 var match = datetimeRegex.exec(str); 149 var timezoneOffset = 0; 150 if (match) { 151 // remove leading zeros 152 for (var i=1; i < 8; i++) { 153 var matchString = match[i]; 154 if (typeof matchString == 'string' && matchString.length > 1 && matchString.slice(0,1) === '0') { 155 match[i] = matchString.slice(0,-1).replace(/^0+/, '') + matchString.slice(-1); 156 } 157 } 158 if (typeof match[4] == 'undefined' || match[4] === '' || match[4] === null) { 159 return new Date(Date.UTC(parseInt(match[1]), parseInt(match[2])-1, parseInt(match[3]))); 160 } 161 if (match[8] && match[8] !== 'Z' && match[8] !== 'GMT' && match[8] !== 'UTC') { 162 var timezoneMatch = timezoneRegex.exec(match[8]); 163 if (timezoneMatch) { 164 timezoneOffset = (timezoneMatch[0] === '-' ? -1 : 1) * (parseInt(timezoneMatch[2]) * 60 + parseInt(timezoneMatch[3])) * 60000; 165 } else { 166 throw new Error('invalid datetime format'); 167 } 168 } 169 return new Date(Date.UTC(parseInt(match[1]), parseInt(match[2])-1, parseInt(match[3]), 170 parseInt(match[4]), parseInt(match[5]), parseInt(match[6]), match[7] ? parseInt(match[7]) : 0) + timezoneOffset); 171 } 172 throw new Error('invalid datetime format'); 173 } 174 ];
175 var validDateStringParser = dateStringParsers[dateStringParsers.length-1]; 176 177 var detectValidDateParser = function(){ 178 var valid = false; 179 var index = 0; 180 var tests = {}; 181 tests['2012-12-04 23:59:59.999Z'] = Date.UTC(2012,11,4,23,59,59,999); 182 tests['2012-01-31T00:54:02'] = Date.UTC(2012,0,31,0,54,2); 183 tests['2012-12-22T18:30:00-03:00'] = Date.UTC(2012,11,22,21,30,0); 184 tests['1987-02-28'] = Date.UTC(1987,1,28); 185 186 while (!valid && index < dateStringParsers.length) { 187 try { 188 valid = true; 189 for (var str in tests) { 190 if (tests[str] !== dateStringParsers[index](str).getTime()) { 191 valid = false; 192 break; 193 } 194 } 195 if (valid) { 196 validDateStringParser = dateStringParsers[index]; 197 } 198 } catch(err) { 199 valid = false; 200 } 201 if (!valid) { 202 index++; 203 } 204 } 205 }; 206 207 detectValidDateParser(); 208 209 lt.parseDate = function(timestamp) { 210 if (typeof str == 'number' || /^[0-9]+$/.test(timestamp)) { 211 return new Date(parseInt(timestamp,10)); 212 } else { 213 try { 214 return validDateStringParser(timestamp); 215 } catch(err) { 216 throw new Error('error parsing datetime "' + timestamp + '": ' + err); 217 } 218 } 219 }; 220 221 lt.now = function(){ 222 if (lt.localTimeOffset === null){ 223 // local time offset unknown yet 224 lt.getLocalTimeOffset(); 225 } 226 var localTimeOffset = lt.localTimeOffset || 0; 227 var now = new Date(); 228 var nowUTC = now.getTime() - now.getTimezoneOffset()*60*1000 + localTimeOffset; 229 return nowUTC; 230 }; 231 232 lt.getLocalTimeOffset = function(){ 233 var cookieOffset, now, dateHeader, match, serverTime; 234 235 if (!options.serverTimeUrl) { 236 lt.localTimeOffset = 0; 237 return; 238 } 239 240 if (!lt.localTimeOffsetRequested && (typeof lt.localTimeOffset == 'undefined' || lt.localTimeOffset === null)) { 241 // get milliseconds of time diff between server and client. positive means client ahead 242 lt.localTimeOffsetRequested = true; 243 lt.localTimeOffset = 0; 244 245 var storedOffset = null; 246 if (window.sessionStorage) { 247 storedOffset = window.sessionStorage.getItem('jlivetime-localtimeoffset'); 248 } else { 249 try { 250 storedOffset = /localtimeoffset=([0-9]+)[^0-9]?/i.exec(document.cookie)[1]; 251 } catch (err) { 252 storedOffset = null; 253 } 254 } 255 256 if (typeof storedOffset !='undefined' && storedOffset !== null) { 257 lt.localTimeOffset = parseInt(storedOffset, 10); 258 } else { 259 // offset not obtained yet, calculate with an ajax request 260 $.ajax({
261 url: options.serverTimeUrl, 262 cache: false, 263 success: function (data, status, req) { 264 // local time 265 now = new Date(); 266 // try to get server time from response Date header 267 dateHeader = req.getResponseHeader('Date'); 268 var serverTime = new Date(dateHeader); 269 // milliseconds of time diff between server and client. positive means client ahead 270 lt.localTimeOffset = now.getTime() - serverTime.getTime(); 271 // save offset for this browser session 272 if (window.sessionStorage) { 273 window.sessionStorage.setItem('jlivetime-localtimeoffset', lt.localTimeOffset); 274 } else { 275 document.cookie = 'localtimeoffset='+lt.localTimeOffset; 276 } 277 if (lt.localTimeOffset > 10000) { 278 log('WARNING: local time offset is ' + Math.round(lt.localTimeOffset / 1000) + 's'); 279 } 280 } 281 }); 282 } 283 } 284 }; 285 286 lt.millisecondsFromNow = function(date){ 287 var ts = date; 288 if (date instanceof Date){ 289 ts = (date.getTime() + date.getTimezoneOffset()*60*1000); 290 } 291 return lt.now() - ts; 292 }; 293 294 lt.refresh = function(element) { 295 296 var start = new Date().getTime(); 297 298 var root = $(element); 299 root.addClass('jlivetime-active'); 300 301 // if there's a pending refresh, cancel it 302 var timerTimeout = root.data('jlivetime-timeout'); 303 if (typeof timerTimeout !== 'undefined'){ 304 clearTimeout(timerTimeout); 305 } 306 307 var nextRefreshMs = 60000; 308 var timestamps = root.find(options.datetimeSelector); 309 if (root.is(options.datetimeSelector)) { 310 timestamps = timestamps.add(root); 311 } 312 timestamps.each(function(){ 313 var tsNextRefreshMs = 60000; 314 var tsElem = $(this); 315 var tsString = tsElem.attr(options.datetimeAttribute); 316 if (tsString) { 317 var ts; 318 try { 319 if (tsString.indexOf('-') > 0) { 320 var date = lt.parseDate(tsString); 321 ts = date.getTime() - date.getTimezoneOffset() * 60000; 322 } else { 323 ts = parseInt(tsString, 10); 324 } 325 } catch (err) { 326 // error parsing timestamp 327 log('error parsing timestamp: '+err); 328 ts = 0; 329 } 330 if (ts > 0) { 331 var timeDiff = lt.millisecondsFromNow(ts); 332 var duration = 0; 333 try { 334 var durationString = tsElem.attr(options.durationAttribute); 335 if (durationString) { 336 duration = parseInt(durationString, 10); 337 } 338 } catch (err) { 339 // error parsing timestamp 340 log('error parsing duration: '+err); 341 duration = 0; 342 } 343 tsElem.data('time-diff', timeDiff); 344 var labels = tsElem.find(options.dateLabelSelector); 345 if (tsElem.is(options.dateLabelSelector)) { 346 labels = labels.add(tsElem); 347 } 348 labels.each(function() { 349 var label = $(this); 350 var htmlChanged = false; 351 var tooltipChanged = false; 352 var formatResult; 353 try { 354 formatResult = lt.format(ts, timeDiff, duration, label.data('time-label') || '#_default'); 355 } catch (error) { 356 log('error updating time label: ' + error); 357 formatResult = { value: '-', nextRefreshMs: 60000 }; 358 } 359 if (formatResult.value !== null && typeof formatResult.value !== 'undefined') { 360 if (label.html()!==formatResult.value) { 361 label.html(formatResult.value.replace(':', '').replace(':', '')); 362 htmlChanged = true; 363 } 364 } 365 if (formatResult.nextRefreshMs) { 366 // next refresh detected when formatting (eg. when format rule will change) 367 tsNextRefreshMs = Math.min(tsNextRefreshMs, formatResult.nextRefreshMs); 368 } 369 if (typeof label.data('time-tooltip') !== 'undefined') { 370 formatResult = lt.format(ts, timeDiff, duration, label.data('time-tooltip') || '#_default_tooltip'); 371 if (formatResult.value !== null && typeof formatResult.value !== 'undefined') { 372 if (label.attr('title') !== formatResult.value) { 373 label.attr('title', formatResult.value); 374 if (typeof label.attr('data-original-title') != 'undefined') { 375 label.attr('data-original-title', formatResult.value); 376 } 377 tooltipChanged = true; 378 } 379 } 380 if (formatResult.nextRefreshMs) { 381 // next refresh detected when formatting (eg. when format rule will change) 382 tsNextRefreshMs = Math.min(tsNextRefreshMs, formatResult.nextRefreshMs); 383 } 384 } 385 386 if ((htmlChanged || tooltipChanged) && options.triggerRefreshComplete) { 387 label.trigger('refreshComplete', { 388 nextRefreshMs: tsNextRefreshMs, 389 htmlChanged: htmlChanged, 390 tooltipChanged: tooltipChanged, 391 refreshElapsedTime: new Date().getTime() - start 392 }); 393 } 394 }); 395 if (timeDiff < 0){ 396 // refresh when time comes 397 tsNextRefreshMs = Math.min(tsNextRefreshMs, -timeDiff); 398 } 399 nextRefreshMs = Math.min(nextRefreshMs, tsNextRefreshMs); 400 } 401 } 402 tsElem.data('jlivetime-nextrefresh',new Date().getTime()+nextRefreshMs); 403 }); 404 nextRefreshMs = Math.max(nextRefreshMs, 65); 405 406 // schedule the next refresh 407 root.data('jlivetime-timeout', setTimeout(function(){ 408 lt.refresh(element); 409 }, nextRefreshMs)); 410 }; 411 412 lt.disable = function(element) { 413 var root = $(element); 414
415 // if there's a pending refresh, cancel it 416 var timerTimeout = root.data('jlivetime-timeout'); 417 if (typeof timerTimeout !== 'undefined'){ 418 clearTimeout(timerTimeout); 419 } 420 root.removeClass('jlivetime-active'); 421 }; 422 423 lt.formatPart = function(ts, timeDiff, format){ 424 if (format==='[]'){ 425 return {value: ''}; 426 } 427 var match = /^(t?d_)?(([yMdewHhmsft])\3*)$/.exec(format); 428 if (match) { 429 if (!match[1]) { 430 // regular date format 431 var date = new Date(ts + new Date(ts).getTimezoneOffset()*60*1000); 432 switch (match[3]) { 433 case 'y': 434 return { 435 value: padLeft(date.getFullYear(), match[2].length) 436 }; 437 case 'M': 438 // use name if length > 2 439 var month = match[2].length > 2 ? unitNames.M[date.getMonth()] : padLeft(date.getMonth() + 1, match[2].length); 440 if (match[2].length === 3) { 441 month = month.slice(0,3); 442 } 443 return { 444 value: month 445 }; 446 case 'w': 447 return { 448 value: padLeft(Math.floor(date.getDate() / 7) + 1, match[2].length) 449 }; 450 case 'd': 451 return { 452 value: padLeft(date.getDate(), match[2].length) 453 }; 454 case 'e': 455 // use name if length > 1 456 var weekday = match[2].length > 1 ? unitNames.e[date.getDay()] : date.getDay() + 1; 457 if (match[2].length > 1 && match[2].length <= 3) { 458 weekday = weekday.slice(0,match[2].length); 459 } 460 return { 461 value: weekday 462 }; 463 case 'h': 464 return { 465 value: padLeft(date.getHours() > 12 ? date.getHours() - 12 : 466 (date.getHours() === 0 ? 12 : date.getHours()), match[2].length) 467 }; 468 case 'H': 469 return { 470 value: padLeft(date.getHours(), match[2].length) 471 }; 472 case 'm': 473 return { 474 value: padLeft(date.getMinutes(), match[2].length) 475 }; 476 case 's': 477 return { 478 value: padLeft(date.getSeconds(), match[2].length) 479 }; 480 case 'f': 481 return { 482 value: padLeft(date.getMilliseconds(), match[2].length) 483 }; 484 case 't': 485 var ampm = date.getHours() >= 12 ? 'pm' : 'am'; 486 return { 487 value: match[2].length > 1 ? ampm : ampm.slice(0,1) 488 }; 489 } 490 } else { 491 var timeUnit = timeUnits[match[3]]; 492 if (timeUnit) { 493 var nextRefreshMs; 494 // time difference format 495 nextRefreshMs = Math.max(65, timeDiff > 0 ? timeUnit - timeDiff % timeUnit : -timeDiff % timeUnit); 496 if (match[1] == 'td_') { 497 // total in unit 498 return { 499 value: padLeft(Math.floor(Math.abs(timeDiff / timeUnit)), match[2].length), 500 nextRefreshMs: nextRefreshMs 501 }; 502 } else if (match[1] == 'd_') { 503 var parentTimeUnit = timeUnits[parentTimeUnits[match[3]]]; 504 return { 505 value: padLeft(Math.floor(Math.abs(parentTimeUnit ? timeDiff % parentTimeUnit : timeDiff) / timeUnit), match[2].length), 506 nextRefreshMs: nextRefreshMs 507 }; 508 } 509 } 510 } 511 } 512 return { 513 value: format 514 }; 515 }; 516 517 lt.getFormatExpression = function(ts, timeDiff, duration, format) { 518 var nextRefreshMs = 60000; 519 var format_expression = format; 520 if (format_expression === null || format_expression === 'null') { 521 format_expression = null; 522 } else { 523 if (typeof format_expression == 'string' && format_expression.slice(0,1) === '#') { 524 format_expression = options.formats[format_expression.slice(1)]; 525 if (typeof format_expression == 'undefined') { 526 throw new Error('time format not found: ' + format); 527 } 528 } 529 var localTs = ts + new Date().getTimezoneOffset() * 60 * 1000; 530 var dayStartLimit, dayEndLimit; 531 if (format_expression instanceof Array) { 532 var fmtLength = format_expression.length; 533 for (var i=0; i< fmtLength; i++) { 534 if (format_expression[i].length === 1) { 535 format_expression = format_expression[i][0]; 536 break; 537 } else { 538 var rangeLimit = format_expression[i][0]; 539 if (typeof rangeLimit == 'string') { 540 var limitParameters = /^([\w_]+)([+\-]\d+)?/i.exec(rangeLimit); 541 if (!limitParameters) { 542 log('error parsing format range: ' + rangeLimit); 543 return { 544 nextRefreshMs: 1000, 545 format_expression: '-' 546 } 547 } 548 var limitKeyword = limitParameters[1]; 549 if (limitKeyword === 'end') { 550 rangeLimit = duration; 551 } else if (limitKeyword === 'daystart') { 552 if (!dayStartLimit) { 553 dayStartLimit = new Date(localTs).setHours(0, 0, 0, 0) - localTs; 554 } 555 rangeLimit = dayStartLimit; 556 }
556 else if (limitKeyword === 'dayend') { 557 if (!dayEndLimit) { 558 dayEndLimit = new Date(localTs).setHours(23, 59, 59, 999) - localTs; 559 } 560 rangeLimit = dayEndLimit; 561 } else { 562 log('error parsing format range, unknown keyword: ' + limitKeyword); 563 return { 564 nextRefreshMs: 1000, 565 format_expression: '-' 566 } 567 } 568 if (limitParameters[2]) { 569 rangeLimit += parseInt(limitParameters[2], 10) * 1000; 570 } 571 } else { 572 rangeLimit = rangeLimit * 1000; 573 } 574 if (rangeLimit >= timeDiff) { 575 // refresh when format range changes 576 nextRefreshMs = Math.min(nextRefreshMs, rangeLimit - timeDiff); 577 format_expression = format_expression[i][1]; 578 break; 579 } 580 } 581 } 582 if (format_expression instanceof Array) { 583 format_expression = '<unknown>'; 584 } 585 } 586 if (typeof format_expression == 'string' && format_expression.slice(0,1) === '#') { 587 return lt.getFormatExpression(ts, timeDiff, duration, format_expression); 588 } 589 } 590 return { 591 expression: format_expression, 592 nextRefreshMs: nextRefreshMs 593 }; 594 }; 595 596 lt.format = function(ts, timeDiff, duration, format){ 597 var partRegex = /\[?([a-z_]+)\]?/gim; 598 var value = []; 599 var lastindex = 0; 600 var fmt = lt.getFormatExpression(ts, timeDiff, duration, format); 601 602 if (fmt.expression === null) { 603 return fmt; 604 } 605 var match = partRegex.exec(fmt.expression); 606 while (match) { 607 value.push(fmt.expression.slice(lastindex, match.index)); 608 if (match[0].length != match[1].length){ 609 value.push(match[1]); 610 }else{ 611 var format_expression = match[0]; 612 var ts_offset = 0; 613 if (format_expression.slice(0,4) === 'end_') { 614 format_expression = format_expression.slice(4); 615 ts_offset = duration; 616 } 617 var formatPartResult = lt.formatPart(ts + ts_offset, timeDiff - ts_offset, format_expression); 618 value.push(formatPartResult.value); 619 if (formatPartResult.nextRefreshMs){ 620 fmt.nextRefreshMs = Math.min(fmt.nextRefreshMs, formatPartResult.nextRefreshMs); 621 } 622 } 623 lastindex = match.index + match[0].length; 624 match = partRegex.exec(fmt.expression); 625 } 626 value.push(fmt.expression.slice(lastindex)); 627 628 fmt.value = value.join(''); 629 return fmt; 630 }; 631 632 $.fn.livetime = function(enable){ 633 this.each(function(){ 634 var elem = $(this); 635 if (!elem.is('.jlivetime-active')) { 636 // if element is an active livetime container, call livetime() on that container 637 var activeParent = $(this).parent('.jlivetime-active'); 638 if (activeParent.length) { 639 elem = activeParent; 640 } 641 } 642 if (enable === false) { 643 lt.disable(elem.get(0)); 644 } else { 645 lt.refresh(elem.get(0)); 646 } 647 }); 648 }; 649 650 $(function(){ 651 if (lt.options.autostart) { 652 $(lt.options.autostart.selector || 'body').livetime(); 653 } 654 }); 655 656})(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.