shoelace/src/components/relative-time/relative-time.ts

129 wiersze
4.5 KiB
TypeScript
Czysty Zwykły widok Historia

2021-05-27 21:00:43 +00:00
import { customElement, property, state } from 'lit/decorators.js';
2023-01-13 20:43:55 +00:00
import { html } from 'lit';
2022-03-24 12:01:09 +00:00
import { LocalizeController } from '../../utilities/localize';
2023-01-13 20:43:55 +00:00
import ShoelaceElement from '../../internal/shoelace-element';
interface UnitConfig {
max: number;
value: number;
unit: Intl.RelativeTimeFormatUnit;
}
const availableUnits: UnitConfig[] = [
{ 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' }
];
2020-11-11 22:31:53 +00:00
/**
Enrich components `@summary` with description from docs (#962) * keep header styles with repositioned description text * `animated-image` move description to component * code style * `avatar` add summary from docs * `badge` add summary from docs * `breadcrumb` add summary from docs * `button` add summary from docs * lead sentence is now part of the header * `button-group` add summary from docs * `card` add summary from docs * `checkbox` add summary from docs * `color-picker` add summary from docs * `details` add summary from docs * `dialog` add summary from docs * `divider` add summary from docs * `drawer` add summary from docs * `dropdown` add summary from docs * `format-bytes` add summary from docs * `format-date` add summary from docs * `format-number` add summary from docs * `icon` add summary from docs * `icon-button` add summary from docs * `image-comparer` add summary from docs * `include` add summary from docs * `input` add summary from docs * `menu` add summary from docs * `menu-item` add summary from docs * `menu-label` add summary from docs * `popup` add summary from docs * `progressbar` add summary from docs * `progress-ring` add summary from docs * `radio` add summary from docs * `radio-button` add summary from docs * `range` add summary from docs * `rating` add summary from docs * `relative-time` add summary from docs * `select` add summary from docs * `skeleton` add summary from docs * `spinner` add summary from docs * `split-panel` add summary from docs * `switch` add summary from docs * `tab-group` add summary from docs * `tag` add summary from docs * `textarea` add summary from docs * `tooltip` add summary from docs * `visually-hidden` add summary from docs * `animation` add summary from docs * `breadcrumb-item` add summary from docs * `mutation-observer` add summary from docs * `radio-group` add summary from docs * `resize-observer` add summary from docs * `tab` add summary from docs * `tab-panel` add summary from docs * `tree` add summary from docs * `tree-item` add summary from docs * remove `title` for further usage of `Sl` classnames in docs * revert: use markdown parser for component summary
2022-10-21 13:56:35 +00:00
* @summary Outputs a localized time phrase relative to the current date and time.
2023-01-12 15:26:25 +00:00
* @documentation https://shoelace.style/components/relative-time
2020-11-11 22:31:53 +00:00
* @status stable
2023-01-12 15:26:25 +00:00
* @since 2.0
2020-11-11 22:31:53 +00:00
*/
2021-03-18 13:04:23 +00:00
@customElement('sl-relative-time')
2022-08-17 15:37:37 +00:00
export default class SlRelativeTime extends ShoelaceElement {
private readonly localize = new LocalizeController(this);
private updateTimeout: number;
2020-11-11 22:31:53 +00:00
@state() private isoTime = '';
@state() private relativeTime = '';
@state() private titleTime = '';
2021-03-06 17:01:39 +00:00
2022-12-06 16:18:14 +00:00
/**
* The date from which to calculate time from. If not set, the current date and time will be used. When passing a
* string, it's strongly recommended to use the ISO 8601 format to ensure timezones are handled correctly. To convert
* a date to this format in JavaScript, use [`date.toISOString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
*/
@property() date: Date | string = new Date();
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-07-01 00:04:46 +00:00
@property({ type: Boolean }) sync = false;
2020-11-20 22:02:38 +00:00
2021-03-06 17:01:39 +00:00
disconnectedCallback() {
super.disconnectedCallback();
2020-11-20 22:02:38 +00:00
clearTimeout(this.updateTimeout);
}
render() {
2020-11-20 22:02:38 +00:00
const now = new Date();
const then = new Date(this.date);
2020-11-11 22:31:53 +00:00
2020-11-20 22:02:38 +00:00
// Check for an invalid date
if (isNaN(then.getMilliseconds())) {
2020-11-20 22:02:38 +00:00
this.relativeTime = '';
this.isoTime = '';
return '';
2020-11-11 22:31:53 +00:00
}
const diff = then.getTime() - now.getTime();
const { unit, value } = availableUnits.find(singleUnit => Math.abs(diff) < singleUnit.max)!;
2020-11-20 22:02:38 +00:00
this.isoTime = then.toISOString();
this.titleTime = this.localize.date(then, {
2020-11-20 22:02:38 +00:00
month: 'long',
year: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'short'
});
2020-11-11 22:31:53 +00:00
this.relativeTime = this.localize.relativeTime(Math.round(diff / value), unit, {
2020-11-20 22:02:38 +00:00
numeric: this.numeric,
style: this.format
});
2020-11-20 22:02:38 +00:00
// If sync is enabled, update as time passes
clearTimeout(this.updateTimeout);
2020-11-20 22:02:38 +00:00
if (this.sync) {
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 = window.setTimeout(() => this.requestUpdate(), nextInterval);
2020-11-20 22:02:38 +00:00
}
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
}
}
// 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.
function getTimeUntilNextUnit(unit: 'second' | 'minute' | 'hour' | 'day') {
const units = { second: 1000, minute: 60000, hour: 3600000, day: 86400000 };
const value = units[unit];
return value - (Date.now() % value);
}
2021-03-12 14:09:08 +00:00
declare global {
interface HTMLElementTagNameMap {
'sl-relative-time': SlRelativeTime;
}
}