UNPKG

22.8 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3 typeof define === 'function' && define.amd ? define(['exports'], factory) :
4 (global = global || self, factory(global.TimeElements = {}));
5}(this, function (exports) { 'use strict';
6
7 const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
8 const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
9
10 function pad(num) {
11 return "0".concat(num).slice(-2);
12 }
13
14 function strftime(time, formatString) {
15 const day = time.getDay();
16 const date = time.getDate();
17 const month = time.getMonth();
18 const year = time.getFullYear();
19 const hour = time.getHours();
20 const minute = time.getMinutes();
21 const second = time.getSeconds();
22 return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function (_arg) {
23 let match;
24 const modifier = _arg[1];
25
26 switch (modifier) {
27 case '%':
28 return '%';
29
30 case 'a':
31 return weekdays[day].slice(0, 3);
32
33 case 'A':
34 return weekdays[day];
35
36 case 'b':
37 return months[month].slice(0, 3);
38
39 case 'B':
40 return months[month];
41
42 case 'c':
43 return time.toString();
44
45 case 'd':
46 return pad(date);
47
48 case 'e':
49 return String(date);
50
51 case 'H':
52 return pad(hour);
53
54 case 'I':
55 return pad(strftime(time, '%l'));
56
57 case 'l':
58 if (hour === 0 || hour === 12) {
59 return String(12);
60 } else {
61 return String((hour + 12) % 12);
62 }
63
64 case 'm':
65 return pad(month + 1);
66
67 case 'M':
68 return pad(minute);
69
70 case 'p':
71 if (hour > 11) {
72 return 'PM';
73 } else {
74 return 'AM';
75 }
76
77 case 'P':
78 if (hour > 11) {
79 return 'pm';
80 } else {
81 return 'am';
82 }
83
84 case 'S':
85 return pad(second);
86
87 case 'w':
88 return String(day);
89
90 case 'y':
91 return pad(year % 100);
92
93 case 'Y':
94 return String(year);
95
96 case 'Z':
97 match = time.toString().match(/\((\w+)\)$/);
98 return match ? match[1] : '';
99
100 case 'z':
101 match = time.toString().match(/\w([+-]\d\d\d\d) /);
102 return match ? match[1] : '';
103 }
104
105 return '';
106 });
107 }
108 function makeFormatter(options) {
109 let format;
110 return function () {
111 if (format) return format;
112
113 if ('Intl' in window) {
114 try {
115 format = new Intl.DateTimeFormat(undefined, options);
116 return format;
117 } catch (e) {
118 if (!(e instanceof RangeError)) {
119 throw e;
120 }
121 }
122 }
123 };
124 }
125 let dayFirst = null;
126 const dayFirstFormatter = makeFormatter({
127 day: 'numeric',
128 month: 'short'
129 }); // Private: Determine if the day should be formatted before the month name in
130 // the user's current locale. For example, `9 Jun` for en-GB and `Jun 9`
131 // for en-US.
132 //
133 // Returns true if the day appears before the month.
134
135 function isDayFirst() {
136 if (dayFirst !== null) {
137 return dayFirst;
138 }
139
140 const formatter = dayFirstFormatter();
141
142 if (formatter) {
143 const output = formatter.format(new Date(0));
144 dayFirst = !!output.match(/^\d/);
145 return dayFirst;
146 } else {
147 return false;
148 }
149 }
150 let yearSeparator = null;
151 const yearFormatter = makeFormatter({
152 day: 'numeric',
153 month: 'short',
154 year: 'numeric'
155 }); // Private: Determine if the year should be separated from the month and day
156 // with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US.
157 //
158 // Returns true if the date needs a separator.
159
160 function isYearSeparator() {
161 if (yearSeparator !== null) {
162 return yearSeparator;
163 }
164
165 const formatter = yearFormatter();
166
167 if (formatter) {
168 const output = formatter.format(new Date(0));
169 yearSeparator = !!output.match(/\d,/);
170 return yearSeparator;
171 } else {
172 return true;
173 }
174 } // Private: Determine if the date occurs in the same year as today's date.
175 //
176 // date - The Date to test.
177 //
178 // Returns true if it's this year.
179
180 function isThisYear(date) {
181 const now = new Date();
182 return now.getUTCFullYear() === date.getUTCFullYear();
183 }
184 function makeRelativeFormat(locale, options) {
185 if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) {
186 try {
187 // eslint-disable-next-line flowtype/no-flow-fix-me-comments
188 // $FlowFixMe: missing RelativeTimeFormat type
189 return new Intl.RelativeTimeFormat(locale, options);
190 } catch (e) {
191 if (!(e instanceof RangeError)) {
192 throw e;
193 }
194 }
195 }
196 } // Private: Get preferred Intl locale for a target element.
197 //
198 // Traverses parents until it finds an explicit `lang` other returns "default".
199
200 function localeFromElement(el) {
201 const container = el.closest('[lang]');
202
203 if (container instanceof HTMLElement && container.lang) {
204 return container.lang;
205 }
206
207 return 'default';
208 }
209
210 const datetimes = new WeakMap();
211 class ExtendedTimeElement extends HTMLElement {
212 static get observedAttributes() {
213 return ['datetime', 'day', 'format', 'lang', 'hour', 'minute', 'month', 'second', 'title', 'weekday', 'year'];
214 }
215
216 connectedCallback() {
217 const title = this.getFormattedTitle();
218
219 if (title && !this.hasAttribute('title')) {
220 this.setAttribute('title', title);
221 }
222
223 const text = this.getFormattedDate();
224
225 if (text) {
226 this.textContent = text;
227 }
228 } // Internal: Refresh the time element's formatted date when an attribute changes.
229
230
231 attributeChangedCallback(attrName, oldValue, newValue) {
232 if (attrName === 'datetime') {
233 const millis = Date.parse(newValue);
234
235 if (isNaN(millis)) {
236 datetimes.delete(this);
237 } else {
238 datetimes.set(this, new Date(millis));
239 }
240 }
241
242 const title = this.getFormattedTitle();
243
244 if (title && !this.hasAttribute('title')) {
245 this.setAttribute('title', title);
246 }
247
248 const text = this.getFormattedDate();
249
250 if (text) {
251 this.textContent = text;
252 }
253 }
254
255 get date() {
256 return datetimes.get(this);
257 } // Internal: Format the ISO 8601 timestamp according to the user agent's
258 // locale-aware formatting rules. The element's existing `title` attribute
259 // value takes precedence over this custom format.
260 //
261 // Returns a formatted time String.
262
263
264 getFormattedTitle() {
265 const date = this.date;
266 if (!date) return;
267 const formatter = titleFormatter();
268
269 if (formatter) {
270 return formatter.format(date);
271 } else {
272 try {
273 return date.toLocaleString();
274 } catch (e) {
275 if (e instanceof RangeError) {
276 return date.toString();
277 } else {
278 throw e;
279 }
280 }
281 }
282 }
283
284 getFormattedDate() {}
285
286 }
287 const titleFormatter = makeFormatter({
288 day: 'numeric',
289 month: 'short',
290 year: 'numeric',
291 hour: 'numeric',
292 minute: '2-digit',
293 timeZoneName: 'short'
294 });
295
296 const formatters = new WeakMap();
297 class LocalTimeElement extends ExtendedTimeElement {
298 attributeChangedCallback(attrName, oldValue, newValue) {
299 if (attrName === 'hour' || attrName === 'minute' || attrName === 'second' || attrName === 'time-zone-name') {
300 formatters.delete(this);
301 }
302
303 super.attributeChangedCallback(attrName, oldValue, newValue);
304 } // Formats the element's date, in the user's current locale, according to
305 // the formatting attribute values. Values are not passed straight through to
306 // an Intl.DateTimeFormat instance so that weekday and month names are always
307 // displayed in English, for now.
308 //
309 // Supported attributes are:
310 //
311 // weekday - "short", "long"
312 // year - "numeric", "2-digit"
313 // month - "short", "long"
314 // day - "numeric", "2-digit"
315 // hour - "numeric", "2-digit"
316 // minute - "numeric", "2-digit"
317 // second - "numeric", "2-digit"
318 //
319 // Returns a formatted time String.
320
321
322 getFormattedDate() {
323 const d = this.date;
324 if (!d) return;
325 const date = formatDate(this, d) || '';
326 const time = formatTime(this, d) || '';
327 return "".concat(date, " ").concat(time).trim();
328 }
329
330 } // Private: Format a date according to the `weekday`, `day`, `month`,
331 // and `year` attribute values.
332 //
333 // This doesn't use Intl.DateTimeFormat to avoid creating text in the user's
334 // language when the majority of the surrounding text is in English. There's
335 // currently no way to separate the language from the format in Intl.
336 //
337 // el - The local-time element to format.
338 //
339 // Returns a date String or null if no date formats are provided.
340
341 function formatDate(el, date) {
342 // map attribute values to strftime
343 const props = {
344 weekday: {
345 short: '%a',
346 long: '%A'
347 },
348 day: {
349 numeric: '%e',
350 '2-digit': '%d'
351 },
352 month: {
353 short: '%b',
354 long: '%B'
355 },
356 year: {
357 numeric: '%Y',
358 '2-digit': '%y'
359 } // build a strftime format string
360
361 };
362 let format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year';
363
364 for (const prop in props) {
365 const value = props[prop][el.getAttribute(prop)];
366 format = format.replace(prop, value || '');
367 } // clean up year separator comma
368
369
370 format = format.replace(/(\s,)|(,\s$)/, ''); // squeeze spaces from final string
371
372 return strftime(date, format).replace(/\s+/, ' ').trim();
373 } // Private: Format a time according to the `hour`, `minute`, and `second`
374 // attribute values.
375 //
376 // el - The local-time element to format.
377 //
378 // Returns a time String or null if no time formats are provided.
379
380
381 function formatTime(el, date) {
382 const options = {}; // retrieve format settings from attributes
383
384 const hour = el.getAttribute('hour');
385 if (hour === 'numeric' || hour === '2-digit') options.hour = hour;
386 const minute = el.getAttribute('minute');
387 if (minute === 'numeric' || minute === '2-digit') options.minute = minute;
388 const second = el.getAttribute('second');
389 if (second === 'numeric' || second === '2-digit') options.second = second;
390 const tz = el.getAttribute('time-zone-name');
391 if (tz === 'short' || tz === 'long') options.timeZoneName = tz; // No time format attributes provided.
392
393 if (Object.keys(options).length === 0) {
394 return;
395 }
396
397 let factory = formatters.get(el);
398
399 if (!factory) {
400 factory = makeFormatter(options);
401 formatters.set(el, factory);
402 }
403
404 const formatter = factory();
405
406 if (formatter) {
407 // locale-aware formatting of 24 or 12 hour times
408 return formatter.format(date);
409 } else {
410 // fall back to strftime for non-Intl browsers
411 const timef = options.second ? '%H:%M:%S' : '%H:%M';
412 return strftime(date, timef);
413 }
414 } // Public: LocalTimeElement constructor.
415 //
416 // var time = new LocalTimeElement()
417 // # => <local-time></local-time>
418 //
419
420
421 if (!window.customElements.get('local-time')) {
422 window.LocalTimeElement = LocalTimeElement;
423 window.customElements.define('local-time', LocalTimeElement);
424 }
425
426 class RelativeTime {
427 constructor(date, locale) {
428 this.date = date;
429 this.locale = locale;
430 }
431
432 toString() {
433 const ago = this.timeElapsed();
434
435 if (ago) {
436 return ago;
437 } else {
438 const ahead = this.timeAhead();
439
440 if (ahead) {
441 return ahead;
442 } else {
443 return "on ".concat(this.formatDate());
444 }
445 }
446 }
447
448 timeElapsed() {
449 const ms = new Date().getTime() - this.date.getTime();
450 const sec = Math.round(ms / 1000);
451 const min = Math.round(sec / 60);
452 const hr = Math.round(min / 60);
453 const day = Math.round(hr / 24);
454
455 if (ms >= 0 && day < 30) {
456 return this.timeAgoFromMs(ms);
457 } else {
458 return null;
459 }
460 }
461
462 timeAhead() {
463 const ms = this.date.getTime() - new Date().getTime();
464 const sec = Math.round(ms / 1000);
465 const min = Math.round(sec / 60);
466 const hr = Math.round(min / 60);
467 const day = Math.round(hr / 24);
468
469 if (ms >= 0 && day < 30) {
470 return this.timeUntil();
471 } else {
472 return null;
473 }
474 }
475
476 timeAgo() {
477 const ms = new Date().getTime() - this.date.getTime();
478 return this.timeAgoFromMs(ms);
479 }
480
481 timeAgoFromMs(ms) {
482 const sec = Math.round(ms / 1000);
483 const min = Math.round(sec / 60);
484 const hr = Math.round(min / 60);
485 const day = Math.round(hr / 24);
486 const month = Math.round(day / 30);
487 const year = Math.round(month / 12);
488
489 if (ms < 0) {
490 return formatRelativeTime(this.locale, 0, 'second');
491 } else if (sec < 10) {
492 return formatRelativeTime(this.locale, 0, 'second');
493 } else if (sec < 45) {
494 return formatRelativeTime(this.locale, -sec, 'second');
495 } else if (sec < 90) {
496 return formatRelativeTime(this.locale, -min, 'minute');
497 } else if (min < 45) {
498 return formatRelativeTime(this.locale, -min, 'minute');
499 } else if (min < 90) {
500 return formatRelativeTime(this.locale, -hr, 'hour');
501 } else if (hr < 24) {
502 return formatRelativeTime(this.locale, -hr, 'hour');
503 } else if (hr < 36) {
504 return formatRelativeTime(this.locale, -day, 'day');
505 } else if (day < 30) {
506 return formatRelativeTime(this.locale, -day, 'day');
507 } else if (month < 12) {
508 return formatRelativeTime(this.locale, -month, 'month');
509 } else if (month < 18) {
510 return formatRelativeTime(this.locale, -year, 'year');
511 } else {
512 return formatRelativeTime(this.locale, -year, 'year');
513 }
514 }
515
516 microTimeAgo() {
517 const ms = new Date().getTime() - this.date.getTime();
518 const sec = Math.round(ms / 1000);
519 const min = Math.round(sec / 60);
520 const hr = Math.round(min / 60);
521 const day = Math.round(hr / 24);
522 const month = Math.round(day / 30);
523 const year = Math.round(month / 12);
524
525 if (min < 1) {
526 return '1m';
527 } else if (min < 60) {
528 return "".concat(min, "m");
529 } else if (hr < 24) {
530 return "".concat(hr, "h");
531 } else if (day < 365) {
532 return "".concat(day, "d");
533 } else {
534 return "".concat(year, "y");
535 }
536 }
537
538 timeUntil() {
539 const ms = this.date.getTime() - new Date().getTime();
540 return this.timeUntilFromMs(ms);
541 }
542
543 timeUntilFromMs(ms) {
544 const sec = Math.round(ms / 1000);
545 const min = Math.round(sec / 60);
546 const hr = Math.round(min / 60);
547 const day = Math.round(hr / 24);
548 const month = Math.round(day / 30);
549 const year = Math.round(month / 12);
550
551 if (month >= 18) {
552 return formatRelativeTime(this.locale, year, 'year');
553 } else if (month >= 12) {
554 return formatRelativeTime(this.locale, year, 'year');
555 } else if (day >= 45) {
556 return formatRelativeTime(this.locale, month, 'month');
557 } else if (day >= 30) {
558 return formatRelativeTime(this.locale, month, 'month');
559 } else if (hr >= 36) {
560 return formatRelativeTime(this.locale, day, 'day');
561 } else if (hr >= 24) {
562 return formatRelativeTime(this.locale, day, 'day');
563 } else if (min >= 90) {
564 return formatRelativeTime(this.locale, hr, 'hour');
565 } else if (min >= 45) {
566 return formatRelativeTime(this.locale, hr, 'hour');
567 } else if (sec >= 90) {
568 return formatRelativeTime(this.locale, min, 'minute');
569 } else if (sec >= 45) {
570 return formatRelativeTime(this.locale, min, 'minute');
571 } else if (sec >= 10) {
572 return formatRelativeTime(this.locale, sec, 'second');
573 } else {
574 return formatRelativeTime(this.locale, 0, 'second');
575 }
576 }
577
578 microTimeUntil() {
579 const ms = this.date.getTime() - new Date().getTime();
580 const sec = Math.round(ms / 1000);
581 const min = Math.round(sec / 60);
582 const hr = Math.round(min / 60);
583 const day = Math.round(hr / 24);
584 const month = Math.round(day / 30);
585 const year = Math.round(month / 12);
586
587 if (day >= 365) {
588 return "".concat(year, "y");
589 } else if (hr >= 24) {
590 return "".concat(day, "d");
591 } else if (min >= 60) {
592 return "".concat(hr, "h");
593 } else if (min > 1) {
594 return "".concat(min, "m");
595 } else {
596 return '1m';
597 }
598 }
599
600 formatDate() {
601 let format = isDayFirst() ? '%e %b' : '%b %e';
602
603 if (!isThisYear(this.date)) {
604 format += isYearSeparator() ? ', %Y' : ' %Y';
605 }
606
607 return strftime(this.date, format);
608 }
609
610 formatTime() {
611 const formatter = timeFormatter();
612
613 if (formatter) {
614 return formatter.format(this.date);
615 } else {
616 return strftime(this.date, '%l:%M%P');
617 }
618 }
619
620 }
621
622 function formatRelativeTime(locale, value, unit) {
623 const formatter = makeRelativeFormat(locale, {
624 numeric: 'auto'
625 });
626
627 if (formatter) {
628 return formatter.format(value, unit);
629 } else {
630 return formatEnRelativeTime(value, unit);
631 }
632 } // Simplified "en" RelativeTimeFormat.format function
633 //
634 // Values should roughly match
635 // new Intl.RelativeTimeFormat('en', {numeric: 'auto'}).format(value, unit)
636 //
637
638
639 function formatEnRelativeTime(value, unit) {
640 if (value === 0) {
641 switch (unit) {
642 case 'year':
643 case 'quarter':
644 case 'month':
645 case 'week':
646 return "this ".concat(unit);
647
648 case 'day':
649 return 'today';
650
651 case 'hour':
652 case 'minute':
653 return "in 0 ".concat(unit, "s");
654
655 case 'second':
656 return 'now';
657 }
658 } else if (value === 1) {
659 switch (unit) {
660 case 'year':
661 case 'quarter':
662 case 'month':
663 case 'week':
664 return "next ".concat(unit);
665
666 case 'day':
667 return 'tomorrow';
668
669 case 'hour':
670 case 'minute':
671 case 'second':
672 return "in 1 ".concat(unit);
673 }
674 } else if (value === -1) {
675 switch (unit) {
676 case 'year':
677 case 'quarter':
678 case 'month':
679 case 'week':
680 return "last ".concat(unit);
681
682 case 'day':
683 return 'yesterday';
684
685 case 'hour':
686 case 'minute':
687 case 'second':
688 return "1 ".concat(unit, " ago");
689 }
690 } else if (value > 1) {
691 switch (unit) {
692 case 'year':
693 case 'quarter':
694 case 'month':
695 case 'week':
696 case 'day':
697 case 'hour':
698 case 'minute':
699 case 'second':
700 return "in ".concat(value, " ").concat(unit, "s");
701 }
702 } else if (value < -1) {
703 switch (unit) {
704 case 'year':
705 case 'quarter':
706 case 'month':
707 case 'week':
708 case 'day':
709 case 'hour':
710 case 'minute':
711 case 'second':
712 return "".concat(-value, " ").concat(unit, "s ago");
713 }
714 }
715
716 throw new RangeError("Invalid unit argument for format() '".concat(unit, "'"));
717 }
718
719 const timeFormatter = makeFormatter({
720 hour: 'numeric',
721 minute: '2-digit'
722 });
723
724 class RelativeTimeElement extends ExtendedTimeElement {
725 getFormattedDate() {
726 const date = this.date;
727
728 if (date) {
729 return new RelativeTime(date, localeFromElement(this)).toString();
730 }
731 }
732
733 connectedCallback() {
734 nowElements.push(this);
735
736 if (!updateNowElementsId) {
737 updateNowElements();
738 updateNowElementsId = setInterval(updateNowElements, 60 * 1000);
739 }
740
741 super.connectedCallback();
742 }
743
744 disconnectedCallback() {
745 const ix = nowElements.indexOf(this);
746
747 if (ix !== -1) {
748 nowElements.splice(ix, 1);
749 }
750
751 if (!nowElements.length) {
752 if (updateNowElementsId) {
753 clearInterval(updateNowElementsId);
754 updateNowElementsId = null;
755 }
756 }
757 }
758
759 } // Internal: Array tracking all elements attached to the document that need
760 // to be updated every minute.
761
762 const nowElements = []; // Internal: Timer ID for `updateNowElements` interval.
763
764 let updateNowElementsId; // Internal: Install a timer to refresh all attached relative-time elements every
765 // minute.
766
767 function updateNowElements() {
768 let time, i, len;
769
770 for (i = 0, len = nowElements.length; i < len; i++) {
771 time = nowElements[i];
772 time.textContent = time.getFormattedDate() || '';
773 }
774 } // Public: RelativeTimeElement constructor.
775 //
776 // var time = new RelativeTimeElement()
777 // # => <relative-time></relative-time>
778 //
779
780
781 if (!window.customElements.get('relative-time')) {
782 window.RelativeTimeElement = RelativeTimeElement;
783 window.customElements.define('relative-time', RelativeTimeElement);
784 }
785
786 class TimeAgoElement extends RelativeTimeElement {
787 getFormattedDate() {
788 const format = this.getAttribute('format');
789 const date = this.date;
790 if (!date) return;
791
792 if (format === 'micro') {
793 return new RelativeTime(date, localeFromElement(this)).microTimeAgo();
794 } else {
795 return new RelativeTime(date, localeFromElement(this)).timeAgo();
796 }
797 }
798
799 }
800
801 if (!window.customElements.get('time-ago')) {
802 window.TimeAgoElement = TimeAgoElement;
803 window.customElements.define('time-ago', TimeAgoElement);
804 }
805
806 class TimeUntilElement extends RelativeTimeElement {
807 getFormattedDate() {
808 const format = this.getAttribute('format');
809 const date = this.date;
810 if (!date) return;
811
812 if (format === 'micro') {
813 return new RelativeTime(date, localeFromElement(this)).microTimeUntil();
814 } else {
815 return new RelativeTime(date, localeFromElement(this)).timeUntil();
816 }
817 }
818
819 }
820
821 if (!window.customElements.get('time-until')) {
822 window.TimeUntilElement = TimeUntilElement;
823 window.customElements.define('time-until', TimeUntilElement);
824 }
825
826 exports.LocalTimeElement = LocalTimeElement;
827 exports.RelativeTimeElement = RelativeTimeElement;
828 exports.TimeAgoElement = TimeAgoElement;
829 exports.TimeUntilElement = TimeUntilElement;
830
831 Object.defineProperty(exports, '__esModule', { value: true });
832
833}));