| const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; |
| const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; |
| |
| function pad(num) { |
| return "0".concat(num).slice(-2); |
| } |
| |
| function strftime(time, formatString) { |
| const day = time.getDay(); |
| const date = time.getDate(); |
| const month = time.getMonth(); |
| const year = time.getFullYear(); |
| const hour = time.getHours(); |
| const minute = time.getMinutes(); |
| const second = time.getSeconds(); |
| return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function (_arg) { |
| let match; |
| const modifier = _arg[1]; |
| |
| switch (modifier) { |
| case '%': |
| return '%'; |
| |
| case 'a': |
| return weekdays[day].slice(0, 3); |
| |
| case 'A': |
| return weekdays[day]; |
| |
| case 'b': |
| return months[month].slice(0, 3); |
| |
| case 'B': |
| return months[month]; |
| |
| case 'c': |
| return time.toString(); |
| |
| case 'd': |
| return pad(date); |
| |
| case 'e': |
| return String(date); |
| |
| case 'H': |
| return pad(hour); |
| |
| case 'I': |
| return pad(strftime(time, '%l')); |
| |
| case 'l': |
| if (hour === 0 || hour === 12) { |
| return String(12); |
| } else { |
| return String((hour + 12) % 12); |
| } |
| |
| case 'm': |
| return pad(month + 1); |
| |
| case 'M': |
| return pad(minute); |
| |
| case 'p': |
| if (hour > 11) { |
| return 'PM'; |
| } else { |
| return 'AM'; |
| } |
| |
| case 'P': |
| if (hour > 11) { |
| return 'pm'; |
| } else { |
| return 'am'; |
| } |
| |
| case 'S': |
| return pad(second); |
| |
| case 'w': |
| return String(day); |
| |
| case 'y': |
| return pad(year % 100); |
| |
| case 'Y': |
| return String(year); |
| |
| case 'Z': |
| match = time.toString().match(/\((\w+)\)$/); |
| return match ? match[1] : ''; |
| |
| case 'z': |
| match = time.toString().match(/\w([+-]\d\d\d\d) /); |
| return match ? match[1] : '';} |
| |
| |
| return ''; |
| }); |
| } |
| function makeFormatter(options) { |
| let format; |
| return function () { |
| if (format) return format; |
| |
| if ('Intl' in window) { |
| try { |
| format = new Intl.DateTimeFormat(undefined, options); |
| return format; |
| } catch (e) { |
| if (!(e instanceof RangeError)) { |
| throw e; |
| } |
| } |
| } |
| }; |
| } |
| let dayFirst = null; |
| const dayFirstFormatter = makeFormatter({ |
| day: 'numeric', |
| month: 'short' }); |
| // Private: Determine if the day should be formatted before the month name in |
| // the user's current locale. For example, `9 Jun` for en-GB and `Jun 9` |
| // for en-US. |
| // |
| // Returns true if the day appears before the month. |
| |
| function isDayFirst() { |
| if (dayFirst !== null) { |
| return dayFirst; |
| } |
| |
| const formatter = dayFirstFormatter(); |
| |
| if (formatter) { |
| const output = formatter.format(new Date(0)); |
| dayFirst = !!output.match(/^\d/); |
| return dayFirst; |
| } else { |
| return false; |
| } |
| } |
| let yearSeparator = null; |
| const yearFormatter = makeFormatter({ |
| day: 'numeric', |
| month: 'short', |
| year: 'numeric' }); |
| // Private: Determine if the year should be separated from the month and day |
| // with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US. |
| // |
| // Returns true if the date needs a separator. |
| |
| function isYearSeparator() { |
| if (yearSeparator !== null) { |
| return yearSeparator; |
| } |
| |
| const formatter = yearFormatter(); |
| |
| if (formatter) { |
| const output = formatter.format(new Date(0)); |
| yearSeparator = !!output.match(/\d,/); |
| return yearSeparator; |
| } else { |
| return true; |
| } |
| } // Private: Determine if the date occurs in the same year as today's date. |
| // |
| // date - The Date to test. |
| // |
| // Returns true if it's this year. |
| |
| function isThisYear(date) { |
| const now = new Date(); |
| return now.getUTCFullYear() === date.getUTCFullYear(); |
| } |
| function makeRelativeFormat(locale, options) { |
| if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) { |
| try { |
| // eslint-disable-next-line flowtype/no-flow-fix-me-comments |
| // $FlowFixMe: missing RelativeTimeFormat type |
| return new Intl.RelativeTimeFormat(locale, options); |
| } catch (e) { |
| if (!(e instanceof RangeError)) { |
| throw e; |
| } |
| } |
| } |
| } // Private: Get preferred Intl locale for a target element. |
| // |
| // Traverses parents until it finds an explicit `lang` other returns "default". |
| |
| function localeFromElement(el) { |
| const container = el.closest('[lang]'); |
| |
| if (container instanceof HTMLElement && container.lang) { |
| return container.lang; |
| } |
| |
| return 'default'; |
| } |
| |
| const datetimes = new WeakMap(); |
| class ExtendedTimeElement extends HTMLElement { |
| static get observedAttributes() { |
| return ['datetime', 'day', 'format', 'lang', 'hour', 'minute', 'month', 'second', 'title', 'weekday', 'year']; |
| } |
| |
| connectedCallback() { |
| const title = this.getFormattedTitle(); |
| |
| if (title && !this.hasAttribute('title')) { |
| this.setAttribute('title', title); |
| } |
| |
| const text = this.getFormattedDate(); |
| |
| if (text) { |
| this.textContent = text; |
| } |
| } // Internal: Refresh the time element's formatted date when an attribute changes. |
| |
| |
| attributeChangedCallback(attrName, oldValue, newValue) { |
| const oldTitle = this.getFormattedTitle(); |
| |
| if (attrName === 'datetime') { |
| const millis = Date.parse(newValue); |
| |
| if (isNaN(millis)) { |
| datetimes.delete(this); |
| } else { |
| datetimes.set(this, new Date(millis)); |
| } |
| } |
| |
| const title = this.getFormattedTitle(); |
| const currentTitle = this.getAttribute('title'); |
| |
| if (attrName !== 'title' && title && (!currentTitle || currentTitle === oldTitle)) { |
| this.setAttribute('title', title); |
| } |
| |
| const text = this.getFormattedDate(); |
| |
| if (text) { |
| this.textContent = text; |
| } |
| } |
| |
| get date() { |
| return datetimes.get(this); |
| } // Internal: Format the ISO 8601 timestamp according to the user agent's |
| // locale-aware formatting rules. The element's existing `title` attribute |
| // value takes precedence over this custom format. |
| // |
| // Returns a formatted time String. |
| |
| |
| getFormattedTitle() { |
| const date = this.date; |
| if (!date) return; |
| const formatter = titleFormatter(); |
| |
| if (formatter) { |
| return formatter.format(date); |
| } else { |
| try { |
| return date.toLocaleString(); |
| } catch (e) { |
| if (e instanceof RangeError) { |
| return date.toString(); |
| } else { |
| throw e; |
| } |
| } |
| } |
| } |
| |
| getFormattedDate() {}} |
| |
| |
| const titleFormatter = makeFormatter({ |
| day: 'numeric', |
| month: 'short', |
| year: 'numeric', |
| hour: 'numeric', |
| minute: '2-digit', |
| timeZoneName: 'short' }); |
| |
| |
| const formatters = new WeakMap(); |
| class LocalTimeElement extends ExtendedTimeElement { |
| attributeChangedCallback(attrName, oldValue, newValue) { |
| if (attrName === 'hour' || attrName === 'minute' || attrName === 'second' || attrName === 'time-zone-name') { |
| formatters.delete(this); |
| } |
| |
| super.attributeChangedCallback(attrName, oldValue, newValue); |
| } // Formats the element's date, in the user's current locale, according to |
| // the formatting attribute values. Values are not passed straight through to |
| // an Intl.DateTimeFormat instance so that weekday and month names are always |
| // displayed in English, for now. |
| // |
| // Supported attributes are: |
| // |
| // weekday - "short", "long" |
| // year - "numeric", "2-digit" |
| // month - "short", "long" |
| // day - "numeric", "2-digit" |
| // hour - "numeric", "2-digit" |
| // minute - "numeric", "2-digit" |
| // second - "numeric", "2-digit" |
| // |
| // Returns a formatted time String. |
| |
| |
| getFormattedDate() { |
| const d = this.date; |
| if (!d) return; |
| const date = formatDate(this, d) || ''; |
| const time = formatTime(this, d) || ''; |
| return "".concat(date, " ").concat(time).trim(); |
| }} |
| |
| // Private: Format a date according to the `weekday`, `day`, `month`, |
| // and `year` attribute values. |
| // |
| // This doesn't use Intl.DateTimeFormat to avoid creating text in the user's |
| // language when the majority of the surrounding text is in English. There's |
| // currently no way to separate the language from the format in Intl. |
| // |
| // el - The local-time element to format. |
| // |
| // Returns a date String or null if no date formats are provided. |
| |
| function formatDate(el, date) { |
| // map attribute values to strftime |
| const props = { |
| weekday: { |
| short: '%a', |
| long: '%A' }, |
| |
| day: { |
| numeric: '%e', |
| '2-digit': '%d' }, |
| |
| month: { |
| short: '%b', |
| long: '%B' }, |
| |
| year: { |
| numeric: '%Y', |
| '2-digit': '%y' } }; |
| |
| // build a strftime format string |
| |
| let format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year'; |
| |
| for (const prop in props) { |
| const value = props[prop][el.getAttribute(prop)]; |
| format = format.replace(prop, value || ''); |
| } // clean up year separator comma |
| |
| |
| format = format.replace(/(\s,)|(,\s$)/, ''); // squeeze spaces from final string |
| |
| return strftime(date, format).replace(/\s+/, ' ').trim(); |
| } // Private: Format a time according to the `hour`, `minute`, and `second` |
| // attribute values. |
| // |
| // el - The local-time element to format. |
| // |
| // Returns a time String or null if no time formats are provided. |
| |
| |
| function formatTime(el, date) { |
| const options = {}; // retrieve format settings from attributes |
| |
| const hour = el.getAttribute('hour'); |
| if (hour === 'numeric' || hour === '2-digit') options.hour = hour; |
| const minute = el.getAttribute('minute'); |
| if (minute === 'numeric' || minute === '2-digit') options.minute = minute; |
| const second = el.getAttribute('second'); |
| if (second === 'numeric' || second === '2-digit') options.second = second; |
| const tz = el.getAttribute('time-zone-name'); |
| if (tz === 'short' || tz === 'long') options.timeZoneName = tz; // No time format attributes provided. |
| |
| if (Object.keys(options).length === 0) { |
| return; |
| } |
| |
| let factory = formatters.get(el); |
| |
| if (!factory) { |
| factory = makeFormatter(options); |
| formatters.set(el, factory); |
| } |
| |
| const formatter = factory(); |
| |
| if (formatter) { |
| // locale-aware formatting of 24 or 12 hour times |
| return formatter.format(date); |
| } else { |
| // fall back to strftime for non-Intl browsers |
| const timef = options.second ? '%H:%M:%S' : '%H:%M'; |
| return strftime(date, timef); |
| } |
| } // Public: LocalTimeElement constructor. |
| // |
| // var time = new LocalTimeElement() |
| // # => <local-time></local-time> |
| // |
| |
| |
| if (!window.customElements.get('local-time')) { |
| window.LocalTimeElement = LocalTimeElement; |
| window.customElements.define('local-time', LocalTimeElement); |
| } |
| |
| class RelativeTime { |
| constructor(date, locale) { |
| this.date = date; |
| this.locale = locale; |
| } |
| |
| toString() { |
| const ago = this.timeElapsed(); |
| |
| if (ago) { |
| return ago; |
| } else { |
| const ahead = this.timeAhead(); |
| |
| if (ahead) { |
| return ahead; |
| } else { |
| return "on ".concat(this.formatDate()); |
| } |
| } |
| } |
| |
| timeElapsed() { |
| const ms = new Date().getTime() - this.date.getTime(); |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| |
| if (ms >= 0 && day < 30) { |
| return this.timeAgoFromMs(ms); |
| } else { |
| return null; |
| } |
| } |
| |
| timeAhead() { |
| const ms = this.date.getTime() - new Date().getTime(); |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| |
| if (ms >= 0 && day < 30) { |
| return this.timeUntil(); |
| } else { |
| return null; |
| } |
| } |
| |
| timeAgo() { |
| const ms = new Date().getTime() - this.date.getTime(); |
| return this.timeAgoFromMs(ms); |
| } |
| |
| timeAgoFromMs(ms) { |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| const month = Math.round(day / 30); |
| const year = Math.round(month / 12); |
| |
| if (ms < 0) { |
| return formatRelativeTime(this.locale, 0, 'second'); |
| } else if (sec < 10) { |
| return formatRelativeTime(this.locale, 0, 'second'); |
| } else if (sec < 45) { |
| return formatRelativeTime(this.locale, -sec, 'second'); |
| } else if (sec < 90) { |
| return formatRelativeTime(this.locale, -min, 'minute'); |
| } else if (min < 45) { |
| return formatRelativeTime(this.locale, -min, 'minute'); |
| } else if (min < 90) { |
| return formatRelativeTime(this.locale, -hr, 'hour'); |
| } else if (hr < 24) { |
| return formatRelativeTime(this.locale, -hr, 'hour'); |
| } else if (hr < 36) { |
| return formatRelativeTime(this.locale, -day, 'day'); |
| } else if (day < 30) { |
| return formatRelativeTime(this.locale, -day, 'day'); |
| } else if (month < 18) { |
| return formatRelativeTime(this.locale, -month, 'month'); |
| } else { |
| return formatRelativeTime(this.locale, -year, 'year'); |
| } |
| } |
| |
| microTimeAgo() { |
| const ms = new Date().getTime() - this.date.getTime(); |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| const month = Math.round(day / 30); |
| const year = Math.round(month / 12); |
| |
| if (min < 1) { |
| return '1m'; |
| } else if (min < 60) { |
| return "".concat(min, "m"); |
| } else if (hr < 24) { |
| return "".concat(hr, "h"); |
| } else if (day < 365) { |
| return "".concat(day, "d"); |
| } else { |
| return "".concat(year, "y"); |
| } |
| } |
| |
| timeUntil() { |
| const ms = this.date.getTime() - new Date().getTime(); |
| return this.timeUntilFromMs(ms); |
| } |
| |
| timeUntilFromMs(ms) { |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| const month = Math.round(day / 30); |
| const year = Math.round(month / 12); |
| |
| if (month >= 18) { |
| return formatRelativeTime(this.locale, year, 'year'); |
| } else if (month >= 12) { |
| return formatRelativeTime(this.locale, year, 'year'); |
| } else if (day >= 45) { |
| return formatRelativeTime(this.locale, month, 'month'); |
| } else if (day >= 30) { |
| return formatRelativeTime(this.locale, month, 'month'); |
| } else if (hr >= 36) { |
| return formatRelativeTime(this.locale, day, 'day'); |
| } else if (hr >= 24) { |
| return formatRelativeTime(this.locale, day, 'day'); |
| } else if (min >= 90) { |
| return formatRelativeTime(this.locale, hr, 'hour'); |
| } else if (min >= 45) { |
| return formatRelativeTime(this.locale, hr, 'hour'); |
| } else if (sec >= 90) { |
| return formatRelativeTime(this.locale, min, 'minute'); |
| } else if (sec >= 45) { |
| return formatRelativeTime(this.locale, min, 'minute'); |
| } else if (sec >= 10) { |
| return formatRelativeTime(this.locale, sec, 'second'); |
| } else { |
| return formatRelativeTime(this.locale, 0, 'second'); |
| } |
| } |
| |
| microTimeUntil() { |
| const ms = this.date.getTime() - new Date().getTime(); |
| const sec = Math.round(ms / 1000); |
| const min = Math.round(sec / 60); |
| const hr = Math.round(min / 60); |
| const day = Math.round(hr / 24); |
| const month = Math.round(day / 30); |
| const year = Math.round(month / 12); |
| |
| if (day >= 365) { |
| return "".concat(year, "y"); |
| } else if (hr >= 24) { |
| return "".concat(day, "d"); |
| } else if (min >= 60) { |
| return "".concat(hr, "h"); |
| } else if (min > 1) { |
| return "".concat(min, "m"); |
| } else { |
| return '1m'; |
| } |
| } |
| |
| formatDate() { |
| let format = isDayFirst() ? '%e %b' : '%b %e'; |
| |
| if (!isThisYear(this.date)) { |
| format += isYearSeparator() ? ', %Y' : ' %Y'; |
| } |
| |
| return strftime(this.date, format); |
| } |
| |
| formatTime() { |
| const formatter = timeFormatter(); |
| |
| if (formatter) { |
| return formatter.format(this.date); |
| } else { |
| return strftime(this.date, '%l:%M%P'); |
| } |
| }} |
| |
| |
| |
| function formatRelativeTime(locale, value, unit) { |
| const formatter = makeRelativeFormat(locale, { |
| numeric: 'auto' }); |
| |
| |
| if (formatter) { |
| return formatter.format(value, unit); |
| } else { |
| return formatEnRelativeTime(value, unit); |
| } |
| } // Simplified "en" RelativeTimeFormat.format function |
| // |
| // Values should roughly match |
| // new Intl.RelativeTimeFormat('en', {numeric: 'auto'}).format(value, unit) |
| // |
| |
| |
| function formatEnRelativeTime(value, unit) { |
| if (value === 0) { |
| switch (unit) { |
| case 'year': |
| case 'quarter': |
| case 'month': |
| case 'week': |
| return "this ".concat(unit); |
| |
| case 'day': |
| return 'today'; |
| |
| case 'hour': |
| case 'minute': |
| return "in 0 ".concat(unit, "s"); |
| |
| case 'second': |
| return 'now';} |
| |
| } else if (value === 1) { |
| switch (unit) { |
| case 'year': |
| case 'quarter': |
| case 'month': |
| case 'week': |
| return "next ".concat(unit); |
| |
| case 'day': |
| return 'tomorrow'; |
| |
| case 'hour': |
| case 'minute': |
| case 'second': |
| return "in 1 ".concat(unit);} |
| |
| } else if (value === -1) { |
| switch (unit) { |
| case 'year': |
| case 'quarter': |
| case 'month': |
| case 'week': |
| return "last ".concat(unit); |
| |
| case 'day': |
| return 'yesterday'; |
| |
| case 'hour': |
| case 'minute': |
| case 'second': |
| return "1 ".concat(unit, " ago");} |
| |
| } else if (value > 1) { |
| switch (unit) { |
| case 'year': |
| case 'quarter': |
| case 'month': |
| case 'week': |
| case 'day': |
| case 'hour': |
| case 'minute': |
| case 'second': |
| return "in ".concat(value, " ").concat(unit, "s");} |
| |
| } else if (value < -1) { |
| switch (unit) { |
| case 'year': |
| case 'quarter': |
| case 'month': |
| case 'week': |
| case 'day': |
| case 'hour': |
| case 'minute': |
| case 'second': |
| return "".concat(-value, " ").concat(unit, "s ago");} |
| |
| } |
| |
| throw new RangeError("Invalid unit argument for format() '".concat(unit, "'")); |
| } |
| |
| const timeFormatter = makeFormatter({ |
| hour: 'numeric', |
| minute: '2-digit' }); |
| |
| |
| class RelativeTimeElement extends ExtendedTimeElement { |
| getFormattedDate() { |
| const date = this.date; |
| |
| if (date) { |
| return new RelativeTime(date, localeFromElement(this)).toString(); |
| } |
| } |
| |
| connectedCallback() { |
| nowElements.push(this); |
| |
| if (!updateNowElementsId) { |
| updateNowElements(); |
| updateNowElementsId = setInterval(updateNowElements, 60 * 1000); |
| } |
| |
| super.connectedCallback(); |
| } |
| |
| disconnectedCallback() { |
| const ix = nowElements.indexOf(this); |
| |
| if (ix !== -1) { |
| nowElements.splice(ix, 1); |
| } |
| |
| if (!nowElements.length) { |
| if (updateNowElementsId) { |
| clearInterval(updateNowElementsId); |
| updateNowElementsId = null; |
| } |
| } |
| }} |
| |
| // Internal: Array tracking all elements attached to the document that need |
| // to be updated every minute. |
| |
| const nowElements = []; // Internal: Timer ID for `updateNowElements` interval. |
| |
| let updateNowElementsId; // Internal: Install a timer to refresh all attached relative-time elements every |
| // minute. |
| |
| function updateNowElements() { |
| let time, i, len; |
| |
| for (i = 0, len = nowElements.length; i < len; i++) { |
| time = nowElements[i]; |
| time.textContent = time.getFormattedDate() || ''; |
| } |
| } // Public: RelativeTimeElement constructor. |
| // |
| // var time = new RelativeTimeElement() |
| // # => <relative-time></relative-time> |
| // |
| |
| |
| if (!window.customElements.get('relative-time')) { |
| window.RelativeTimeElement = RelativeTimeElement; |
| window.customElements.define('relative-time', RelativeTimeElement); |
| } |
| |
| class TimeAgoElement extends RelativeTimeElement { |
| getFormattedDate() { |
| const format = this.getAttribute('format'); |
| const date = this.date; |
| if (!date) return; |
| |
| if (format === 'micro') { |
| return new RelativeTime(date, localeFromElement(this)).microTimeAgo(); |
| } else { |
| return new RelativeTime(date, localeFromElement(this)).timeAgo(); |
| } |
| }} |
| |
| |
| |
| if (!window.customElements.get('time-ago')) { |
| window.TimeAgoElement = TimeAgoElement; |
| window.customElements.define('time-ago', TimeAgoElement); |
| } |
| |
| class TimeUntilElement extends RelativeTimeElement { |
| getFormattedDate() { |
| const format = this.getAttribute('format'); |
| const date = this.date; |
| if (!date) return; |
| |
| if (format === 'micro') { |
| return new RelativeTime(date, localeFromElement(this)).microTimeUntil(); |
| } else { |
| return new RelativeTime(date, localeFromElement(this)).timeUntil(); |
| } |
| }} |
| |
| |
| |
| if (!window.customElements.get('time-until')) { |
| window.TimeUntilElement = TimeUntilElement; |
| window.customElements.define('time-until', TimeUntilElement); |
| } |
| |
| export { LocalTimeElement, RelativeTimeElement, TimeAgoElement, TimeUntilElement }; |