2021-03-24 14:21:21 +00:00
|
|
|
import { LitElement, html } from 'lit';
|
|
|
|
import { customElement, property, state } from 'lit/decorators';
|
2021-03-18 13:04:23 +00:00
|
|
|
import { watch } from '../../internal/decorators';
|
2020-11-11 22:31:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @since 2.0
|
|
|
|
* @status stable
|
|
|
|
*/
|
2021-03-18 13:04:23 +00:00
|
|
|
@customElement('sl-relative-time')
|
2021-03-09 00:14:32 +00:00
|
|
|
export default class SlRelativeTime extends LitElement {
|
2021-02-26 14:09:13 +00:00
|
|
|
private updateTimeout: any;
|
2020-11-11 22:31:53 +00:00
|
|
|
|
2021-03-24 14:21:21 +00:00
|
|
|
@state() private isoTime = '';
|
|
|
|
@state() private relativeTime = '';
|
|
|
|
@state() private titleTime = '';
|
2021-03-06 17:01:39 +00:00
|
|
|
|
2020-11-11 22:31:53 +00:00
|
|
|
/** The date from which to calculate time from. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() date: Date | string;
|
2020-11-11 22:31:53 +00:00
|
|
|
|
|
|
|
/** The locale to use when formatting the number. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() locale: string;
|
2020-11-11 22:31:53 +00:00
|
|
|
|
|
|
|
/** The formatting style to use. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() format: 'long' | 'short' | 'narrow' = 'long';
|
2020-11-11 22:31:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* When `auto`, values such as "yesterday" and "tomorrow" will be shown when possible. When `always`, values such as
|
|
|
|
* "1 day ago" and "in 1 day" will be shown.
|
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() numeric: 'always' | 'auto' = 'auto';
|
2020-11-11 22:31:53 +00:00
|
|
|
|
2020-11-20 22:02:38 +00:00
|
|
|
/** Keep the displayed value up to date as time passes. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) sync = false;
|
2020-11-20 22:02:38 +00:00
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
firstUpdated() {
|
2020-11-20 22:02:38 +00:00
|
|
|
this.updateTime();
|
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
disconnectedCallback() {
|
|
|
|
super.disconnectedCallback();
|
2020-11-20 22:02:38 +00:00
|
|
|
clearTimeout(this.updateTimeout);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:39:48 +00:00
|
|
|
@watch('date')
|
|
|
|
@watch('locale')
|
|
|
|
@watch('format')
|
|
|
|
@watch('numeric')
|
|
|
|
@watch('sync')
|
2020-11-20 22:02:38 +00:00
|
|
|
updateTime() {
|
|
|
|
const now = new Date();
|
2020-11-11 22:31:53 +00:00
|
|
|
const date = new Date(this.date);
|
|
|
|
|
2020-11-20 22:02:38 +00:00
|
|
|
// Check for an invalid date
|
|
|
|
if (isNaN(date.getMilliseconds())) {
|
|
|
|
this.relativeTime = '';
|
|
|
|
this.isoTime = '';
|
|
|
|
return;
|
2020-11-11 22:31:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-20 22:02:38 +00:00
|
|
|
const diff = +date - +now;
|
|
|
|
const availableUnits = [
|
|
|
|
{ max: 2760000, value: 60000, unit: 'minute' }, // max 46 minutes
|
|
|
|
{ max: 72000000, value: 3600000, unit: 'hour' }, // max 20 hours
|
|
|
|
{ max: 518400000, value: 86400000, unit: 'day' }, // max 6 days
|
|
|
|
{ max: 2419200000, value: 604800000, unit: 'week' }, // max 28 days
|
|
|
|
{ max: 28512000000, value: 2592000000, unit: 'month' }, // max 11 months
|
|
|
|
{ max: Infinity, value: 31536000000, unit: 'year' }
|
|
|
|
];
|
2021-02-26 14:09:13 +00:00
|
|
|
const { unit, value } = availableUnits.find(unit => Math.abs(diff) < unit.max) as any;
|
2020-11-20 22:02:38 +00:00
|
|
|
|
|
|
|
this.isoTime = date.toISOString();
|
|
|
|
this.titleTime = new Intl.DateTimeFormat(this.locale, {
|
|
|
|
month: 'long',
|
|
|
|
year: 'numeric',
|
|
|
|
day: 'numeric',
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric',
|
|
|
|
timeZoneName: 'short'
|
|
|
|
}).format(date);
|
2020-11-11 22:31:53 +00:00
|
|
|
|
2020-11-20 22:02:38 +00:00
|
|
|
this.relativeTime = new Intl.RelativeTimeFormat(this.locale, {
|
|
|
|
numeric: this.numeric,
|
|
|
|
style: this.format
|
|
|
|
}).format(Math.round(diff / value), unit);
|
|
|
|
|
|
|
|
// If sync is enabled, update as time passes
|
|
|
|
clearTimeout(this.updateTimeout);
|
|
|
|
if (this.sync) {
|
|
|
|
// Calculates the number of milliseconds until the next respective unit changes. This ensures that all components
|
|
|
|
// update at the same time which is less distracting than updating independently.
|
|
|
|
const getTimeUntilNextUnit = (unit: 'second' | 'minute' | 'hour' | 'day') => {
|
|
|
|
const units = { second: 1000, minute: 60000, hour: 3600000, day: 86400000 };
|
|
|
|
const value = units[unit];
|
|
|
|
return value - (now.getTime() % value);
|
|
|
|
};
|
|
|
|
|
|
|
|
let nextInterval: number;
|
|
|
|
|
|
|
|
// NOTE: this could be optimized to determine when the next update should actually occur, but the size and cost of
|
|
|
|
// that logic probably isn't worth the performance benefit
|
|
|
|
if (unit === 'minute') {
|
|
|
|
nextInterval = getTimeUntilNextUnit('second');
|
|
|
|
} else if (unit === 'hour') {
|
|
|
|
nextInterval = getTimeUntilNextUnit('minute');
|
|
|
|
} else if (unit === 'day') {
|
|
|
|
nextInterval = getTimeUntilNextUnit('hour');
|
|
|
|
} else {
|
|
|
|
// Cap updates at once per day. It's unlikely a user will reach this value, plus setTimeout has a limit on the
|
|
|
|
// value it can accept. https://stackoverflow.com/a/3468650/567486
|
|
|
|
nextInterval = getTimeUntilNextUnit('day'); // next day
|
|
|
|
}
|
|
|
|
this.updateTimeout = setTimeout(() => this.updateTime(), nextInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2021-02-26 14:09:13 +00:00
|
|
|
return html` <time datetime=${this.isoTime} title=${this.titleTime}>${this.relativeTime}</time> `;
|
2020-11-11 22:31:53 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-12 14:07:38 +00:00
|
|
|
|
2021-03-12 14:09:08 +00:00
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'sl-relative-time': SlRelativeTime;
|
|
|
|
}
|
|
|
|
}
|