PageSourceSearch

https://niktsanka.github.io/weatherapp/js/clock.js?v=10

js niktsanka.github.io collected 2026-10-03 09:41:17 UTC 9,998 bytes, 276 lines download raw bytes

1/* clock.js - ONE ticker for the whole app.
2   A single setInterval walks every registered clock and writes into existing text nodes.
3   No per-card timer, and no innerHTML on a tick: re-rendering a card every second would
4   destroy focus, restart animations and thrash the layout.
5
6   Time is always computed locally from the IANA zone. The API's `time.iso` is a snapshot
7   of when the file was generated, not a clock source, so it is never used here. */
8(function (global) {
9  'use strict';
10
11  var WTW = global.WTW = global.WTW || {};
12  var doc = global.document;
13
14  /* Pick the display locale once. navigator.language can be undefined or a weird tag,
15     so fall back explicitly rather than letting Intl choose per call. */
16  var LOCALE = (function () {
17    var tag = global.navigator && global.navigator.language;
18    if (typeof tag !== 'string' || tag.length < 2) { return 'en-GB'; }
19    try { new Intl.DateTimeFormat(tag); return tag; } catch (err) { return 'en-GB'; }
20  })();
21
22  var entries = [];
23  var timer = null;
24  var hour12 = false;
25  var formatters = Object.create(null);
26  var supported = Object.create(null);
27
28  function zoneSupported(timezone) {
29    if (!timezone) { return false; }
30    if (timezone in supported) { return supported[timezone]; }
31    var ok = false;
32    try {
33      new Intl.DateTimeFormat('en-GB', { timeZone: timezone }).format(new Date());
34      ok = true;
35    } catch (err) { ok = false; }
36    supported[timezone] = ok;
37    return ok;
38  }
39
40  /* hour12 is passed explicitly, together with the matching hourCycle. Relying on the
41     locale default would make the 12/24-hour toggle a no-op in some locales. */
42  function timeFormatter(timezone) {
43    var key = 't|' + timezone + '|' + hour12;
44    if (!formatters[key]) {
45      formatters[key] = new Intl.DateTimeFormat(LOCALE, {
46        timeZone: timezone,
47        hour: '2-digit', minute: '2-digit', second: '2-digit',
48        hour12: hour12, hourCycle: hour12 ? 'h12' : 'h23'
49      });
50    }
51    return formatters[key];
52  }
53
54  function dateFormatter(timezone) {
55    var key = 'd|' + timezone;
56    if (!formatters[key]) {
57      formatters[key] = new Intl.DateTimeFormat(LOCALE, {
58        timeZone: timezone, weekday: 'long', day: 'numeric', month: 'long'
59      });
60    }
61    return formatters[key];
62  }
63
64  function pad(n) { return (n < 10 ? '0' : '') + n; }
65
66  /* Degraded path: the zone is unknown to this browser's Intl, so shift UTC by the
67     offset the API reported. Correct until that zone next changes its offset. */
68  function offsetParts(offsetSeconds, date) {
69    var shifted = new Date(date.getTime() + (offsetSeconds || 0) * 1000);
70    var h = shifted.getUTCHours();
71    var display = h;
72    var period = '';
73    if (hour12) {
74      period = h < 12 ? 'AM' : 'PM';
75      display = h % 12 === 0 ? 12 : h % 12;
76    }
77    return {
78      hour: pad(display), minute: pad(shifted.getUTCMinutes()), second: pad(shifted.getUTCSeconds()),
79      period: period, hour24: h
80    };
81  }
82
83  function intlParts(timezone, date) {
84    var parts = timeFormatter(timezone).formatToParts(date);
85    var out = { hour: '00', minute: '00', second: '00', period: '', hour24: 0 };
86    for (var i = 0; i < parts.length; i++) {
87      var p = parts[i];
88      if (p.type === 'hour') { out.hour = p.value; }
89      else if (p.type === 'minute') { out.minute = p.value; }
90      else if (p.type === 'second') { out.second = p.value; }
91      else if (p.type === 'dayPeriod') { out.period = p.value; }
92    }
93    out.hour24 = hour12 ? to24(out.hour, out.period) : parseInt(out.hour, 10) || 0;
94    return out;
95  }
96
97  function to24(hourText, period) {
98    var h = parseInt(hourText, 10) || 0;
99    var isPm = /p/i.test(period || '');
100    if (h === 12) { return isPm ? 12 : 0; }
101    return isPm ? h + 12 : h;
102  }
103
104  function span(className, text) {
105    var el = doc.createElement('span');
106    if (className) { el.className = className; }
107    el.textContent = text || '';
108    return el;
109  }
110
111  function tick() {
112    var now = new Date();
113    for (var i = 0; i < entries.length; i++) {
114      render(entries[i], now);
115    }
116  }
117
118  function render(entry, now) {
119    var parts = entry.degraded ? offsetParts(entry.offsetSeconds, now) : intlParts(entry.timezone, now);
120
121    if (entry.secEl.nodeValue !== parts.second) { entry.secEl.nodeValue = parts.second; }
122
123    /* Only touch the hour/minute nodes when they actually change: they sit inside the
124       aria-live region, so a needless write would re-announce the same minute. */
125    var hm = parts.hour + ':' + parts.minute + '|' + parts.period;
126    if (entry.lastHM !== hm) {
127      entry.lastHM = hm;
128      entry.hourEl.nodeValue = parts.hour;
129      entry.minuteEl.nodeValue = parts.minute;
130      if (entry.periodEl) { entry.periodEl.textContent = parts.period ? ' ' + parts.period : ''; }
131
132      if (entry.dateEl) {
133        var dateText = entry.degraded
134          ? new Date(now.getTime() + (entry.offsetSeconds || 0) * 1000).toUTCString().slice(0, 16)
135          : dateFormatter(entry.timezone).format(now);
136        if (entry.dateEl.textContent !== dateText) { entry.dateEl.textContent = dateText; }
137      }
138
139      var phase = WTW.weather.phaseForHour(parts.hour24);
140      if (phase !== entry.lastPhase) {
141        entry.lastPhase = phase;
142        if (entry.onPhase) { entry.onPhase(phase); }
143      }
144    }
145  }
146
147  function start() {
148    if (timer === null) { timer = global.setInterval(tick, 1000); }
149  }
150
151  function stop() {
152    if (timer !== null) { global.clearInterval(timer); timer = null; }
153  }
154
155  var clock = {
156    LOCALE: LOCALE,
157    zoneSupported: zoneSupported,
158
159    /* container: the element carrying aria-live="polite". We build its inner spans so
160       this module owns the text nodes it has to update. */
161    register: function (container, options) {
162      var opts = options || {};
163      var degraded = !zoneSupported(opts.timezone);
164
165      var hmEl = span('clock__hm', '');
166      var hourNode = doc.createTextNode('00');
167      var minuteNode = doc.createTextNode('00');
168      hmEl.appendChild(hourNode);
169      hmEl.appendChild(span('clock__colon', ':'));
170      hmEl.appendChild(minuteNode);
171
172      var secEl = span('clock__sec', '');
173      secEl.setAttribute('aria-hidden', 'true');
174      var secNode = doc.createTextNode('00');
175      secEl.appendChild(doc.createTextNode(':'));
176      secEl.appendChild(secNode);
177
178      container.textContent = '';
179      container.appendChild(hmEl);
180      container.appendChild(secEl);
181
182      /* AM/PM goes after the seconds so the line reads "11:00:34 AM". It stays outside
183         the aria-hidden seconds so it is still announced with the minute. */
184      var periodEl = null;
185      if (hour12) { periodEl = span('clock__suffix', ''); container.appendChild(periodEl); }
186
187      var entry = {
188        timezone: opts.timezone,
189        offsetSeconds: opts.offsetSeconds || 0,
190        degraded: degraded,
191        hourEl: hourNode,
192        minuteEl: minuteNode,
193        periodEl: periodEl,
194        secEl: secNode,
195        dateEl: opts.dateEl || null,
196        onPhase: opts.onPhase || null,
197        lastHM: null,
198        lastPhase: null
199      };
200
201      entries.push(entry);
202      render(entry, new Date());
203      start();
204      return {
205        degraded: degraded,
206        destroy: function () {
207          var i = entries.indexOf(entry);
208          if (i >= 0) { entries.splice(i, 1); }
209          if (entries.length === 0) { stop(); }
210        }
211      };
212    },
213
214    clear: function () { entries.length = 0; stop(); },
215
216    /* Changing the 12/24-hour setting rebuilds every clock's markup, so ui.js re-renders
217       rather than calling this mid-flight. Kept for the Phase 2 settings panel. */
218    setHour12: function (value) {
219      hour12 = value === true;
220      formatters = Object.create(null);
221    },
222
223    isHour12: function () { return hour12; },
224
225    /* Exposed for the detail view and (Phase 3) the planner. */
226    hourIn: function (timezone, date, offsetSeconds) {
227      var d = date || new Date();
228      return zoneSupported(timezone) ? intlParts(timezone, d).hour24 : offsetParts(offsetSeconds, d).hour24;
229    },
230
231    /* The zone's offset ON A GIVEN DATE, computed by Intl. This is the only correct way
232       to ask the question: the API's utc_offset_seconds is today's offset and would be
233       wrong on the other side of a DST boundary. Returns seconds, or null if the browser
234       does not know the zone. */
235    offsetAt: function (timezone, date) {
236      if (!zoneSupported(timezone)) { return null; }
237      var key = 'o|' + timezone;
238      if (!formatters[key]) {
239        formatters[key] = new Intl.DateTimeFormat('en-GB', {
240          timeZone: timezone, timeZoneName: 'longOffset'
241        });
242      }
243      var parts = formatters[key].formatToParts(date || new Date());
244      var name = '';
245      for (var i = 0; i < parts.length; i++) {
246        if (parts[i].type === 'timeZoneName') { name = parts[i].value; }
247      }
248      /* "GMT" alone means UTC;
248 otherwise "GMT+05:45" / "GMT-03:30". */
249      var m = /GMT([+-])(\d{1,2}):?(\d{2})?/.exec(name);
250      if (!m) { return /GMT/.test(name) ? 0 : null; }
251      var seconds = (parseInt(m[2], 10) * 3600) + (parseInt(m[3] || '0', 10) * 60);
252      return m[1] === '-' ? -seconds : seconds;
253    },
254
255    /* The viewer's own zone, for the DST comparison. Undefined on very old engines. */
256    userTimeZone: function () {
257      try {
258        var tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
259        return typeof tz === 'string' && tz ? tz : null;
260      } catch (err) { return null; }
261    },
262
263    longDate: function (timezone, date) {
264      if (!zoneSupported(timezone)) { return null; }
265      var key = 'l|' + timezone;
266      if (!formatters[key]) {
267        formatters[key] = new Intl.DateTimeFormat(LOCALE, {
268          timeZone: timezone, weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
269        });
270      }
271      return formatters[key].format(date || new Date());
272    }
273  };
274
275  WTW.clock = clock;
276})(typeof window !== 'undefined' ? window : globalThis);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.