1/* GSR Header Balance v1.1.0 · 2026-10-05. Rebalances the masthead row; sends nothing anywhere. 2 Works with GSR Send Bar 1.1.x through 1.3.x. Send Bar owns the buttons and their menus; this file only 3 moves the Editor (G) button to the outer left edge, labels the right side, and keeps the logo centered. 4 API for other GSR header tools: 5 window.GSR_HB.slot() -> the right-side reader-tools container (#gsr-hb-tools), or null 6 window.GSR_HB.rebalance() -> re-measure after you change the header 7 document 'gsr-hb:ready' -> fires once the slot exists 8 Anything added to .header-left or .header-right is picked up and the logo re-centered. */ 9(function () { 10 'use strict'; 11 12 var NETS = { 13 'a-facebook': 'Facebook', 'a-youtube': 'YouTube', 'a-medium': 'Medium', 'a-linkedin': 'LinkedIn', 14 'a-instagram': 'Instagram', 'a-twitter': 'X', 'a-threads': 'Threads', 'a-bluesky': 'Bluesky', 15 'a-tiktok': 'TikTok', 'a-rss': 'RSS' 16 }; 17 var DESK = window.matchMedia('(min-width: 992px)'); 18 var CARET = '<svg class="gsr-sb-caret" viewBox="0 0 10 6" width="8" height="5" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m1 1 4 4 4-4"/></svg>'; 19 var FOLLOW_ICON = '<svg class="gsr-sb-svg" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="m8.3 13.3 7.4 4.4"/><path d="m15.7 6.3-7.4 4.4"/></svg>'; 20 var queued = false; 21 var moving = false; 22 23 function mainHeader() { return document.querySelector('header.main-header:not(.fixed)'); } 24 25 function netName(a) { 26 var n = ''; 27 Object.keys(NETS).forEach(function (k) { if (a.classList.contains(k)) n = NETS[k]; }); 28 return n; 29 } 30 31 /* ---------- Follow menu ---------- */ 32 var fpop = null; 33 var fopen = null; 34 35 function buildFollow() { 36 var h = mainHeader(); 37 var soc = h && h.querySelector('.social-icons-wrapper'); 38 if (!soc) return null; 39 var pop = document.createElement('div'); 40 pop.className = 'gsr-sb-pop gsr-hb-pop'; 41 pop.id = 'gsr-hb-pop-follow'; 42 pop.setAttribute('role', 'dialog'); 43 pop.setAttribute('aria-label', 'Follow Granite State Report'); 44 pop.hidden = true; 45 var hd = document.createElement('p'); 46 hd.className = 'gsr-sb-pop-h'; 47 hd.textContent = 'Follow Granite State Report'; 48 var sub = document.createElement('p'); 49 sub.className = 'gsr-sb-pop-sub'; 50 sub.textContent = 'Each opens in a new tab.'; 51 var ul = document.createElement('ul'); 52 ul.className = 'gsr-sb-list gsr-sb-list-ico'; 53 Array.prototype.forEach.call(soc.querySelectorAll('a[href]'), function (src) { 54 var net = netName(src); 55 var host = ''; 56 try { host = new URL(src.href).hostname.replace(/^www\./, ''); } catch (e) { host = ''; } 57 var li = document.createElement('li'); 58 var a = document.createElement('a'); 59 a.href = src.href; 60 a.target = '_blank'; 61 a.rel = 'noopener'; 62 var icon = src.querySelector('i'); 63 var ic = document.createElement('i'); 64 ic.className = (icon ? icon.className : 'fa fa-link') + ' gsr-hb-net'; 65 ic.setAttribute('aria-hidden', 'true'); 66 var b = document.createElement('b'); 67 b.textContent = net || host || 'Link'; 68 var sp = document.createElement('span'); 69 sp.textContent = host; 70 a.appendChild(ic); 71 a.appendChild(b); 72 a.appendChild(sp); 73 li.appendChild(a); 74 ul.appendChild(li); 75 }); 76 pop.appendChild(hd); 77 pop.appendChild(sub); 78 pop.appendChild(ul); 79 document.body.appendChild(pop); 80 return pop; 81 } 82 83 function placeFollow(btn) { 84 var r = btn.getBoundingClientRect(); 85 var vw = document.documentElement.clientWidth; 86 var top = Math.round(r.bottom + 8); 87 fpop.style.top = top + 'px'; 88 fpop.style.maxHeight = Math.max(200, window.innerHeight - top - 16) + 'px'; 89 fpop.style.overflowY = 'auto'; 90 var w = fpop.offsetWidth; 91 fpop.style.left = Math.round(Math.max(12, Math.min(r.right - w, vw - w - 12))) + 'px'; 92 } 93 function closeFollow(returnFocus) { 94 if (!fopen) return; 95 fpop.hidden = true; 96 fopen.setAttribute('aria-expanded', 'false'); 97 var b = fopen; 98 fopen = null; 99 if (returnFocus) b.focus(); 100 } 101 function wireFollow() { 102 document.addEventListener('click', function (e) { 103 var t = e.target; 104 if (!t || !t.closest) return; 105 var btn = t.closest('.gsr-hb-follow-btn'); 106 if (btn) { 107 if (!fpop) fpop = buildFollow(); 108 if (!fpop) return; 109 var same = fopen === btn; 110 closeFollow(false); 111 if (same) return; 112 fpop.hidden = false;
113 placeFollow(btn); 114 btn.setAttribute('aria-expanded', 'true'); 115 fopen = btn; 116 return; 117 } 118 if (fopen && fpop && !fpop.contains(t)) closeFollow(false); 119 }, true); 120 document.addEventListener('keydown', function (e) { 121 if (e.key === 'Escape' && fopen) closeFollow(true); 122 }); 123 window.addEventListener('resize', function () { closeFollow(false); }); 124 window.addEventListener('scroll', function () { if (fopen) closeFollow(false); }, { passive: true }); 125 } 126 127 function cap(text) { 128 var s = document.createElement('span'); 129 s.className = 'gsr-hb-cap'; 130 s.setAttribute('aria-hidden', 'true'); 131 s.textContent = text; 132 return s; 133 } 134 135 /* 1. Editor (G): first item of the send row on the left, whatever group Send Bar filed it under */ 136 function placeG() { 137 var h = mainHeader(); 138 if (!h) return; 139 var row = h.querySelector('#gsr-sendbar > .gsr-sb-row'); 140 var g = h.querySelector('.gsr-sb-g'); 141 var li = g && g.closest('.gsr-sb-item'); 142 if (!row || !li) return; 143 li.classList.add('gsr-hb-g-item'); 144 if (row.firstElementChild !== li) { 145 moving = true; 146 row.insertBefore(li, row.firstElementChild); 147 moving = false; 148 } 149 /* its menu now opens from the left edge, so anchor it left */ 150 var pop = document.getElementById(g.getAttribute('aria-controls') || ''); 151 if (pop) pop.classList.remove('gsr-sb-pop-right'); 152 } 153 154 function prep(h) { 155 if (h.getAttribute('data-gsr-hb')) return; 156 h.setAttribute('data-gsr-hb', '1'); 157 var main = !h.classList.contains('fixed'); 158 159 var right = h.querySelector('.header-right'); 160 if (!right) return; 161 right.classList.add('gsr-hb-right'); 162 163 /* 2. the reader-tools slot */ 164 if (!right.querySelector('.gsr-hb-tools')) { 165 var slot = document.createElement('div'); 166 slot.className = 'gsr-sb gsr-hb-tools'; 167 slot.setAttribute('role', 'group'); 168 slot.setAttribute('aria-label', 'Reader tools'); 169 if (main) slot.id = 'gsr-hb-tools'; 170 right.insertBefore(slot, right.firstChild); 171 } 172 173 /* 3. social icons: each link named for screen readers (the theme's row still shows on tablets) */ 174 var soc = right.querySelector('.social-icons-wrapper'); 175 if (soc && !soc.getAttribute('data-gsr-hb')) { 176 soc.setAttribute('data-gsr-hb', '1'); 177 soc.setAttribute('role', 'group'); 178 soc.setAttribute('aria-label', 'Follow Granite State Report'); 179 Array.prototype.forEach.call(soc.querySelectorAll('a'), function (a) { 180 var net = netName(a); 181 if (net && !a.getAttribute('aria-label')) { 182 a.setAttribute('aria-label', 'Granite State Report on ' + net + (a.target === '_blank' ? ' (opens in a new tab)' : '')); 183 } 184 if (a.target === '_blank') { 185 var rel = (a.getAttribute('rel') || '').split(/\s+/).filter(Boolean); 186 if (rel.indexOf('noopener') < 0) rel.push('noopener'); 187 a.setAttribute('rel', rel.join(' ')); 188 } 189 }); 190 } 191 192 /* 3b. desktop: the same links as one Follow button with a menu, so each side carries three buttons */ 193 if (soc && soc.querySelector('a[href]') && !right.querySelector('.gsr-hb-follow')) { 194 var fw = document.createElement('div'); 195 fw.className = 'gsr-sb gsr-hb-follow'; 196 var fb = document.createElement('button'); 197 fb.type = 'button'; 198 fb.className = 'gsr-sb-btn gsr-hb-follow-btn'; 199 fb.setAttribute('aria-expanded', 'false'); 200 fb.setAttribute('aria-controls', 'gsr-hb-pop-follow'); 201 fb.title = 'Follow Granite State Report'; 202 fb.innerHTML = FOLLOW_ICON + '<span class="gsr-sb-lbl">Follow' + CARET + '</span>'; 203 fw.appendChild(fb); 204 /* in DOM order too, so Tab moves Saved, Follow, Search the way the eye does */ 205 var sw = right.querySelector('.search-toggle-wrapper'); 206 if (sw && sw.parentNode === right) right.insertBefore(fw, sw); 207 else right.appendChild(fw); 208 } 209 210 /* 4. search: a label, and reachable by keyboard */ 211 var sb = right.querySelector('.search-toggle-btn'); 212 if (sb && !sb.querySelector('.gsr-hb-cap')) { 213 sb.appendChild(cap('Search')); 214 if (!sb.hasAttribute('href')) { 215 if (!sb.hasAttribute('tabindex')) sb.setAttribute('tabindex', '0'); 216 sb.setAttribute('role', 'button'); 217 sb.addEventListener('keydown', function (e) { 218 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sb.click(); } 219 }); 220 } 221 } 222 } 223 224 function natural(el) { 225 if (!el) return 0; 226 return Math.max(el.scrollWidth || 0, el.getBoundingClientRect().width || 0); 227 } 228 229 function balance() { 230 queued = false;
231 var h = mainHeader(); 232 var row = h && h.querySelector('.row > .col-md-12'); 233 if (!row) return; 234 placeG(); 235 row.classList.add('gsr-hb-on'); 236 if (!DESK.matches) { row.style.removeProperty('--gsr-hb-side'); return; } 237 var side = Math.ceil(Math.max(natural(h.querySelector('.header-left')), natural(h.querySelector('.header-right')))); 238 if (side > 0 && row.style.getPropertyValue('--gsr-hb-side') !== side + 'px') { 239 row.style.setProperty('--gsr-hb-side', side + 'px'); 240 } 241 } 242 243 function schedule() { 244 if (queued) return; 245 queued = true; 246 window.requestAnimationFrame(balance); 247 } 248 249 function init() { 250 Array.prototype.forEach.call(document.querySelectorAll('header.main-header'), prep); 251 placeG(); 252 balance(); 253 254 var h = mainHeader(); 255 if (h && window.MutationObserver) { 256 var mo = new MutationObserver(function () { if (!moving) schedule(); }); 257 ['.header-left', '.header-right'].forEach(function (s) { 258 var el = h.querySelector(s); 259 if (el) mo.observe(el, { childList: true, subtree: true }); 260 }); 261 } 262 if (h && window.ResizeObserver) { 263 var ro = new ResizeObserver(schedule); 264 ['.header-left', '.header-right'].forEach(function (s) { 265 var el = h.querySelector(s); 266 if (el) ro.observe(el); 267 }); 268 } 269 window.addEventListener('resize', schedule, { passive: true }); 270 if (DESK.addEventListener) DESK.addEventListener('change', schedule); 271 else if (DESK.addListener) DESK.addListener(schedule); 272 if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule); 273 window.addEventListener('load', schedule); 274 wireFollow(); 275 276 window.GSR_HB = { 277 version: '1.1.0', 278 slot: function () { 279 var hd = mainHeader(); 280 return hd ? hd.querySelector('.gsr-hb-tools') : null; 281 }, 282 rebalance: schedule 283 }; 284 try { 285 document.dispatchEvent(new CustomEvent('gsr-hb:ready')); 286 } catch (e) { /* old browsers: tools place themselves */ } 287 } 288 289 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 290 else init(); 291})();
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.