the home site for me: also iteration 3 or 4 of my site
1class RelativeTimeElement extends HTMLElement {
2 static get observedAttributes() {
3 return ['datetime', 'threshold', 'prefix', 'format'];
4 }
5
6 connectedCallback() {
7 this.update();
8 }
9
10 disconnectedCallback() {
11 this.stopTimer();
12 }
13
14 attributeChangedCallback() {
15 this.update();
16 }
17
18 scheduleUpdate(ms) {
19 this.stopTimer();
20 this.timer = setTimeout(() => this.update(), ms);
21 }
22
23 stopTimer() {
24 if (this.timer) {
25 clearTimeout(this.timer);
26 this.timer = null;
27 }
28 }
29
30 get datetime() {
31 return this.getAttribute('datetime') || '';
32 }
33
34 get threshold() {
35 return this.getAttribute('threshold') || 'P14D';
36 }
37
38 get prefix() {
39 return this.getAttribute('prefix') || 'on';
40 }
41
42 get format() {
43 return this.getAttribute('format') || 'relative';
44 }
45
46 parseThreshold(iso) {
47 const match = iso.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
48 if (!match) return 30 * 24 * 60 * 60 * 1000;
49 const days = parseInt(match[1] || 0, 10);
50 const hours = parseInt(match[2] || 0, 10);
51 const minutes = parseInt(match[3] || 0, 10);
52 const seconds = parseInt(match[4] || 0, 10);
53 return ((days * 24 + hours) * 60 + minutes) * 60 * 1000 + seconds * 1000;
54 }
55
56 update() {
57 const datetime = this.datetime;
58 if (!datetime) return;
59
60 const date = new Date(datetime);
61 if (isNaN(date.getTime())) return;
62
63 const now = Date.now();
64 const diff = now - date.getTime();
65 const absDiff = Math.abs(diff);
66 const thresholdMs = this.parseThreshold(this.threshold);
67
68 if (this.format === 'datetime' || absDiff > thresholdMs) {
69 this.textContent = this.formatDatetime(date);
70 this.scheduleUpdate(3600000);
71 } else {
72 this.textContent = this.formatRelative(diff);
73 const delay = this.getNextUpdateDelay(absDiff);
74 if (delay !== null) this.scheduleUpdate(delay);
75 }
76 }
77
78 getNextUpdateDelay(absDiff) {
79 const seconds = Math.floor(absDiff / 1000);
80 const minutes = Math.floor(seconds / 60);
81 const hours = Math.floor(minutes / 60);
82 const days = Math.floor(hours / 24);
83
84 if (seconds < 60) {
85 return 1000;
86 } else if (minutes < 60) {
87 return 60000;
88 } else if (hours < 24) {
89 return 60000 * 5;
90 } else if (days < 7) {
91 return 3600000;
92 } else if (days < 30) {
93 return 3600000 * 6;
94 } else {
95 return null;
96 }
97 }
98
99 getRtf() {
100 if (!this._rtf || this._rtfLang !== navigator.language) {
101 this._rtfLang = navigator.language;
102 this._rtf = new Intl.RelativeTimeFormat(navigator.language, {
103 numeric: 'auto',
104 style: 'long'
105 });
106 }
107 return this._rtf;
108 }
109
110 formatRelative(diff) {
111 const rtf = this.getRtf();
112
113 const absDiff = Math.abs(diff);
114 const sign = diff > 0 ? -1 : 1;
115 const seconds = Math.floor(absDiff / 1000);
116 const minutes = Math.floor(seconds / 60);
117 const hours = Math.floor(minutes / 60);
118 const days = Math.floor(hours / 24);
119 const date = new Date(Date.now() - diff);
120 const now = new Date();
121 const months = (now.getFullYear() - date.getFullYear()) * 12 + (now.getMonth() - date.getMonth());
122 const years = Math.floor(Math.abs(months) / 12) * sign;
123
124 if (seconds < 60) {
125 return rtf.format(sign * seconds, 'second');
126 } else if (minutes < 60) {
127 return rtf.format(sign * minutes, 'minute');
128 } else if (hours < 24) {
129 return rtf.format(sign * hours, 'hour');
130 } else if (days < 30) {
131 return rtf.format(sign * days, 'day');
132 } else if (Math.abs(months) < 12) {
133 return rtf.format(months, 'month');
134 } else {
135 return rtf.format(years, 'year');
136 }
137 }
138
139 formatDatetime(date) {
140 const now = new Date();
141 const sameYear = date.getFullYear() === now.getFullYear();
142
143 const options = {
144 month: 'short',
145 day: 'numeric',
146 ...(sameYear ? {} : { year: 'numeric' })
147 };
148
149 const prefix = this.prefix;
150 const formatted = new Intl.DateTimeFormat(navigator.language, options).format(date);
151 return prefix ? `${prefix} ${formatted}` : formatted;
152 }
153}
154
155customElements.define('relative-time', RelativeTimeElement);