diff --git a/README.md b/README.md index bc5ca8d..4a28cb3 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,57 @@ # <relative-time> element -Formats a timestamp as a localized string or as relative text that auto-updates in the user's browser. +Formats a timestamp as a localized string or as relative text that auto-updates +in the user's browser. -This allows the server to cache HTML fragments containing dates and lets the browser choose how to localize the displayed time according to the user's preferences. For example, the server may have cached the following generated markup: +This allows the server to cache HTML fragments containing dates and lets the +browser choose how to localize the displayed time according to the user's +preferences. For example, the server may have cached the following generated +markup: ```html - - April 1, 2014 4:30pm - + April 1, 2014 4:30pm ``` -Every visitor is served the same markup from the server's cache. When it reaches the browser, the custom `relative-time` JavaScript localizes the element's text into the local timezone and formatting. +Every visitor is served the same markup from the server's cache. When it reaches +the browser, the custom `relative-time` JavaScript localizes the element's text +into the local timezone and formatting. ```html - - 1 Apr 2014 21:30 - + 1 Apr 2014 21:30 ``` -Dates are displayed before months, and a 24-hour clock is used, according to the user's browser settings. +Dates are displayed before months, and a 24-hour clock is used, according to the +user's browser settings. -If the browser's JavaScript is disabled, the default text served in the cached markup is still displayed. +If the browser's JavaScript is disabled, the default text served in the cached +markup is still displayed. ## Installation -Available on [npm](https://www.npmjs.com/) as [**@github/relative-time-element**](https://www.npmjs.com/package/@github/relative-time-element). +Available on [npm](https://www.npmjs.com/) as +[**@github/relative-time-element**](https://www.npmjs.com/package/@github/relative-time-element). ``` -$ npm install @github/relative-time-element +npm install @github/relative-time-element ``` -This element uses the `Intl.DateTimeFormat` & `Intl.RelativeTimeFormat` APIs, which are supported by all modern JS engines. If you need to support an older browser, you may need to introduce a polyfill for `Intl.DateTimeFormat` & `Intl.RelativeTimeFormat`. +This element uses the `Intl.DateTimeFormat` & `Intl.RelativeTimeFormat` APIs, +which are supported by all modern JS engines. If you need to support an older +browser, you may need to introduce a polyfill for `Intl.DateTimeFormat` & +`Intl.RelativeTimeFormat`. ## Usage -Add a `` element to your markup. Provide a default formatted date as the element's text content (e.g. April 1, 2014). It also MUST have a `datetime` attribute set to an ISO 8601 formatted timestamp. +Add a `` element to your markup. Provide a default formatted date +as the element's text content (e.g. April 1, 2014). It also MUST have a +`datetime` attribute set to an ISO 8601 formatted timestamp. ```html - - April 1, 2014 - + April 1, 2014 ``` -Depending on how far in the future this is being viewed, the element's text will be replaced with one of the following formats: +Depending on how far in the future this is being viewed, the element's text will +be replaced with one of the following formats: - 6 years from now - 20 days from now @@ -58,135 +67,153 @@ Depending on how far in the future this is being viewed, the element's text will - 20 days ago - on Apr 1, 2014 -So, a relative date phrase is used for up to a month and then the actual date is shown. +So, a relative date phrase is used for up to a month and then the actual date is +shown. #### Attributes -| Property Name | Attribute Name | Possible Values | Default Value | -|:---------------|:-----------------|:--------------------------------------------------------------------------------------------|:---------------------------------| -| `datetime` | `datetime` | `string` | - | -| `format` | `format` | `'datetime'\|'relative'\|'duration'` | `'auto'` | -| `date` | - | `Date \| null` | - | -| `tense` | `tense` | `'auto'\|'past'\|'future'` | `'auto'` | -| `precision` | `precision` | `'year'\|'month'\|'day'\|'hour'\|'minute'\|'second'` | `'second'` | -| `threshold` | `threshold` | `string` | `'P30D'` | -| `prefix` | `prefix` | `string` | `'on'` | -| `formatStyle` | `format-style` | `'long'\|'short'\|'narrow'` | * | -| `second` | `second` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `minute` | `minute` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `hour` | `hour` | `'numeric'\|'2-digit'\|undefined` | `undefined` | -| `weekday` | `weekday` | `'short'\|'long'\|'narrow'\|undefined` | ** | -| `day` | `day` | `'numeric'\|'2-digit'\|undefined` | `'numeric'` | -| `month` | `month` | `'numeric'\|'2-digit'\|'short'\|'long'\|'narrow'\|undefined` | *** | -| `year` | `year` | `'numeric'\|'2-digit'\|undefined` | **** | -| `timeZoneName` | `time-zone-name` | `'long'\|'short'\|'shortOffset'\|'longOffset'` `\|'shortGeneric'\|'longGeneric'\|undefined` | `undefined` | - -*: If unspecified, `formatStyle` will return `'narrow'` if `format` is `'elapsed'` or `'micro'`, `'short'` if the format is `'relative'` or `'datetime'`, otherwise it will be `'long'`. - -**: If unspecified, `month` will return the same value as `formatStyle` whenever `format` is `'datetime'`, otherwise it wil be `'short'`. - -***: If unspecified, `weekday` will return the same value as `formatStyle` whenever `format` is `'datetime'`, otherwise it will be `undefined`. - -****: If unspecified, `year` will return `'numeric'` if `datetime` represents the same year as the current year. It will return `undefined` if unspecified and if `datetime` represents a different year to the current year. +| Property Name | Attribute Name | Possible Values | Default Value | +| :------------- | :--------------- | :------------------------------------------------------------------------------------------ | :------------------ | +| `datetime` | `datetime` | `string` | - | +| `format` | `format` | `'datetime'\|'relative'\|'duration'` | `'relative'` | +| `date` | - | `Date \| null` | - | +| `tense` | `tense` | `'auto'\|'past'\|'future'` | `'auto'` | +| `precision` | `precision` | `'year'\|'month'\|'day'\|'hour'\|'minute'\|'second'` | `'second'` | +| `threshold` | `threshold` | `string` | `'P30D'` | +| `prefix` | `prefix` | `string` | `'on'` | +| `formatStyle` | `format-style` | `'long'\|'short'\|'narrow'` | \* | +| `second` | `second` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `minute` | `minute` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `hour` | `hour` | `'numeric'\|'2-digit'\|undefined` | `undefined` | +| `weekday` | `weekday` | `'short'\|'long'\|'narrow'\|undefined` | \*\* | +| `day` | `day` | `'numeric'\|'2-digit'\|undefined` | `'numeric'` | +| `month` | `month` | `'numeric'\|'2-digit'\|'short'\|'long'\|'narrow'\|undefined` | \*\*\* | +| `year` | `year` | `'numeric'\|'2-digit'\|undefined` | \*\*\*\* | +| `timeZoneName` | `time-zone-name` | `'long'\|'short'\|'shortOffset'\|'longOffset'` `\|'shortGeneric'\|'longGeneric'\|undefined` | `undefined` | + +\*: If unspecified, `formatStyle` will `'short'` if the format is +`'relative'` or `'datetime'`, otherwise it will be `'long'`. + +\*\*: If unspecified, `month` will return the same value as +`formatStyle` whenever `format` is `'datetime'`, otherwise it will be `'short'`. + +\*\*\*: If unspecified, `weekday` will return the same value as +`formatStyle` whenever `format` is `'datetime'`, otherwise it will be +`undefined`. + +\*\*\*\*: If unspecified, `year` will return `'numeric'` if +`datetime` represents the same year as the current year. It will return +`undefined` if unspecified and if `datetime` represents a different year to the +current year. ##### datetime (`string`) -This is the datetime that the element is meant to represent. This must be a valid [ISO8601 DateTime](https://en.wikipedia.org/wiki/ISO_8601). It is also possible to use the `date` property on the element to set the date. `el.date` expects a `Date` object, while `el.datetime` expects a string. Setting one will override the other. +This is the datetime that the element is meant to represent. This must be a +valid [ISO8601 DateTime](https://en.wikipedia.org/wiki/ISO_8601). It is also +possible to use the `date` property on the element to set the date. `el.date` +expects a `Date` object, while `el.datetime` expects a string. Setting one will +override the other. ```html - April 1, 2038 + April 1, 2038 + ``` -##### format (`'datetime'|'relative'|'duration'|'auto'|'micro'|'elapsed'`) - -Format can be either `'datetime'`, `'relative'`, or `'duration'`. It can also be one of several deprecated formats of `'auto'`, `'micro'`, or `'elapsed'`. +##### format (`'datetime'|'relative'|'duration'`) -The default format is `auto`, which is an alias for `relative`. In the next major version this will be `relative`. +Format can be either `'datetime'`, `'relative'`, or `'duration'`. The default +format is `relative`. `relative`. ###### `format=datetime` -The `datetime` format will display a localised datetime, based on the other properties of the element. It uses `Intl.DateTimeFormat` to display the `datetime` in a localised format. +The `datetime` format will display a localised datetime, based on the other +properties of the element. It uses `Intl.DateTimeFormat` to display the +`datetime` in a localised format. -Unless specified, it will consider `weekday` to be `'long'`, `month` to be `'long'`, and `'year'` to be `numeric` if the `datetime` is the same as the given year. Overriding `formatStyle` will change both `weekday` and `month` default values. Examples of this format with the default options and an `en` locale: +Unless specified, it will consider `weekday` to be `'long'`, `month` to be +`'long'`, and `'year'` to be `numeric` if the `datetime` is the same as the +given year. Overriding `formatStyle` will change both `weekday` and `month` +default values. Examples of this format with the default options and an `en` +locale: - - `Wed, 26 Aug 2021` - - `Sat, 31 Dec` (assuming the `datetime` is same year as the current year) +- `Wed, 26 Aug 2021` +- `Sat, 31 Dec` (assuming the `datetime` is same year as the current year) ###### `format=relative` -The default `relative` format will display dates relative to the current time (unless they are past the `threshold` value - see below). The values are rounded to display a single unit, for example if the time between the given `datetime` and the current wall clock time exceeds a day, then the format will _only_ ouput in days, and will not display hours, minutes or seconds. Some examples of this format with the default options and an `en` locale: - - - `in 20 days` - - `20 days ago` - - `in 1 minute` - - `on 31 Aug` (assuming the current date is the same year as the current year, and is more than 30 days away from 31 Aug) - - `on 26 Aug 2021` (assuming the current date is more than 30 days away from 26 Aug 2021) +The default `relative` format will display dates relative to the current time +(unless they are past the `threshold` value - see below). The values are rounded +to display a single unit, for example if the time between the given `datetime` +and the current wall clock time exceeds a day, then the format will _only_ ouput +in days, and will not display hours, minutes or seconds. Some examples of this +format with the default options and an `en` locale: + +- `in 20 days` +- `20 days ago` +- `in 1 minute` +- `on 31 Aug` (assuming the current date is the same year as the current year, + and is more than 30 days away from 31 Aug) +- `on 26 Aug 2021` (assuming the current date is more than 30 days away from 26 + Aug 2021) ###### `format=duration` -The `duration` format will display the time remaining (or elapsed time) from the given datetime, counting down the number of years, months, weeks, days, hours, minutes, and seconds. Any value that is `0` will be omitted from the display by default. Examples of this format with the default options and an `en` locale: +The `duration` format will display the time remaining (or elapsed time) from the +given datetime, counting down the number of years, months, weeks, days, hours, +minutes, and seconds. Any value that is `0` will be omitted from the display by +default. Examples of this format with the default options and an `en` locale: - `4 hours, 2 minutes, 30 seconds` - `4 hours` - `8 days, 30 minutes, 1 second` -###### Deprecated Formats - -###### `format=elapsed` - -This is similar to the `format=duration`, except the `formatStyle` defaults to `narrow`. Code that uses `format=elapsed` should migrate to `format=duration formatStyle=narrow`, as it will be removed in a later version. - -###### `format=auto` - -This is identical to `format=relative`. Code that uses `format=auto` should migrae to `format=relative` as this will be the new default in a later version. - -###### `format=micro` - -The `micro` format which will display relative dates (within the threshold) in a more compact format. Similar to `relative`, the `micro` format rounds values to the nearest largest value. Additionally, `micro` format will not round _lower_ than 1 minute, as such a `datetime` which is less than a minute from the current wall clock time will display `'1m'`. - -Code that uses `format=micro` should consider migrating to `format=relative` (perhaps with `formatStyle=narrow`), as `format=micro` can be difficult for users to understand, and can cause issues with assistive technologies. For example some screen readers (such as VoiceOver for mac) will read out `1m` as `1 meter`. - ###### Cheatsheet -| `format=datetime` | `format=relative` | `format=duration` | `format=micro` | `format=elapsed` | -|:-----------------:|:-----------------:|:------------------------------------------------:|:-----------------:|:----------------:| -| Wed 26 May 2024 | in 2 years | 2 years, 10 days, 3 hours, 20 minutes, 8 seconds | 2y | 2y 10d 3h 20m 8s | -| Wed 26 Aug 2021 | 2 years ago | 2 years, 10 days, 3 hours, 8 seconds | 2y | 2y 10d 3h 8s | -| Jan 15 2023 | in 30 days | 30 days, 4 hours, 20 minutes, 8 seconds | 30d | 30d 4h 20m 8s | -| Dec 15 2022 | 21 minutes ago | 21 minutes, 30 seconds | 21m | 21m 30s | -| Dec 15 2022 | 37 seconds ago | 37 seconds | 1m | 37s | +| `format=datetime` | `format=relative` | `format=duration` | +| :---------------: | :---------------: | :----------------------------------------------: | +| Wed 26 May 2024 | in 2 years | 2 years, 10 days, 3 hours, 20 minutes, 8 seconds | +| Wed 26 Aug 2021 | 2 years ago | 2 years, 10 days, 3 hours, 8 seconds | +| Jan 15 2023 | in 30 days | 30 days, 4 hours, 20 minutes, 8 seconds | +| Dec 15 2022 | 21 minutes ago | 21 minutes, 30 seconds | +| Dec 15 2022 | 37 seconds ago | 37 seconds | ##### tense (`'auto'|'past'|'future'`, default: `auto`) If `format` is `'datetime'` then this value will be ignored. -Tense can be used to prevent `duration` or `relative` formatted dates displaying dates in a tense other than the one specified. Setting `tense=past` will always display future `relative` dates as `now` and `duration` dates as `0 seconds`, while setting it to `future` will always display past dates `relative` as `now` and past `duration` dates as `0 seconds`. +Tense can be used to prevent `duration` or `relative` formatted dates displaying +dates in a tense other than the one specified. Setting `tense=past` will always +display future `relative` dates as `now` and `duration` dates as `0 seconds`, +while setting it to `future` will always display past dates `relative` as `now` +and past `duration` dates as `0 seconds`. For example when the given `datetime` is 40 seconds behind of the current date: -| `tense=`| format=duration | format=relative | -|:-------:|:----------------:|:---------------:| -| future | 0s | now | -| past | 40s | 40s ago | -| auto | 40s | 40s ago | +| `tense=` | format=duration | format=relative | +| :------: | :-------------: | :-------------: | +| future | 0s | now | +| past | 40s | 40s ago | +| auto | 40s | 40s ago | ```html - April 1, 2038 + April 1, 2038 + ``` ```html - April 1, 2038 + April 1, 2038 + ``` @@ -194,34 +221,47 @@ For example when the given `datetime` is 40 seconds behind of the current date: If `format` is `datetime` then this value will be ignored. -Precision can be used to limit the display of an `relative` or `duration` formatted time. By default times will display down to the `second` level of precision. Changing this value will truncate the display by zeroing out any unit lower than the given unit, as such units smaller than the given unit won't be displayed during `duration`, and `relative` will display `now` if the time away from the current time is less than the given precision unit. - -| `precision=` | format=duration | -|:-------------:|:-------------------:| -| seconds | 2y 6m 10d 3h 20m 8s | -| minutes | 2y 6m 10d 3h 20m | -| hours | 2y 6m 10d 3h | -| days | 2y 6m 10d | -| months | 2y 6m | -| years | 2y | - -| `precision=` | format=relative | -|:-------------:|:-------------------:| -| seconds | 25 seconds | -| minutes | now | -| hours | now | -| days | now | -| months | now | -| years | now | - +Precision can be used to limit the display of an `relative` or `duration` +formatted time. By default times will display down to the `second` level of +precision. Changing this value will truncate the display by zeroing out any unit +lower than the given unit, as such units smaller than the given unit won't be +displayed during `duration`, and `relative` will display `now` if the time away +from the current time is less than the given precision unit. + +| `precision=` | format=duration | +| :----------: | :-----------------: | +| seconds | 2y 6m 10d 3h 20m 8s | +| minutes | 2y 6m 10d 3h 20m | +| hours | 2y 6m 10d 3h | +| days | 2y 6m 10d | +| months | 2y 6m | +| years | 2y | + +| `precision=` | format=relative | +| :----------: | :-------------: | +| seconds | 25 seconds | +| minutes | now | +| hours | now | +| days | now | +| months | now | +| years | now | ##### threshold (`string`, default: `P30D`) -If `tense` is anything other than `'auto'`, or `format` is `'relative'` (or the deprecated `'auto'` or `'micro'` values), then this value will be ignored. +If `tense` is anything other than `'auto'`, or `format` is `'relative'`, then +this value will be ignored. -Threshold can be used to specify when a relative display (e.g. "5 days ago") should turn into an absolute display (i.e. the full date). This should be a valid [ISO8601 Time Duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). If the difference between the current time and the specified `datetime` is _more_ than the duration, then the date will be displayed as an absolute value (i.e. the full date), otherwise it will be formatted to a relative display (e.g. "5 days ago"). +Threshold can be used to specify when a relative display (e.g. "5 days ago") +should turn into an absolute display (i.e. the full date). This should be a +valid [ISO8601 Time Duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). +If the difference between the current time and the specified `datetime` is +_more_ than the duration, then the date will be displayed as an absolute value +(i.e. the full date), otherwise it will be formatted to a relative display (e.g. +"5 days ago"). -The default value for this is `P30D`, meaning if the current time is more than 30 days away from the specified date time, then an absolute date will be displayed. +The default value for this is `P30D`, meaning if the current time is more than +30 days away from the specified date time, then an absolute date will be +displayed. ```html @@ -237,9 +277,12 @@ The default value for this is `P30D`, meaning if the current time is more than 3 ##### prefix (`string`, default: `'on'`) -If `tense` is anything other than `'auto'`, or `format` is anything other than `'relative'` (or the deprecated `'auto'` or `'micro'` values), then this value will be ignored. +If `tense` is anything other than `'auto'`, or `format` is anything other than +`'relative'`, then this value will be ignored. -When formatting an absolute date (see above `threshold` for more details) it can be useful to prefix the date with some text. The default value for this is `on` but it can be any string value, an will be prepended to the date. +When formatting an absolute date (see above `threshold` for more details) it can +be useful to prefix the date with some text. The default value for this is `on` +but it can be any string value, an will be prepended to the date. ```html @@ -249,43 +292,58 @@ When formatting an absolute date (see above `threshold` for more details) it can ##### formatStyle (`'long'|'short'|'narrow'`, default: `'narrow'|'long'`) -This will used to determine the length of the unit names. This value is passed to the `Intl` objects as the `style` option. Some examples of how this can be used: +This will used to determine the length of the unit names. This value is passed +to the `Intl` objects as the `style` option. Some examples of how this can be +used: -| `format=` | `formatStyle=` | Display | -|:----------:|:--------------:|:------------------------:| -| relative | long | in 1 month | -| relative | short | in 1 mo. | -| relative | narrow | in 1 mo. | -| duration | long | 1 month, 2 days, 4 hours | -| duration | short | 1 mth, 2 days, 4 hr | -| duration | narrow | 1m 2d 4h | +| `format=` | `formatStyle=` | Display | +| :-------: | :------------: | :----------------------: | +| relative | long | in 1 month | +| relative | short | in 1 mo. | +| relative | narrow | in 1 mo. | +| duration | long | 1 month, 2 days, 4 hours | +| duration | short | 1 mth, 2 days, 4 hr | +| duration | narrow | 1m 2d 4h | ##### second, minute, hour, weekday, day, month, year, timeZoneName -For dates outside of the specified `threshold`, the formatting of the date can be configured using these attributes. The values for these attributes are passed to [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat): +For dates outside of the specified `threshold`, the formatting of the date can +be configured using these attributes. The values for these attributes are passed +to +[Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat): ##### lang -Lang is a [built-in global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang). Relative Time will use this to provide an applicable language to the `Intl` APIs. If the individual element does not have a `lang` attribute then it will traverse upwards in the tree to find the closest element that does, or default the lang to `en`. +Lang is a +[built-in global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang). +Relative Time will use this to provide an applicable language to the `Intl` +APIs. If the individual element does not have a `lang` attribute then it will +traverse upwards in the tree to find the closest element that does, or default +the lang to `en`. ## Browser Support -Browsers without native [custom element support][support] require a [polyfill][ce-polyfill]. +Browsers without native [custom element support][support] require a +[polyfill][ce-polyfill]. -Browsers without native support for [`Intl.RelativeTimeFormat`][relativetimeformat] or [`Intl.DateTimeFormat`][datetimeformat] (such as Safari 13 or Edge 18) will also need polyfills. +Browsers without native support for +[`Intl.RelativeTimeFormat`][relativetimeformat] or +[`Intl.DateTimeFormat`][datetimeformat] (such as Safari 13 or Edge 18) will also +need polyfills. - Chrome - Firefox - Safari (version 14 and above) - Microsoft Edge (version 79 and above) -[support]: https://caniuse.com/custom-elementsv1 [relativetimeformat]: https://caniuse.com/mdn-javascript_builtins_intl_relativetimeformat_format [datetimeformat]: https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_format -[ce-polyfill]: https://github.com/webcomponents/custom-elements ## See Also -Most of this implementation is based on Basecamp's [local_time](https://github.com/basecamp/local_time) component. Thanks to @javan for open sourcing that work and allowing for others to build on top of it. +Most of this implementation is based on Basecamp's +[local_time](https://github.com/basecamp/local_time) component. Thanks to @javan +for open sourcing that work and allowing for others to build on top of it. -@rmm5t's [jquery-timeago](https://github.com/rmm5t/jquery-timeago) is one of the old time-ago-in-words JS plugins. +@rmm5t's [jquery-timeago](https://github.com/rmm5t/jquery-timeago) is one of the +old time-ago-in-words JS plugins. diff --git a/src/relative-time-element.ts b/src/relative-time-element.ts index b0b870d..dd6b861 100644 --- a/src/relative-time-element.ts +++ b/src/relative-time-element.ts @@ -1,15 +1,12 @@ -import {Duration, elapsedTime, getRelativeTimeUnit, isDuration, roundToSingleUnit, Unit, unitNames} from './duration.js' +import {Duration, elapsedTime, getRelativeTimeUnit, isDuration, Unit, unitNames} from './duration.js' const root = (typeof globalThis !== 'undefined' ? globalThis : window) as typeof window const HTMLElement = root.HTMLElement || (null as unknown as typeof window['HTMLElement']) -export type DeprecatedFormat = 'auto' | 'micro' | 'elapsed' -export type ResolvedFormat = 'duration' | 'relative' | 'datetime' -export type Format = DeprecatedFormat | ResolvedFormat +export type Format = 'duration' | 'relative' | 'datetime' export type FormatStyle = 'long' | 'short' | 'narrow' export type Tense = 'auto' | 'past' | 'future' const emptyDuration = new Duration() -const microEmptyDuration = new Duration(0, 0, 0, 0, 0, 1) export class RelativeTimeUpdatedEvent extends Event { constructor(public oldText: string, public newText: string, public oldTitle: string, public newTitle: string) { @@ -19,7 +16,7 @@ export class RelativeTimeUpdatedEvent extends Event { function getUnitFactor(el: RelativeTimeElement): number { if (!el.date) return Infinity - if (el.format === 'duration' || el.format === 'elapsed') { + if (el.format === 'duration') { const precision = el.precision if (precision === 'second') { return 1000 @@ -125,7 +122,7 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat }).format(date) } - #resolveFormat(duration: Duration): ResolvedFormat { + #resolveFormat(duration: Duration): Format { const format: string = this.format if (format === 'datetime') return 'datetime' if (format === 'duration') return 'duration' @@ -135,8 +132,7 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat // 'micro' is an alias for 'duration' if (format === 'micro') return 'duration' - // 'auto' is an alias for 'relative' - if ((format === 'auto' || format === 'relative') && typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) { + if (format === 'relative' && typeof Intl !== 'undefined' && Intl.RelativeTimeFormat) { const tense = this.tense if (tense === 'past' || tense === 'future') return 'relative' if (Duration.compare(duration, this.threshold) === 1) return 'relative' @@ -146,22 +142,17 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat #getDurationFormat(duration: Duration): string { const locale = this.#lang - const format = this.format const style = this.formatStyle const tense = this.tense - let empty = emptyDuration - if (format === 'micro') { - duration = roundToSingleUnit(duration) - empty = microEmptyDuration - if ((this.tense === 'past' && duration.sign !== -1) || (this.tense === 'future' && duration.sign !== 1)) { - duration = microEmptyDuration - } - } else if ((tense === 'past' && duration.sign !== -1) || (tense === 'future' && duration.sign !== 1)) { - duration = empty + if ((tense === 'past' && duration.sign !== -1) || (tense === 'future' && duration.sign !== 1)) { + duration = emptyDuration } const display = `${this.precision}sDisplay` if (duration.blank) { - return empty.toLocaleString(locale, {style, [display]: 'always'}) + return emptyDuration.toLocaleString(locale, { + style, + [display]: 'always', + }) } return duration.abs().toLocaleString(locale, {style}) } @@ -341,7 +332,6 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat get precision(): Unit { const precision = this.getAttribute('precision') as unknown as Unit if (unitNames.includes(precision)) return precision - if (this.format === 'micro') return 'minute' return 'second' } @@ -352,11 +342,8 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat get format(): Format { const format = this.getAttribute('format') if (format === 'datetime') return 'datetime' - if (format === 'relative') return 'relative' if (format === 'duration') return 'duration' - if (format === 'micro') return 'micro' - if (format === 'elapsed') return 'elapsed' - return 'auto' + return 'relative' } set format(value: Format) { @@ -369,7 +356,6 @@ export default class RelativeTimeElement extends HTMLElement implements Intl.Dat if (formatStyle === 'short') return 'short' if (formatStyle === 'narrow') return 'narrow' const format = this.format - if (format === 'elapsed' || format === 'micro') return 'narrow' if (format === 'datetime') return 'short' return 'long' } diff --git a/test/relative-time.js b/test/relative-time.js index d5e76b1..b74b96c 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -474,44 +474,24 @@ suite('relative-time', function () { assert.equal(time.shadowRoot.textContent, '3 months ago') }) - test('micro formats years', async () => { - const now = new Date(Date.now() - 10 * 365 * 24 * 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'past') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '11y') - }) - - test('micro formats future times', async () => { - const now = new Date(Date.now() + 3 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'past') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1m') - }) - - test('micro formats hours', async () => { - const now = new Date(Date.now() - 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'past') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') + test('rewrites relative-time datetimes < 18 months as "last year"', async () => { + freezeTime(new Date(2020, 0, 1)) + const then = new Date(2018, 9, 1).toISOString() + const timeElement = document.createElement('relative-time') + timeElement.setAttribute('tense', 'past') + timeElement.setAttribute('datetime', then) await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1h') + assert.equal(timeElement.shadowRoot.textContent, 'last year') }) - test('micro formats days', async () => { - const now = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'past') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') + test('rewrites relative-time datetimes >= 18 months as "years ago"', async () => { + freezeTime(new Date(2020, 0, 1)) + const then = new Date(2018, 6, 1).toISOString() + const timeElement = document.createElement('relative-time') + timeElement.setAttribute('tense', 'past') + timeElement.setAttribute('datetime', then) await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1d') + assert.equal(timeElement.shadowRoot.textContent, 'last year') }) }) @@ -562,46 +542,6 @@ suite('relative-time', function () { await Promise.resolve() assert.equal(root.children[0].shadowRoot.textContent, 'now') }) - - test('micro formats years', async () => { - const now = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'future') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '10y') - }) - - test('micro formats past times', async () => { - const now = new Date(Date.now() + 3 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'future') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1m') - }) - - test('micro formats hours', async () => { - const now = new Date(Date.now() + 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'future') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1h') - }) - - test('micro formats days', async () => { - const now = new Date(Date.now() + 25 * 60 * 60 * 1000).toISOString() - const time = document.createElement('relative-time') - time.setAttribute('tense', 'future') - time.setAttribute('datetime', now) - time.setAttribute('format', 'micro') - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '1d') - }) }) suite('[threshold=0][prefix=""]', () => { @@ -1791,670 +1731,4 @@ suite('relative-time', function () { }) } }) - - suite('legacy formats', function () { - const referenceDate = '2022-10-24T14:46:00.000Z' - const tests = new Set([ - // Same as the current time - { - datetime: '2022-10-24T14:46:00.000z', - tense: 'future', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-24T14:46:00.000z', - tense: 'past', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-24T14:46:00.000z', - tense: 'auto', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-24T14:46:00.000z', - tense: 'auto', - format: 'auto', - expected: 'now', - }, - - // Dates in the past - { - datetime: '2022-09-24T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-23T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-24T13:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1m', - }, - - // Dates in the future - { - datetime: '2022-10-24T15:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T16:00:00.000Z', - tense: 'future', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T16:15:00.000Z', - tense: 'future', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T16:31:00.000Z', - tense: 'future', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-30T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1w', - }, - { - datetime: '2022-11-24T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1m', - }, - { - datetime: '2023-10-23T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1y', - }, - { - datetime: '2023-10-24T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '1y', - }, - { - datetime: '2024-03-31T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '2y', - }, - { - datetime: '2024-04-01T14:46:00.000Z', - tense: 'future', - format: 'micro', - expected: '2y', - }, - - // Dates in the future - { - datetime: '2022-11-24T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-25T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1m', - }, - { - datetime: '2022-10-24T15:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1m', - }, - - // Dates in the past - { - datetime: '2022-10-24T13:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T13:30:00.000Z', - tense: 'past', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T13:17:00.000Z', - tense: 'past', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-24T13:01:00.000Z', - tense: 'past', - format: 'micro', - expected: '1h', - }, - { - datetime: '2022-10-18T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1w', - }, - { - datetime: '2022-09-23T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1m', - }, - { - datetime: '2021-10-25T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1y', - }, - { - datetime: '2021-10-24T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1y', - }, - { - datetime: '2021-05-18T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1y', - }, - { - datetime: '2021-05-17T14:46:00.000Z', - tense: 'past', - format: 'micro', - expected: '1y', - }, - - // Elapsed Times - { - datetime: '2022-10-24T14:46:10.000Z', - format: 'elapsed', - expected: '10s', - }, - { - datetime: '2022-10-24T14:45:50.000Z', - format: 'elapsed', - expected: '10s', - }, - { - datetime: '2022-10-24T14:45:50.000Z', - format: 'elapsed', - precision: 'minute', - expected: '0m', - }, - { - datetime: '2022-10-24T14:47:40.000Z', - format: 'elapsed', - expected: '1m 40s', - }, - { - datetime: '2022-10-24T14:44:20.000Z', - format: 'elapsed', - expected: '1m 40s', - }, - { - datetime: '2022-10-24T14:44:20.000Z', - format: 'elapsed', - precision: 'minute', - expected: '1m', - }, - { - datetime: '2022-10-24T15:51:40.000Z', - format: 'elapsed', - expected: '1h 5m 40s', - }, - { - datetime: '2022-10-24T15:51:40.000Z', - format: 'elapsed', - precision: 'minute', - expected: '1h 5m', - }, - { - datetime: '2022-10-24T15:52:00.000Z', - format: 'elapsed', - expected: '1h 6m', - }, - { - datetime: '2022-10-24T17:46:00.000Z', - format: 'elapsed', - expected: '3h', - }, - { - datetime: '2022-10-24T10:46:00.000Z', - format: 'elapsed', - expected: '4h', - }, - { - datetime: '2022-10-25T18:46:00.000Z', - format: 'elapsed', - expected: '1d 4h', - }, - { - datetime: '2022-10-23T10:46:00.000Z', - format: 'elapsed', - expected: '1d 4h', - }, - { - datetime: '2022-10-23T10:46:00.000Z', - format: 'elapsed', - precision: 'day', - expected: '1d', - }, - { - datetime: '2021-10-30T14:46:00.000Z', - format: 'elapsed', - expected: '11m 29d', - }, - { - datetime: '2021-10-30T14:46:00.000Z', - format: 'elapsed', - precision: 'month', - expected: '11m', - }, - { - datetime: '2021-10-29T14:46:00.000Z', - format: 'elapsed', - expected: '1y', - }, - - // Dates in the past - { - datetime: '2022-09-24T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'now', - }, - { - datetime: '2022-10-23T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'now', - }, - { - datetime: '2022-10-24T13:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'now', - }, - - // Dates in the future - { - datetime: '2022-10-24T15:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 1 hour', - }, - { - datetime: '2022-10-24T16:00:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 1 hour', - }, - { - datetime: '2022-10-24T16:15:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 1 hour', - }, - { - datetime: '2022-10-24T16:31:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 1 hour', - }, - { - datetime: '2022-10-30T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'next week', - }, - { - datetime: '2022-11-24T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'next month', - }, - { - datetime: '2023-10-23T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'next year', - }, - { - datetime: '2023-10-24T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'next year', - }, - { - datetime: '2024-03-31T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 2 years', - }, - { - datetime: '2024-04-01T14:46:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 2 years', - }, - { - datetime: '2022-10-24T15:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 1 hr.', - }, - { - datetime: '2022-10-24T16:00:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 1 hr.', - }, - { - datetime: '2022-10-24T16:15:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 1 hr.', - }, - { - datetime: '2022-10-24T16:31:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 1 hr.', - }, - { - datetime: '2022-10-30T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'next wk.', - }, - { - datetime: '2022-11-24T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'next mo.', - }, - { - datetime: '2023-10-23T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'next yr.', - }, - { - datetime: '2023-10-24T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'next yr.', - }, - { - datetime: '2024-03-31T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 2 yr.', - }, - { - datetime: '2024-04-01T14:46:00.000Z', - lang: 'en', - tense: 'future', - formatStyle: 'narrow', - expected: 'in 2 yr.', - }, - - // Dates in the future - { - datetime: '2022-11-24T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'now', - }, - { - datetime: '2022-10-25T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'now', - }, - { - datetime: '2022-10-24T15:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'now', - }, - - // Dates in the past - { - datetime: '2022-10-24T13:46:00.000Z', - tense: 'past', - format: 'auto', - expected: '1 hour ago', - }, - { - datetime: '2022-10-24T13:30:00.000Z', - tense: 'past', - format: 'auto', - expected: '1 hour ago', - }, - { - datetime: '2022-10-24T13:17:00.000Z', - tense: 'past', - format: 'auto', - expected: '1 hour ago', - }, - { - datetime: '2022-10-24T13:01:00.000Z', - tense: 'past', - format: 'auto', - expected: '1 hour ago', - }, - { - datetime: '2022-10-18T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last week', - }, - { - datetime: '2022-09-23T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last month', - }, - { - datetime: '2021-10-25T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last year', - }, - { - datetime: '2021-10-24T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last year', - }, - { - datetime: '2021-05-18T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last year', - }, - { - datetime: '2021-05-17T14:46:00.000Z', - tense: 'past', - format: 'auto', - expected: 'last year', - }, - { - datetime: '2022-10-24T13:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: '1 hr. ago', - }, - { - datetime: '2022-10-24T13:30:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: '1 hr. ago', - }, - { - datetime: '2022-10-24T13:17:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: '1 hr. ago', - }, - { - datetime: '2022-10-24T13:01:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: '1 hr. ago', - }, - { - datetime: '2022-10-18T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last wk.', - }, - { - datetime: '2022-09-23T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last mo.', - }, - { - datetime: '2021-10-25T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last yr.', - }, - { - datetime: '2021-10-24T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last yr.', - }, - { - datetime: '2021-05-18T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last yr.', - }, - { - datetime: '2021-05-17T14:46:00.000Z', - lang: 'en', - tense: 'past', - formatStyle: 'narrow', - expected: 'last yr.', - }, - - // Edge case dates - { - reference: '2022-01-01T12:00:00.000Z', - datetime: '2021-12-31T12:00:00.000Z', - tense: 'past', - format: 'auto', - expected: 'yesterday', - }, - { - reference: '2022-01-01T12:00:00.000Z', - datetime: '2021-12-31T12:00:00.000Z', - tense: 'past', - format: 'micro', - expected: '1d', - }, - { - reference: '2022-12-31T12:00:00.000Z', - datetime: '2022-01-01T12:00:00.000Z', - tense: 'past', - format: 'micro', - expected: '1y', - }, - { - reference: '2022-12-31T12:00:00.000Z', - datetime: '2024-03-01T12:00:00.000Z', - tense: 'future', - format: 'auto', - expected: 'in 2 years', - }, - { - reference: '2022-12-31T12:00:00.000Z', - datetime: '2024-03-01T12:00:00.000Z', - tense: 'future', - format: 'micro', - expected: '2y', - }, - { - reference: '2021-04-24T12:00:00.000Z', - datetime: '2023-02-01T12:00:00.000Z', - tense: 'future', - format: 'micro', - expected: '2y', - }, - ]) - - for (const { - datetime, - expected, - tense, - format, - formatStyle, - precision = '', - lang, - reference = referenceDate, - } of tests) { - const attrs = Object.entries({ - datetime, - tense, - format, - formatStyle, - precision, - }).map(([k, v]) => (v ? `${k}="${v}"` : '')) - test(` => ${expected}`, async () => { - freezeTime(new Date(reference)) - const time = document.createElement('relative-time') - time.setAttribute('datetime', datetime) - if (tense) time.setAttribute('tense', tense) - if (format) time.setAttribute('format', format) - if (precision) time.setAttribute('precision', precision) - if (lang) time.setAttribute('lang', lang) - if (formatStyle) time.formatStyle = formatStyle - await Promise.resolve() - assert.equal(time.shadowRoot.textContent, expected) - }) - } - }) })