Skip to content

Commit a81bcb1

Browse files
eyevanabennypowers
andauthored
feat: timestamp controller (#2524)
* feat: create timestamp controller class * refactor: move relative calculation to controller * refactor: move date logic to controller * refactor: rename format type * Update elements/pf-timestamp/pf-timestamp.ts Co-authored-by: Benny Powers <[email protected]> * Update elements/pf-timestamp/pf-timestamp.ts Co-authored-by: Benny Powers <[email protected]> * fix: rename controller * refactor: code review feedback * feat: sync updates between host and controller * refactor: apply code review feedback * refactor(core): make timestamp props public and options private * fix(core): wrangle typescript in timestamp controller * fix: timestamp defaults * docs: changeset --------- Co-authored-by: Benny Powers <[email protected]> Co-authored-by: Benny Powers <[email protected]>
1 parent f4a7ae7 commit a81bcb1

File tree

4 files changed

+167
-64
lines changed

4 files changed

+167
-64
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
"@patternfly/pfe-core": minor
3+
---
4+
**Controllers**: Added timestamp controller
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
2+
3+
export type DateTimeFormat = 'full' | 'long' | 'medium' | 'short';
4+
5+
export interface TimestampOptions {
6+
dateFormat?: DateTimeFormat;
7+
timeFormat?: DateTimeFormat;
8+
customFormat?: Intl.DateTimeFormatOptions;
9+
displaySuffix: string;
10+
locale: Intl.LocalesArgument;
11+
relative: boolean;
12+
utc: boolean;
13+
hour12: boolean;
14+
}
15+
16+
const defaults = {
17+
dateFormat: undefined,
18+
timeFormat: undefined,
19+
customFormat: undefined,
20+
displaySuffix: '',
21+
locale: undefined,
22+
relative: false,
23+
utc: false,
24+
hour12: false,
25+
} as const;
26+
27+
export class TimestampController implements ReactiveController {
28+
static #isTimestampOptionKey(prop: PropertyKey): prop is keyof TimestampOptions {
29+
return prop in defaults;
30+
}
31+
32+
#date = new Date();
33+
34+
#options: TimestampOptions = {} as TimestampOptions;
35+
36+
#host: ReactiveControllerHost;
37+
38+
get localeString() {
39+
return this.#date.toLocaleString(this.#options.locale);
40+
}
41+
42+
get date() {
43+
return this.#date;
44+
}
45+
46+
set date(string) {
47+
this.#date = new Date(string);
48+
}
49+
50+
get isoString() {
51+
return this.#date.toISOString();
52+
}
53+
54+
get time() {
55+
if (this.#options.relative) {
56+
return this.#getTimeRelative();
57+
} else {
58+
let { displaySuffix, locale } = this.#options;
59+
if (this.#options.utc) {
60+
displaySuffix ||= 'UTC';
61+
}
62+
const localeString = this.#date.toLocaleString(locale, this.#options.customFormat ?? {
63+
hour12: this.#options.hour12,
64+
timeStyle: this.#options.timeFormat,
65+
dateStyle: this.#options.dateFormat,
66+
...this.#options.utc && { timeZone: 'UTC' },
67+
});
68+
69+
return `${localeString} ${displaySuffix ?? ''}`.trim();
70+
}
71+
}
72+
73+
constructor(host: ReactiveControllerHost, options?: Partial<TimestampOptions>) {
74+
this.#host = host;
75+
host.addController(this);
76+
for (const [name, value] of Object.entries(this.#options)) {
77+
// @ts-expect-error: seems typescript compiler isn't up to the task here
78+
this.#options[name] = options?.[name] ?? value;
79+
}
80+
}
81+
82+
hostConnected?(): void
83+
84+
/**
85+
* Based off of Github Relative Time
86+
* https://github.com/github/time-elements/blob/master/src/relative-time.js
87+
*/
88+
#getTimeRelative() {
89+
const date = this.#date;
90+
const { locale } = this.#options;
91+
const rtf = new Intl.RelativeTimeFormat(locale as string, { localeMatcher: 'best fit', numeric: 'auto', style: 'long' });
92+
const ms: number = date.getTime() - Date.now();
93+
const tense = ms > 0 ? 1 : -1;
94+
let qty = 0;
95+
let units: Intl.RelativeTimeFormatUnit | undefined;
96+
const s = Math.round(Math.abs(ms) / 1000);
97+
const min = Math.round(s / 60);
98+
const h = Math.round(min / 60);
99+
const d = Math.round(h / 24);
100+
const m = Math.round(d / 30);
101+
const y = Math.round(m / 12);
102+
if (m >= 12) {
103+
qty = y;
104+
units = 'year';
105+
} else if (d >= 30) {
106+
qty = m;
107+
units = 'month';
108+
} else if (h >= 24) {
109+
qty = d;
110+
units = 'day';
111+
} else if (min >= 45) {
112+
qty = h;
113+
units = 'hour';
114+
} else if (s >= 45) {
115+
qty = min;
116+
units = 'minute';
117+
} else if (s >= 10) {
118+
qty = s;
119+
units = 'second';
120+
}
121+
122+
return typeof (units) !== 'undefined' ? rtf.format(tense * qty, units) : 'just now';
123+
}
124+
125+
set(prop: PropertyKey, value: unknown) {
126+
if (TimestampController.#isTimestampOptionKey(prop)) {
127+
// @ts-expect-error: seems typescript compiler isn't up to the task here
128+
this.#options[prop] = value;
129+
this.#host.requestUpdate();
130+
}
131+
}
132+
}

core/pfe-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js",
2929
"./controllers/slot-controller.js": "./controllers/slot-controller.js",
3030
"./controllers/style-controller.js": "./controllers/style-controller.js",
31+
"./controllers/timestamp-controller.js": "./controllers/timestamp-controller.js",
3132
"./decorators/bound.js": "./decorators/bound.js",
3233
"./decorators/cascades.js": "./decorators/cascades.js",
3334
"./decorators/deprecation.js": "./decorators/deprecation.js",

elements/pf-timestamp/pf-timestamp.ts

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import type { ComplexAttributeConverter } from 'lit';
1+
import type { ComplexAttributeConverter, PropertyValues } from 'lit';
22

33
import { LitElement, html } from 'lit';
44
import { customElement } from 'lit/decorators/custom-element.js';
55
import { property } from 'lit/decorators/property.js';
66

7+
import {
8+
TimestampController,
9+
type DateTimeFormat,
10+
} from '@patternfly/pfe-core/controllers/timestamp-controller.js';
11+
712
import style from './pf-timestamp.css';
813

914
const BooleanStringConverter: ComplexAttributeConverter = {
@@ -19,19 +24,9 @@ const BooleanStringConverter: ComplexAttributeConverter = {
1924
export class PfTimestamp extends LitElement {
2025
static readonly styles = [style];
2126

22-
@property({ reflect: true })
23-
get date() {
24-
return this.#date.toLocaleString();
25-
}
27+
@property({ reflect: true, attribute: 'date-format' }) dateFormat?: DateTimeFormat;
2628

27-
set date(string) {
28-
this.#date = new Date(string);
29-
this.#isoString = this.#date.toISOString();
30-
}
31-
32-
@property({ reflect: true, attribute: 'date-format' }) dateFormat?: 'full' | 'long' | 'medium' | 'short';
33-
34-
@property({ reflect: true, attribute: 'time-format' }) timeFormat?: 'full' | 'long' | 'medium' | 'short';
29+
@property({ reflect: true, attribute: 'time-format' }) timeFormat?: DateTimeFormat;
3530

3631
@property({ attribute: false }) customFormat?: object;
3732

@@ -45,72 +40,43 @@ export class PfTimestamp extends LitElement {
4540

4641
@property({ reflect: true, attribute: 'hour-12', converter: BooleanStringConverter }) hour12?: boolean;
4742

48-
#date = new Date();
43+
@property({ reflect: true })
44+
get date() {
45+
return this.#timestamp.localeString;
46+
}
4947

50-
#isoString = this.#date.toISOString();
48+
set date(string) {
49+
this.#timestamp.date = new Date(string);
50+
}
5151

5252
get isoString() {
53-
return this.#isoString;
53+
return this.#timestamp.isoString;
5454
}
5555

5656
get time() {
57-
const { hour12, customFormat, dateFormat: dateStyle, timeFormat: timeStyle, utc } = this;
58-
const timeZone = utc ? 'UTC' : undefined;
59-
const formatOptions = customFormat || { hour12, dateStyle, timeStyle, timeZone };
60-
const formattedDate = this.#date.toLocaleString(this.locale, formatOptions);
61-
return this.relative ? this.#getTimeRelative(this.#date) : `${formattedDate}${this.displaySuffix ? ` ${this.displaySuffix}` : ''}`;
57+
return this.#timestamp.time;
58+
}
59+
60+
#timestamp = new TimestampController(this);
61+
62+
connectedCallback() {
63+
super.connectedCallback();
64+
if (this.hasAttribute('date')) {
65+
this.#timestamp.date = new Date(this.getAttribute('date')!);
66+
}
6267
}
6368

64-
willUpdate() {
65-
if (!this.displaySuffix && this.utc) {
66-
this.displaySuffix = 'UTC';
69+
willUpdate(changedProperties: PropertyValues<this>) {
70+
for (const [prop] of changedProperties) {
71+
this.#timestamp.set(prop, this[prop as keyof this]);
6772
}
6873
}
6974

7075
render() {
7176
return html`
72-
<time datetime="${this.isoString}">${this.time}</time>
77+
<time datetime="${this.#timestamp.isoString}">${this.#timestamp.time}</time>
7378
`;
7479
}
75-
76-
/**
77-
* Based off of Github Relative Time
78-
* https://github.com/github/time-elements/blob/master/src/relative-time.js
79-
*/
80-
#getTimeRelative(date: Date) {
81-
const rtf = new Intl.RelativeTimeFormat(this.locale, { localeMatcher: 'best fit', numeric: 'auto', style: 'long' });
82-
const ms: number = date.getTime() - Date.now();
83-
const tense = ms > 0 ? 1 : -1;
84-
let qty = 0;
85-
let units: Intl.RelativeTimeFormatUnit | undefined;
86-
const s = Math.round(Math.abs(ms) / 1000);
87-
const min = Math.round(s / 60);
88-
const h = Math.round(min / 60);
89-
const d = Math.round(h / 24);
90-
const m = Math.round(d / 30);
91-
const y = Math.round(m / 12);
92-
if (m >= 12) {
93-
qty = y;
94-
units = 'year';
95-
} else if (d >= 30) {
96-
qty = m;
97-
units = 'month';
98-
} else if (h >= 24) {
99-
qty = d;
100-
units = 'day';
101-
} else if (min >= 45) {
102-
qty = h;
103-
units = 'hour';
104-
} else if (s >= 45) {
105-
qty = min;
106-
units = 'minute';
107-
} else if (s >= 10) {
108-
qty = s;
109-
units = 'second';
110-
}
111-
112-
return typeof (units) !== 'undefined' ? rtf.format(tense * qty, units) : 'just now';
113-
}
11480
}
11581

11682
declare global {

0 commit comments

Comments
 (0)