PageSourceSearch

https://granitestatereport.com/wp-content/plugins/gsr-header-balance/assets/balance.js?m=1791207833

js granitestatereport.com collected 2026-10-07 03:17:45 UTC 11,017 bytes, 291 lines download raw bytes

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.