59f117f33955f1539fc1309885d063436deaeaf1
[motion.git] / public / bower_components / time-elements / time-elements.js
1 (function() {
2   'use strict';
3
4   // Shout out to https://github.com/basecamp/local_time/blob/master/app/assets/javascripts/local_time.js.coffee
5   var weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
6   var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
7
8   function pad(num) {
9     return ('0' + num).slice(-2);
10   }
11
12   function strftime(time, formatString) {
13     var day = time.getDay();
14     var date = time.getDate();
15     var month = time.getMonth();
16     var year = time.getFullYear();
17     var hour = time.getHours();
18     var minute = time.getMinutes();
19     var second = time.getSeconds();
20     return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function(_arg) {
21       var match;
22       var modifier = _arg[1];
23       switch (modifier) {
24         case '%':
25           return '%';
26         case 'a':
27           return weekdays[day].slice(0, 3);
28         case 'A':
29           return weekdays[day];
30         case 'b':
31           return months[month].slice(0, 3);
32         case 'B':
33           return months[month];
34         case 'c':
35           return time.toString();
36         case 'd':
37           return pad(date);
38         case 'e':
39           return date;
40         case 'H':
41           return pad(hour);
42         case 'I':
43           return pad(strftime(time, '%l'));
44         case 'l':
45           if (hour === 0 || hour === 12) {
46             return 12;
47           } else {
48             return (hour + 12) % 12;
49           }
50           break;
51         case 'm':
52           return pad(month + 1);
53         case 'M':
54           return pad(minute);
55         case 'p':
56           if (hour > 11) {
57             return 'PM';
58           } else {
59             return 'AM';
60           }
61           break;
62         case 'P':
63           if (hour > 11) {
64             return 'pm';
65           } else {
66             return 'am';
67           }
68           break;
69         case 'S':
70           return pad(second);
71         case 'w':
72           return day;
73         case 'y':
74           return pad(year % 100);
75         case 'Y':
76           return year;
77         case 'Z':
78           match = time.toString().match(/\((\w+)\)$/);
79           return match ? match[1] : '';
80         case 'z':
81           match = time.toString().match(/\w([+-]\d\d\d\d) /);
82           return match ? match[1] : '';
83       }
84     });
85   }
86
87   function RelativeTime(date) {
88     this.date = date;
89   }
90
91   RelativeTime.prototype.toString = function() {
92     var ago = this.timeElapsed();
93     if (ago) {
94       return ago;
95     } else {
96       return 'on ' + this.formatDate();
97     }
98   };
99
100   RelativeTime.prototype.timeElapsed = function() {
101     var ms = new Date().getTime() - this.date.getTime();
102     var sec = Math.round(ms / 1000);
103     var min = Math.round(sec / 60);
104     var hr = Math.round(min / 60);
105     var day = Math.round(hr / 24);
106     if (ms < 0) {
107       return 'just now';
108     } else if (sec < 10) {
109       return 'just now';
110     } else if (sec < 45) {
111       return sec + ' seconds ago';
112     } else if (sec < 90) {
113       return 'a minute ago';
114     } else if (min < 45) {
115       return min + ' minutes ago';
116     } else if (min < 90) {
117       return 'an hour ago';
118     } else if (hr < 24) {
119       return hr + ' hours ago';
120     } else if (hr < 36) {
121       return 'a day ago';
122     } else if (day < 30) {
123       return day + ' days ago';
124     } else {
125       return null;
126     }
127   };
128
129   RelativeTime.prototype.timeAgo = function() {
130     var ms = new Date().getTime() - this.date.getTime();
131     var sec = Math.round(ms / 1000);
132     var min = Math.round(sec / 60);
133     var hr = Math.round(min / 60);
134     var day = Math.round(hr / 24);
135     var month = Math.round(day / 30);
136     var year = Math.round(month / 12);
137     if (ms < 0) {
138       return 'just now';
139     } else if (sec < 10) {
140       return 'just now';
141     } else if (sec < 45) {
142       return sec + ' seconds ago';
143     } else if (sec < 90) {
144       return 'a minute ago';
145     } else if (min < 45) {
146       return min + ' minutes ago';
147     } else if (min < 90) {
148       return 'an hour ago';
149     } else if (hr < 24) {
150       return hr + ' hours ago';
151     } else if (hr < 36) {
152       return 'a day ago';
153     } else if (day < 30) {
154       return day + ' days ago';
155     } else if (day < 45) {
156       return 'a month ago';
157     } else if (month < 12) {
158       return month + ' months ago';
159     } else if (month < 18) {
160         return 'a year ago';
161     } else {
162       return year + ' years ago';
163     }
164   };
165
166   RelativeTime.prototype.microTimeAgo = function() {
167     var ms = new Date().getTime() - this.date.getTime();
168     var sec = ms / 1000;
169     var min = sec / 60;
170     var hr = min / 60;
171     var day = hr / 24;
172     var month = day / 30;
173     var year = month / 12;
174     if (min < 1) {
175       return '1m';
176     } else if (min < 60) {
177       return Math.round(min) + 'm';
178     } else if (hr < 24) {
179       return Math.round(hr) + 'h';
180     } else if (day < 365) {
181       return Math.round(day) + 'd';
182     } else {
183       return Math.round(year) + 'y';
184     }
185   };
186
187   // Private: Determine if the day should be formatted before the month name in
188   // the user's current locale. For example, `9 Jun` for en-GB and `Jun 9`
189   // for en-US.
190   //
191   // Returns true if the day appears before the month.
192   function isDayFirst() {
193     if (dayFirst !== null) {
194       return dayFirst;
195     }
196
197     if (!('Intl' in window)) {
198       return false;
199     }
200
201     var options = {day: 'numeric', month: 'short'};
202     var formatter = new window.Intl.DateTimeFormat(undefined, options);
203     var output = formatter.format(new Date(0));
204
205     dayFirst = !!output.match(/^\d/);
206     return dayFirst;
207   }
208   var dayFirst = null;
209
210   // Private: Determine if the year should be separated from the month and day
211   // with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US.
212   //
213   // Returns true if the date needs a separator.
214   function isYearSeparator() {
215     if (yearSeparator !== null) {
216       return yearSeparator;
217     }
218
219     if (!('Intl' in window)) {
220       return true;
221     }
222
223     var options = {day: 'numeric', month: 'short', year: 'numeric'};
224     var formatter = new window.Intl.DateTimeFormat(undefined, options);
225     var output = formatter.format(new Date(0));
226
227     yearSeparator = !!output.match(/\d,/);
228     return yearSeparator;
229   }
230   var yearSeparator = null;
231
232   // Private: Determine if the date occurs in the same year as today's date.
233   //
234   // date - The Date to test.
235   //
236   // Returns true if it's this year.
237   function isThisYear(date) {
238     var now = new Date();
239     return now.getUTCFullYear() === date.getUTCFullYear();
240   }
241
242   RelativeTime.prototype.formatDate = function() {
243     var format = isDayFirst() ? '%e %b' : '%b %e';
244     if (!isThisYear(this.date)) {
245       format += isYearSeparator() ? ', %Y': ' %Y';
246     }
247     return strftime(this.date, format);
248   };
249
250   RelativeTime.prototype.formatTime = function() {
251     if ('Intl' in window) {
252       var formatter = new window.Intl.DateTimeFormat(undefined, {hour: 'numeric', minute: '2-digit'});
253       return formatter.format(this.date);
254     } else {
255       return strftime(this.date, '%l:%M%P');
256     }
257   };
258
259
260   // Internal: Array tracking all elements attached to the document that need
261   // to be updated every minute.
262   var nowElements = [];
263
264   // Internal: Timer ID for `updateNowElements` interval.
265   var updateNowElementsId;
266
267   // Internal: Install a timer to refresh all attached relative-time elements every
268   // minute.
269   function updateNowElements() {
270     var time, i, len;
271     for (i = 0, len = nowElements.length; i < len; i++) {
272       time = nowElements[i];
273       time.textContent = time.getFormattedDate();
274     }
275   }
276
277
278   var ExtendedTimePrototype;
279   if ('HTMLTimeElement' in window) {
280     ExtendedTimePrototype = Object.create(window.HTMLTimeElement.prototype);
281   } else {
282     ExtendedTimePrototype = Object.create(window.HTMLElement.prototype);
283   }
284
285   // Internal: Refresh the time element's formatted date when an attribute changes.
286   //
287   // Returns nothing.
288   ExtendedTimePrototype.attributeChangedCallback = function(attrName, oldValue, newValue) {
289     if (attrName === 'datetime') {
290       var millis = Date.parse(newValue);
291       this._date = isNaN(millis) ? null : new Date(millis);
292     }
293
294     var title = this.getFormattedTitle();
295     if (title) {
296       this.setAttribute('title', title);
297     }
298
299     var text = this.getFormattedDate();
300     if (text) {
301       this.textContent = text;
302     }
303   };
304
305   // Internal: Format the ISO 8601 timestamp according to the user agent's
306   // locale-aware formatting rules. The element's existing `title` attribute
307   // value takes precedence over this custom format.
308   //
309   // Returns a formatted time String.
310   ExtendedTimePrototype.getFormattedTitle = function() {
311     if (!this._date) {
312       return;
313     }
314
315     if (this.hasAttribute('title')) {
316       return this.getAttribute('title');
317     }
318
319     if ('Intl' in window) {
320       var options = {day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short'};
321       var formatter = new window.Intl.DateTimeFormat(undefined, options);
322       return formatter.format(this._date);
323     }
324
325     return this._date.toLocaleString();
326   };
327
328
329   var RelativeTimePrototype = Object.create(ExtendedTimePrototype);
330
331   RelativeTimePrototype.createdCallback = function() {
332     var value = this.getAttribute('datetime');
333     if (value) {
334       this.attributeChangedCallback('datetime', null, value);
335     }
336   };
337
338   RelativeTimePrototype.getFormattedDate = function() {
339     if (this._date) {
340       return new RelativeTime(this._date).toString();
341     }
342   };
343
344   RelativeTimePrototype.attachedCallback = function() {
345     nowElements.push(this);
346
347     if (!updateNowElementsId) {
348       updateNowElements();
349       updateNowElementsId = setInterval(updateNowElements, 60 * 1000);
350     }
351   };
352
353   RelativeTimePrototype.detachedCallback = function() {
354     var ix = nowElements.indexOf(this);
355     if (ix !== -1) {
356       nowElements.splice(ix, 1);
357     }
358
359     if (!nowElements.length) {
360       if (updateNowElementsId) {
361         clearInterval(updateNowElementsId);
362         updateNowElementsId = null;
363       }
364     }
365   };
366
367   var TimeAgoPrototype = Object.create(RelativeTimePrototype);
368   TimeAgoPrototype.getFormattedDate = function() {
369     if (this._date) {
370       var format = this.getAttribute('format');
371       if (format === 'micro') {
372         return new RelativeTime(this._date).microTimeAgo();
373       } else {
374         return new RelativeTime(this._date).timeAgo();
375       }
376     }
377   };
378
379
380   var LocalTimePrototype = Object.create(ExtendedTimePrototype);
381
382   LocalTimePrototype.createdCallback = function() {
383     var value;
384     if (value = this.getAttribute('datetime')) {
385       this.attributeChangedCallback('datetime', null, value);
386     }
387     if (value = this.getAttribute('format')) {
388       this.attributeChangedCallback('format', null, value);
389     }
390   };
391
392   // Formats the element's date, in the user's current locale, according to
393   // the formatting attribute values. Values are not passed straight through to
394   // an Intl.DateTimeFormat instance so that weekday and month names are always
395   // displayed in English, for now.
396   //
397   // Supported attributes are:
398   //
399   //   weekday - "short", "long"
400   //   year    - "numeric", "2-digit"
401   //   month   - "short", "long"
402   //   day     - "numeric", "2-digit"
403   //   hour    - "numeric", "2-digit"
404   //   minute  - "numeric", "2-digit"
405   //   second  - "numeric", "2-digit"
406   //
407   // Returns a formatted time String.
408   LocalTimePrototype.getFormattedDate = function() {
409     if (!this._date) {
410       return;
411     }
412
413     var date = formatDate(this) || '';
414     var time = formatTime(this) || '';
415     return (date + ' ' + time).trim();
416   };
417
418   // Private: Format a date according to the `weekday`, `day`, `month`,
419   // and `year` attribute values.
420   //
421   // This doesn't use Intl.DateTimeFormat to avoid creating text in the user's
422   // language when the majority of the surrounding text is in English. There's
423   // currently no way to separate the language from the format in Intl.
424   //
425   // el - The local-time element to format.
426   //
427   // Returns a date String or null if no date formats are provided.
428   function formatDate(el) {
429     // map attribute values to strftime
430     var props = {
431       weekday: {
432         'short': '%a',
433         'long': '%A'
434       },
435       day: {
436         'numeric': '%e',
437         '2-digit': '%d'
438       },
439       month: {
440         'short': '%b',
441         'long': '%B'
442       },
443       year: {
444         'numeric': '%Y',
445         '2-digit': '%y'
446       }
447     };
448
449     // build a strftime format string
450     var format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year';
451     for (var prop in props) {
452       var value = props[prop][el.getAttribute(prop)];
453       format = format.replace(prop, value || '');
454     }
455
456     // clean up year separator comma
457     format = format.replace(/(\s,)|(,\s$)/, '');
458
459     // squeeze spaces from final string
460     return strftime(el._date, format).replace(/\s+/, ' ').trim();
461   }
462
463   // Private: Format a time according to the `hour`, `minute`, and `second`
464   // attribute values.
465   //
466   // el - The local-time element to format.
467   //
468   // Returns a time String or null if no time formats are provided.
469   function formatTime(el) {
470     // retrieve format settings from attributes
471     var options = {
472       hour: el.getAttribute('hour'),
473       minute: el.getAttribute('minute'),
474       second: el.getAttribute('second')
475     };
476
477     // remove unset format attributes
478     for (var opt in options) {
479       if (!options[opt]) {
480         delete options[opt];
481       }
482     }
483
484     // no time format attributes provided
485     if (Object.keys(options).length === 0) {
486       return;
487     }
488
489     // locale-aware formatting of 24 or 12 hour times
490     if ('Intl' in window) {
491       var formatter = new window.Intl.DateTimeFormat(undefined, options);
492       return formatter.format(el._date);
493     }
494
495     // fall back to strftime for non-Intl browsers
496     var timef = options.second ? '%H:%M:%S' : '%H:%M';
497     return strftime(el._date, timef);
498   }
499
500   // Public: RelativeTimeElement constructor.
501   //
502   //   var time = new RelativeTimeElement()
503   //   # => <time is='relative-time'></time>
504   //
505   window.RelativeTimeElement = document.registerElement('relative-time', {
506     prototype: RelativeTimePrototype,
507     'extends': 'time'
508   });
509
510   window.TimeAgoElement = document.registerElement('time-ago', {
511     prototype: TimeAgoPrototype,
512     'extends': 'time'
513   });
514
515   // Public: LocalTimeElement constructor.
516   //
517   //   var time = new LocalTimeElement()
518   //   # => <time is='local-time'></time>
519   //
520   window.LocalTimeElement = document.registerElement('local-time', {
521     prototype: LocalTimePrototype,
522     'extends': 'time'
523   });
524
525 })();