PageSourceSearch

https://setform.surf/js-min/views/spot-header.js?v=20261005b

js setform.surf collected 2026-10-06 13:39:17 UTC 6,161 bytes, 165 lines download raw bytes

1const FORECAST_NOTE = {
2full: 'Surf forecast for open water near the coast.',
3short: 'Open-water surf forecast.',
4};
5const RECORD_NOTE_SHORT = 'Past conditions, not a forecast.';
6function recordNoteFull(historical) {
7const d = historical?.date ? new Date(`${historical.date}T12:00:00Z`) : null;
8if (!d || Number.isNaN(d.getTime())) return RECORD_NOTE_SHORT;
9const day = new Intl.DateTimeFormat('en-GB', {
10day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC',
11}).format(d);
12return `Conditions on ${day}, not a forecast.`;
13}
14function seaTempDegrees(celsius, unit) {
15if (typeof celsius !== 'number' || !Number.isFinite(celsius)) return null;
16return unit === 'ft' ? Math.round(celsius * 9 / 5 + 32) : Math.round(celsius);
17}
18export function formatSeaTemp(celsius, unit) {
19const n = seaTempDegrees(celsius, unit);
20return n === null ? null : `${n}°${unit === 'ft' ? 'F' : 'C'}`;
21}
22function spokenSeaTemp(celsius, unit) {
23const n = seaTempDegrees(celsius, unit);
24return n === null ? '' : `Water temperature, ${n} degrees ${unit === 'ft' ? 'Fahrenheit' : 'Celsius'}`;
25}
26let els = null;
27const last = { placeFull: null, placeShort: null, temp: null, tempShown: null, noteFull: null, noteShort: null, timeText: '', timeShown: null, historical: false };
28function refs() {
29if (els) return els;
30const root = document.getElementById('spot-header');
31if (!root) return null;
32els = {
33root,
34place: document.getElementById('spot-header-place'),
35temp: document.getElementById('spot-header-temp'),
36tempValue: document.getElementById('spot-header-temp-value'),
37tempSpoken: document.getElementById('spot-header-temp-sr'),
38note: document.getElementById('spot-header-note'),
39time: document.getElementById('spot-header-time'),
40sep: document.getElementById('spot-header-sep'),
41};
42window.addEventListener('resize', fitSpotHeader);
43document.fonts?.ready?.then(fitSpotHeader);
44if (typeof ResizeObserver !== 'undefined') new ResizeObserver(fitSpotHeader).observe(root);
45return els;
46}
47function setText(el, key, text) {
48if (last[key] === text) return;
49last[key] = text;
50el.textContent = text;
51}
52export function renderSpotHeader(state, { ready } = {}) {
53const r = refs();
54if (!r) return;
55const show = !!ready && !!state.locationName;
56if (r.root.hidden === show) r.root.hidden = !show;
57if (!show) return;
58const placeFull = state.section
59? `${state.locationName} (${state.section})`
60: state.locationName;
61const placeChanged = last.placeFull !== placeFull;
62last.placeFull = placeFull;
63last.placeShort = state.locationName;
64const historical = state.historicalRecord;
65const temp = historical ? null : formatSeaTemp(state.seaSurfaceTemp, state.unit);
66r.temp.hidden = !temp;
67if (temp) {
68setText(r.tempValue, 'temp', temp);
69if (r.tempSpoken) setText(r.tempSpoken, 'tempSpoken', spokenSeaTemp(state.seaSurfaceTemp, state.unit));
70}
71const tempChanged = last.tempShown !== (temp || '');
72last.tempShown = temp || '';
73if (last.historical !== !!historical) {
74last.historical = !!historical;
75applyTime();
76}
77const noteFull = historical ? recordNoteFull(historical) : FORECAST_NOTE.full;
78const noteChanged = last.noteFull !== noteFull;
79if (noteChanged) {
80last.noteFull = noteFull;
81last.noteShort = historical ? RECORD_NOTE_SHORT : FORECAST_NOTE.short;
82}
83if (placeChanged || tempChanged || noteChanged) fitSpotHeader();
84}
85export function setSpotHeaderTime(text) {
86last.timeText = text || '';
87applyTime();
88}
89function applyTime() {
90const r = refs();
91if (!r || !r.time) return;
92const shown = last.historical ? '' : last.timeText;
93if (shown === last.timeShown) return;
94last.timeShown = shown;
95r.time.textContent = shown;
96r.time.hidden = !shown;
97if (r.sep) r.sep.hidden = !shown;
98fitSpotHeader();
99}
100export function fitSpotHeader() {
101const r = refs();
102if (!r) return;
103syncCapLead(r);
104if (r.root.hidden) return;
105if (last.placeFull) fitLine(r.place, last.placeFull, last.placeShort);
106if (last.noteFull) {
107const widest = widestTimeWidth(r);
108if (widest) r.time.style.minWidth = `${widest}px`;
109fitLine(r.note, last.noteFull, last.noteShort);
110if (widest) r.time.style.removeProperty('min-width');
111}
112}
113const TIME_PREFIXES = ['Now', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
114let timeCtx = null;
115const widestCache = new Map();
116function widestTimeWidth(r) {
117const text = last.timeShown;
118if (!text || !r.time || r.time.hidden) return 0;
119const cs = getComputedStyle(r.time);
120const font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
121timeCtx = timeCtx || document.createElement('canvas').getContext('2d');
122if (!timeCtx) return 0;
123timeCtx.font = font;
124const twelveHour = /[AP]M$/.test(text);
125const key = `${font}|${twelveHour}`;
126let widest = widestCache.get(key);
127if (widest == null) {
128const w = (t) => timeCtx.measureText(t).width;
129const digit = [...'0123456789'].reduce((a, d) => (w(d) > w(a) ? d : a));
130const prefix = TIME_PREFIXES.reduce((a, p) => (w(p) > w(a) ? p : a));
131const hm = `${digit}${digit}:${digit}${digit}`;
132const suffix = twelveHour ? (w('AM') > w('PM') ? ' AM' : ' PM') : '';
133widest = w(`${prefix} ${hm}${suffix}`);
134widestCache.set(key, widest);
135}
136return Math.ceil(widest);
137}
138let capCtx = null;
139let lastCapLead = null;
140function syncCapLead(r) {
141if (!r.place) return;
142const cs = getComputedStyle(r.place);
143const size = parseFloat(cs.fontSize);
144if (!(size > 0)) return;
145capCtx = capCtx || document.createElement('canvas').getContext('2d');
146if (!capCtx) return;
147capCtx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
148const m = capCtx.measureText('H');
149const ascent = m.fontBoundingBoxAscent;
150const descent = m.fontBoundingBoxDescent;
151const cap = m.actualBoundingBoxAscent;
152if (!(ascent > 0) || !(cap > 0)) return;
153const lineHeight = parseFloat(cs.lineHeight);
154const halfLeading = lineHeight > 0 ? (lineHeight - (ascent + (descent || 0))) / 2 : 0;
155const lead = Math.max(0, halfLeading + ascent - cap);
156const px = `${lead.toFixed(2)}px`;
157if (px === lastCapLead) return;
158lastCapLead = px;
159r.root.style.setProperty('--spot-header-cap-lead', px);
160}
161function fitLine(el, full, short) {
162if (el.textContent !== full) el.textContent = full;
163if (el.clientWidth === 0 || short === full) return;
164if (el.scrollWidth > el.clientWidth + 1) el.textContent = short;
165}

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.