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'];
9 return ('0' + num).slice(-2);
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) {
22 var modifier = _arg[1];
27 return weekdays[day].slice(0, 3);
31 return months[month].slice(0, 3);
35 return time.toString();
43 return pad(strftime(time, '%l'));
45 if (hour === 0 || hour === 12) {
48 return (hour + 12) % 12;
52 return pad(month + 1);
74 return pad(year % 100);
78 match = time.toString().match(/\((\w+)\)$/);
79 return match ? match[1] : '';
81 match = time.toString().match(/\w([+-]\d\d\d\d) /);
82 return match ? match[1] : '';
87 function RelativeTime(date) {
91 RelativeTime.prototype.toString = function() {
92 var ago = this.timeElapsed();
96 return 'on ' + this.formatDate();
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);
108 } else if (sec < 10) {
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) {
122 } else if (day < 30) {
123 return day + ' days ago';
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);
139 } else if (sec < 10) {
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) {
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) {
162 return year + ' years ago';
166 RelativeTime.prototype.microTimeAgo = function() {
167 var ms = new Date().getTime() - this.date.getTime();
172 var month = day / 30;
173 var year = month / 12;
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';
183 return Math.round(year) + 'y';
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`
191 // Returns true if the day appears before the month.
192 function isDayFirst() {
193 if (dayFirst !== null) {
197 if (!('Intl' in window)) {
201 var options = {day: 'numeric', month: 'short'};
202 var formatter = new window.Intl.DateTimeFormat(undefined, options);
203 var output = formatter.format(new Date(0));
205 dayFirst = !!output.match(/^\d/);
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.
213 // Returns true if the date needs a separator.
214 function isYearSeparator() {
215 if (yearSeparator !== null) {
216 return yearSeparator;
219 if (!('Intl' in window)) {
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));
227 yearSeparator = !!output.match(/\d,/);
228 return yearSeparator;
230 var yearSeparator = null;
232 // Private: Determine if the date occurs in the same year as today's date.
234 // date - The Date to test.
236 // Returns true if it's this year.
237 function isThisYear(date) {
238 var now = new Date();
239 return now.getUTCFullYear() === date.getUTCFullYear();
242 RelativeTime.prototype.formatDate = function() {
243 var format = isDayFirst() ? '%e %b' : '%b %e';
244 if (!isThisYear(this.date)) {
245 format += isYearSeparator() ? ', %Y': ' %Y';
247 return strftime(this.date, format);
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);
255 return strftime(this.date, '%l:%M%P');
260 // Internal: Array tracking all elements attached to the document that need
261 // to be updated every minute.
262 var nowElements = [];
264 // Internal: Timer ID for `updateNowElements` interval.
265 var updateNowElementsId;
267 // Internal: Install a timer to refresh all attached relative-time elements every
269 function updateNowElements() {
271 for (i = 0, len = nowElements.length; i < len; i++) {
272 time = nowElements[i];
273 time.textContent = time.getFormattedDate();
278 var ExtendedTimePrototype;
279 if ('HTMLTimeElement' in window) {
280 ExtendedTimePrototype = Object.create(window.HTMLTimeElement.prototype);
282 ExtendedTimePrototype = Object.create(window.HTMLElement.prototype);
285 // Internal: Refresh the time element's formatted date when an attribute changes.
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);
294 var title = this.getFormattedTitle();
296 this.setAttribute('title', title);
299 var text = this.getFormattedDate();
301 this.textContent = text;
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.
309 // Returns a formatted time String.
310 ExtendedTimePrototype.getFormattedTitle = function() {
315 if (this.hasAttribute('title')) {
316 return this.getAttribute('title');
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);
325 return this._date.toLocaleString();
329 var RelativeTimePrototype = Object.create(ExtendedTimePrototype);
331 RelativeTimePrototype.createdCallback = function() {
332 var value = this.getAttribute('datetime');
334 this.attributeChangedCallback('datetime', null, value);
338 RelativeTimePrototype.getFormattedDate = function() {
340 return new RelativeTime(this._date).toString();
344 RelativeTimePrototype.attachedCallback = function() {
345 nowElements.push(this);
347 if (!updateNowElementsId) {
349 updateNowElementsId = setInterval(updateNowElements, 60 * 1000);
353 RelativeTimePrototype.detachedCallback = function() {
354 var ix = nowElements.indexOf(this);
356 nowElements.splice(ix, 1);
359 if (!nowElements.length) {
360 if (updateNowElementsId) {
361 clearInterval(updateNowElementsId);
362 updateNowElementsId = null;
367 var TimeAgoPrototype = Object.create(RelativeTimePrototype);
368 TimeAgoPrototype.getFormattedDate = function() {
370 var format = this.getAttribute('format');
371 if (format === 'micro') {
372 return new RelativeTime(this._date).microTimeAgo();
374 return new RelativeTime(this._date).timeAgo();
380 var LocalTimePrototype = Object.create(ExtendedTimePrototype);
382 LocalTimePrototype.createdCallback = function() {
384 if (value = this.getAttribute('datetime')) {
385 this.attributeChangedCallback('datetime', null, value);
387 if (value = this.getAttribute('format')) {
388 this.attributeChangedCallback('format', null, value);
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.
397 // Supported attributes are:
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"
407 // Returns a formatted time String.
408 LocalTimePrototype.getFormattedDate = function() {
413 var date = formatDate(this) || '';
414 var time = formatTime(this) || '';
415 return (date + ' ' + time).trim();
418 // Private: Format a date according to the `weekday`, `day`, `month`,
419 // and `year` attribute values.
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.
425 // el - The local-time element to format.
427 // Returns a date String or null if no date formats are provided.
428 function formatDate(el) {
429 // map attribute values to strftime
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 || '');
456 // clean up year separator comma
457 format = format.replace(/(\s,)|(,\s$)/, '');
459 // squeeze spaces from final string
460 return strftime(el._date, format).replace(/\s+/, ' ').trim();
463 // Private: Format a time according to the `hour`, `minute`, and `second`
466 // el - The local-time element to format.
468 // Returns a time String or null if no time formats are provided.
469 function formatTime(el) {
470 // retrieve format settings from attributes
472 hour: el.getAttribute('hour'),
473 minute: el.getAttribute('minute'),
474 second: el.getAttribute('second')
477 // remove unset format attributes
478 for (var opt in options) {
484 // no time format attributes provided
485 if (Object.keys(options).length === 0) {
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);
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);
500 // Public: RelativeTimeElement constructor.
502 // var time = new RelativeTimeElement()
503 // # => <time is='relative-time'></time>
505 window.RelativeTimeElement = document.registerElement('relative-time', {
506 prototype: RelativeTimePrototype,
510 window.TimeAgoElement = document.registerElement('time-ago', {
511 prototype: TimeAgoPrototype,
515 // Public: LocalTimeElement constructor.
517 // var time = new LocalTimeElement()
518 // # => <time is='local-time'></time>
520 window.LocalTimeElement = document.registerElement('local-time', {
521 prototype: LocalTimePrototype,