1/* blog-hub.js â VSHR Knowledge Hub interactive explorer. 2 Progressive enhancement over the server-rendered index: instant client-side 3 search, topic filtering, and a compliance-by-state choropleth (state 4 geometry reused from the Minimum Wage Navigator, Albers USA 1050x600). 5 The server-rendered featured + 12-card grid stay as the JS-off fallback; 6 this script takes over #bxResults once the post index has loaded. */ 7(function () { 8 'use strict'; 9 var root = document.getElementById('bxHub'); 10 if (!root) return; 11 12 var REST = root.dataset.rest; // /wp-json/wp/v2 13 var GEO = root.dataset.geo; // blog-map-geo.json url 14 var FEATURED = parseInt(root.dataset.featured || '0', 10) || 0; // lead-story post ID (kept out of the default grid) 15 var HIDE = root.dataset.hideCat || ''; // category slug kept out of the unfiltered grid (Compliance Wire), mirrors the server grid 16 var elMap = document.getElementById('bxMap'); 17 var elSearch = document.getElementById('bxSearch'); 18 var elChips = document.getElementById('bxChips'); 19 var elResults= document.getElementById('bxResults'); 20 var elCount = document.getElementById('bxCount'); 21 var elActive = document.getElementById('bxActive'); 22 var elMore = document.getElementById('bxMore'); 23 24 /* ---- category â topic icon + accent (mirrors partials/blog/thumb.blade.php) ---- */ 25 function themeFor(slug) { 26 var g = [ 27 [['legal-news-compliance','compliance','anti-harassment','wage-hour','independent-contractor','freedom-from-workplace-bullies-week'], 'legal', '#29ABE2'], 28 [['california-compliance','multi-state-compliance'], 'map', '#34D3B5'], 29 [['minimum-wage','payroll','outsourcing-accounting','wage'], 'coins', '#F9A03F'], 30 [['benefits-compensation','benefits-administration','employer-supported-childcare','insurance','workers-comp','workers-compensation-and-risk-managment'], 'shield', '#3DD68C'], 31 [['employee-wellness','healthcare','covid-19-coronavirus'], 'leaf', '#2DD4BF'], 32 [['training-workplace-culture','workplace-culture','leadership-management'], 'growth', '#A78BFA'], 33 [['employee-retention','hiring','employee-onboarding'], 'badge', '#38BDF8'], 34 [['workplace-safety'], 'safety', '#FBBF24'], 35 [['human-resources','human-resource-management'], 'nodes', '#5B9BE0'], 36 [['hris','hr-technology','hr-software','artificial-intelligence'], 'chip', '#818CF8'], 37 [['timekeeping'], 'clock', '#29ABE2'] 38 ]; 39 for (var i = 0; i < g.length; i++) if (g[i][0].indexOf(slug) !== -1) return { icon: g[i][1], accent: g[i][2] }; 40 return { icon: 'mark', accent: '#29ABE2' }; 41 } 42 var ICONS = { 43 legal:'<path class="dt-fill" d="M5 8l-2.6 5h5.2L5 8zm14 0l-2.6 5h5.2L19 8z"/><path class="dt-line" d="M12 3v18M7.5 21h9M12 6 5 8m7-2 7 2M5 8l-2.6 5h5.2L5 8zm14 0l-2.6 5h5.2L19 8z"/><path class="dt-line" d="M2.4 13a2.6 2.6 0 0 0 5.2 0M16.4 13a2.6 2.6 0 0 0 5.2 0"/>', 44 map:'<path class="dt-fill" d="M12 2c4 0 7 3 7 6.9 0 4.7-7 12.1-7 12.1S5 13.6 5 8.9C5 5 8 2 12 2z"/><path class="dt-line" d="M12 2c4 0 7 3 7 6.9 0 4.7-7 12.1-7 12.1S5 13.6 5 8.9C5 5 8 2 12 2z"/><circle class="dt-line" cx="12" cy="9" r="2.4"/>', 45 coins:'<ellipse class="dt-fill" cx="12" cy="7" rx="7" ry="3"/><path class="dt-line" d="M5 7c0 1.7 3.1 3 7 3s7-1.3 7-3-3.1-3-7-3-7 1.3-7 3z"/><path class="dt-line" d="M5 7v5c0 1.7 3.1 3 7 3s7-1.3 7-3V7"/><path class="dt-line" d="M5 12v5c0 1.7 3.1 3 7 3s7-1.3 7-3v-5"/>', 46 shield:'<path class="dt-fill" d="M12 2l8 3v6c0 5-3.5 8.6-8 11-4.5-2.4-8-6-8-11V5l8-3z"/><path class="dt-line" d="M12 2l8 3v6c0 5-3.5 8.6-8 11-4.5-2.4-8-6-8-11V5l8-3z"/><path class="dt-line" d="M12 8v6M9 11h6"/>', 47 leaf:'<path class="dt-fill" d="M4 20c0-9 7-16 16-16 0 9-7 16-16 16z"/><path class="dt-line" d="M4 20c0-9 7-16 16-16 0 9-7 16-16 16z"/><path class="dt-line" d="M4.5 19.5C8 14 12 11 17 9"/>', 48 growth:'<path class="dt-fill" d="M4 14h4v6H4zm6-3h4v9h-4zm6-3h4v12h-4z"/><path class="dt-line" d="M4 14h4v6H4zm6-3h4v9h-4zm6-3h4v12h-4z"/><path class="dt-line" d="M4 10l6-4 4 2 6-4M16 4h4v4"/>', 49 badge:'<circle class="dt-fill" cx="12" cy="9.5" r="6.5"/><circle class="dt-line" cx="12" cy="9.5" r="6.5"/><path class="dt-line" d="M9.2 9.6l2 2 3.6-3.7"/><path class="dt-line" d="M8.5 15.4 7 21l5-2 5 2-1.5-5.6"/>', 50 safety:'<path class="dt-fill" d="M3.5 17a8.5 8.5 0 0 1 17 0z"/><path class="dt-line" d="M3.5 17a8.5 8.5 0 0 1 17 0"/><path class="dt-line" d="M2 17h20M9 8.5V6.5h6v2M12 8.5V5"/>', 51 nodes:'<circle class="dt-fill" cx="12" cy="5" r="2.6"/><circle class="dt-fill" cx="5" cy="19" r="2.6"/><circle class="dt-fill" cx="19" cy="19" r="2.6"/><circle class="dt-line" cx="12" cy="5" r="2.6"/><circle class="dt-line" cx="5" cy="19" r="2.6"/><circle class="dt-line" cx="19" cy="19" r="2.6"/><path class="dt-line" d="M12 7.6v3.9M12 11.5H5v4.9M12 11.5h7v4.9"/>', 52 chip:'<rect class="dt-fill" x="7" y="7" width="10" height="10" rx="1.5"/><rect class="dt-line" x="6" y="6" width="12" height="12" rx="2"/><rect class="dt-line" x="9.5" y="9.5" width="5" height="5" rx="1"/><path class="dt-line" d="M9 6V3m6 3V3M9 21v-3m6 3v-3M6 9H3m3 6H3m18-6h-3m3 6h-3"/>', 53 clock:'<circle class="dt-fill" cx="12" cy="12" r="9"/><circle class="dt-line" cx="12" cy="12" r="9"/><path class="dt-line" d="M12 7v5l3.5 2"/>', 54 mark:'<path class="dt-fill" d="M6 3h9l4 4v14l-6.5-3.1L6 21V3z"/><path class="dt-line" d="M6 3h9l4 4v14l-6.5-3.1L6 21z"/><path class="dt-line" d="M9 9h6M9 12.5h4"/>' 55 }; 56 57 var NAME2AB = {Alabama:'AL',Alaska:'AK',Arizona:'AZ',Arkansas:'AR',California:'CA',Colorado:'CO',Connecticut:'CT',Delaware:'DE',Florida:'FL',Georgia:'GA',Hawaii:'HI',Idaho:'ID',Illinois:'IL',Indiana:'IN',Iowa:'IA',Kansas:'KS',Kentucky:'KY',Louisiana:'LA',Maine:'ME',Maryland:'MD',Massachusetts:'MA',Michigan:'MI',Minnesota:'MN',Mississippi:'MS',Missouri:'MO',Montana:'MT',Nebraska:'NE',Nevada:'NV','New Hampshire':'NH','New Jersey':'NJ','New Mexico':'NM','New York':'NY','North Carolina':'NC','North Dakota':'ND',Ohio:'OH',Oklahoma:'OK',Oregon:'OR',Pennsylvania:'PA','Rhode Island':'RI','South Carolina':'SC','South Dakota':'SD',Tennessee:'TN',Texas:'TX',Utah:'UT',Vermont:'VT',Virginia:'VA',Washington:'WA','West Virginia':'WV',Wisconsin:'WI',Wyoming:'WY','District of Columbia':'DC'}; 58 var AB2NAME = {};
58 Object.keys(NAME2AB).forEach(function (n) { AB2NAME[NAME2AB[n]] = n; }); 59 var STATE_NAMES = Object.keys(NAME2AB); 60 61 function decode(s){ var t = document.createElement('textarea'); t.innerHTML = s; return t.value.replace(/<[^>]+>/g, '').trim(); } 62 function esc(s){ return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } 63 /* word-safe truncation: cut at the last word boundary before max, real ellipsis */ 64 function clip(s, max){ 65 s = (s || '').trim(); 66 if (s.length <= max) return s; 67 var cut = s.slice(0, max); 68 var sp = cut.lastIndexOf(' '); 69 if (sp > 0) cut = cut.slice(0, sp); 70 return cut.replace(/[\s.,;:!?âââ¦-]+$/, '') + 'â¦'; 71 } 72 73 var POSTS = []; // full index 74 var CATS = {}; // id -> {name,slug} 75 var STATE_COUNT = {}; // ab -> count 76 var filter = { q: '', cat: '', state: '' }; 77 var shown = 0, PAGE = 18; 78 79 /* âââââââââ data load âââââââââ */ 80 /* SHAPE is the contract between a cached record and cardHTML(). It is baked 81 into the cache key AND re-checked on every read, so adding a field to the 82 record built below invalidates every existing cache by itself. 83 84 Bumping a hand-written key is what failed before: b0a4619d added `img` (the 85 real article image) but left the key at its previous value, so any tab 86 already holding a pre-fix payload kept reading p.img === undefined and 87 rendered all 182 cards as the branded icon panel. Nothing caught it â the 88 server-rendered fallback in the HTML was correct, the REST field was 89 correct, and the cache only dies when the tab does. */ 90 var SHAPE = ['id', 't', 'u', 'e', 'c', 'cs', 'd', 'st', 'img', 'icon', 'accent']; 91 var CACHE_KEY = 'bxPosts:' + SHAPE.join(','); 92 try { ['bxPosts', 'bxPosts2', 'bxPosts3'].forEach(function (k) { sessionStorage.removeItem(k); }); } catch (e) {} 93 94 function readCache() { 95 var raw = null; 96 try { raw = JSON.parse(sessionStorage.getItem(CACHE_KEY) || 'null'); } catch (e) { return null; } 97 if (!raw || !raw.length) return null; 98 for (var i = 0; i < raw.length; i++) { 99 for (var k = 0; k < SHAPE.length; k++) { 100 if (!(SHAPE[k] in raw[i])) return null; // predates a field cardHTML() reads 101 } 102 } 103 return raw; 104 } 105 106 /* Stale-while-revalidate. The shape check above is necessary but NOT 107 sufficient: on 2026-08-18 a tab cached a payload whose shape was current 108 but whose CONTENT predated a server-side change (config/hub-thumbs.php was 109 emptied minutes after the tab stored img:'' for 12 posts), and the tab 110 kept painting icon panels on every reload. No key scheme can see a content 111 change, so the cache is now only allowed to win the first paint: every 112 load refetches the index in the background and repaints if it differs. */ 113 function loadPosts() { 114 var cached = readCache(); 115 if (cached) { POSTS = cached; revalidate(); return Promise.resolve(); } 116 return fetchIndex().then(function (fresh) { POSTS = fresh; saveCache(); }); 117 } 118 function saveCache() { 119 try { sessionStorage.setItem(CACHE_KEY, JSON.stringify(POSTS)); } catch (e) {} 120 } 121 function revalidate() { 122 fetchIndex().then(function (fresh) { 123 var changed = JSON.stringify(fresh) !== JSON.stringify(POSTS); 124 POSTS = fresh; saveCache(); 125 if (changed) { buildChips(); render(false); } 126 }).catch(function (e) { /* offline etc. â cached paint stands */ }); 127 } 128 function fetchIndex() { 129 return fetch(REST + '/categories?per_page=100&_fields=id,name,slug') 130 .then(function (r) { return r.json(); }) 131 .then(function (cs) { 132 cs.forEach(function (c) { CATS[c.id] = { name: decode(c.name), slug: c.slug }; }); 133 return fetchPage(1, []); 134 }) 135 .then(function (all) { 136 return all.map(function (p) { 137 var title = decode(p.title.rendered); 138 // bx_excerpt = the server's word-safe cleaned excerpt (App\vshr_blog_excerpt, 139 // already plain text); fall back to the decoded rendered excerpt. clip() 140 // guarantees a word-boundary cut either way. 141 var ex = clip(p.bx_excerpt ? p.bx_excerpt : decode(p.excerpt.rendered), 150); 142 var hay = title + ' ' + ex; 143 var st = STATE_NAMES.filter(function (n) { return new RegExp('\\b' + n + '\\b', 'i').test(hay); }).map(function (n) { return NAME2AB[n]; }); 144 // Mirrors App vshr_primary_cat(): 'Blog'/'Uncategorized' are container 145 // taxonomy, not editorial signal - skip unless nothing else exists. 146 var pc = { name: 'Insights', slug: '' }, pcFirst = null;
147 (p.categories || []).forEach(function (cid) { 148 var c = CATS[cid]; if (!c) return; 149 if (!pcFirst) pcFirst = c; 150 if (pc.slug === '' && c.slug !== 'blog' && c.slug !== 'uncategorized') pc = c; 151 }); 152 // No pcFirst fallback: a bare 'Blog'/'Uncategorized' post reads as Insights. 153 var th = themeFor(pc.slug); 154 return { id: p.id, t: title, u: p.link.replace(/^https?:\/\/[^/]+/, ''), e: ex, c: pc.name, cs: pc.slug, d: p.date.slice(0, 10), st: st, img: p.bx_thumb || '', icon: th.icon, accent: th.accent }; 155 }); 156 }); 157 } 158 function fetchPage(page, acc) { 159 return fetch(REST + '/posts?per_page=100&page=' + page + '&_fields=id,link,title,excerpt,date,categories,bx_thumb,bx_excerpt') 160 .then(function (r) { return r.ok ? r.json() : []; }) 161 .then(function (b) { 162 acc = acc.concat(b); 163 return (b.length === 100) ? fetchPage(page + 1, acc) : acc; 164 }); 165 } 166 167 /* âââââââââ map âââââââââ */ 168 function buildMap() { 169 POSTS.forEach(function (p) { p.st.forEach(function (ab) { STATE_COUNT[ab] = (STATE_COUNT[ab] || 0) + 1; }); }); 170 var max = Math.max.apply(null, [1].concat(Object.keys(STATE_COUNT).map(function (k) { return STATE_COUNT[k]; }))); 171 return fetch(GEO).then(function (r) { return r.json(); }).then(function (geo) { 172 var svg = '<svg viewBox="' + geo.vb + '" class="bx-usmap" role="group" aria-label="Compliance coverage â all 50 states" preserveAspectRatio="xMidYMid meet">'; 173 var labels = ''; 174 // Every state is covered: federal guidance applies nationwide. The heat 175 // only shows where we ALSO have state-specific deep-dives â no state is 176 // ever "empty", because clicking it surfaces the nationwide library too. 177 Object.keys(geo.states).forEach(function (ab) { 178 var s = geo.states[ab], n = STATE_COUNT[ab] || 0; 179 var lvl = n === 0 ? 0 : Math.min(4, Math.ceil(n / max * 4)); 180 var al = AB2NAME[ab] + (n ? ', ' + n + ' state-specific guides plus nationwide' : ', nationwide coverage'); 181 svg += '<path d="' + s.d + '" class="bx-st h' + lvl + ' on" data-ab="' + ab + '" tabindex="0" role="button" aria-label="' + al + '"></path>'; 182 if (s.lx != null && n > 1) labels += '<text x="' + s.lx + '" y="' + s.ly + '" class="bx-stlabel">' + ab + '</text>'; 183 }); 184 // NE callout dots (state paths too small to click) â render all of them 185 Object.keys(geo.calls).forEach(function (ab) { 186 var n = STATE_COUNT[ab] || 0, c = geo.calls[ab], lvl = n === 0 ? 0 : Math.min(4, Math.ceil(n / max * 4)); 187 var al = AB2NAME[ab] + (n ? ', ' + n + ' state-specific guides plus nationwide' : ', nationwide coverage'); 188 svg += '<circle cx="' + c.cx + '" cy="' + c.cy + '" r="6" class="bx-st bx-stdot h' + lvl + ' on" data-ab="' + ab + '" tabindex="0" role="button" aria-label="' + al + '"></circle>'; 189 }); 190 svg += labels + '</svg>'; 191 elMap.innerHTML = svg; 192 var tip = document.createElement('div'); tip.className = 'bx-maptip'; tip.hidden = true; elMap.appendChild(tip); 193 elMap.querySelectorAll('.bx-st.on').forEach(function (el) { 194 var ab = el.getAttribute('data-ab'), n = STATE_COUNT[ab] || 0; 195 el.addEventListener('click', function () { toggleState(ab); }); 196 el.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleState(ab); } }); 197 el.addEventListener('mousemove', function (e) { 198 var r = elMap.getBoundingClientRect(); 199 tip.innerHTML = '<b>' + AB2NAME[ab] + '</b><span>' + (n ? n + ' state-specific + nationwide' : 'Nationwide coverage') + '</span>'; 200 tip.style.left = (e.clientX - r.left) + 'px'; 201 tip.style.top = (e.clientY - r.top) + 'px'; 202 tip.hidden = false; 203 }); 204 el.addEventListener('mouseleave', function () { tip.hidden = true; }); 205 }); 206 }); 207 } 208 function toggleState(ab) { filter.state = (filter.state === ab) ? '' : ab; sync(); } 209 210 /* âââââââââ chips âââââââââ */ 211 function buildChips() { 212 var counts = {};
213 POSTS.forEach(function (p) { if (p.cs) counts[p.cs] = (counts[p.cs] || { n: 0, name: p.c }); if (p.cs) counts[p.cs].n++; }); 214 var skip = { uncategorized: 1, blog: 1 }; 215 var top = Object.keys(counts).filter(function (s) { return !skip[s]; }).sort(function (a, b) { return counts[b].n - counts[a].n; }).slice(0, 9); 216 var html = '<button class="bx-chip is-active" data-cat="" aria-pressed="true">All topics</button>'; 217 top.forEach(function (slug) { html += '<button class="bx-chip" data-cat="' + slug + '" aria-pressed="false">' + esc(counts[slug].name) + ' <span>' + counts[slug].n + '</span></button>'; }); 218 elChips.innerHTML = html; 219 elChips.querySelectorAll('.bx-chip').forEach(function (ch) { 220 ch.addEventListener('click', function () { filter.cat = ch.dataset.cat; sync(); }); 221 }); 222 } 223 224 /* âââââââââ filter + render âââââââââ */ 225 function matches(p) { 226 if (filter.cat && p.cs !== filter.cat) return false; 227 if (filter.state && p.st.indexOf(filter.state) === -1 && p.st.length !== 0) return false; // state-specific OR nationwide 228 if (filter.q) { 229 var q = filter.q.toLowerCase(); 230 if ((p.t + ' ' + p.e + ' ' + p.c).toLowerCase().indexOf(q) === -1) return false; 231 } 232 return true; 233 } 234 function fmtDate(iso) { // "2026-06-11" -> "June 11, 2026", matching the server-rendered cards 235 var d = new Date(iso + 'T00:00:00'); 236 return isNaN(d) ? iso : d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); 237 } 238 function cardHTML(p, i) { 239 return '<a class="bx-card bx-reveal" style="--d:' + ((i % 6) * 45) + 'ms" href="' + p.u + '">' + 240 '<div class="bx-card-media">' + (p.img 241 ? '<img src="' + p.img + '" alt="" width="768" height="403" loading="lazy">' 242 : '<div class="bx-thumb" style="--accent:' + p.accent + '"><span class="bx-thumb-icon"><svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">' + ICONS[p.icon] + '</svg></span></div>') + '</div>' + 243 '<div class="bx-card-body"><span class="bx-eyebrow">' + esc(p.c) + '</span>' + 244 '<h3>' + esc(p.t) + '</h3><p class="bx-card-excerpt">' + esc(p.e) + '</p>' + 245 '<div class="bx-card-meta"><span class="bx-dot"></span>' + fmtDate(p.d) + '</div></div></a>'; 246 } 247 function render(reset) { 248 var list = POSTS.filter(matches); 249 // Unfiltered view mirrors the server grid: the Compliance Wire stream stays 250 // out until the reader searches, picks a state, or clicks its chip. 251 var isFiltered = !!(filter.q || filter.cat || filter.state); 252 if (!isFiltered && HIDE) list = list.filter(function (p) { return p.cs !== HIDE; }); 253 if (filter.state) { // surface the state-specific guides first, nationwide below 254 list = list.slice().sort(function (a, b) { 255 return (a.st.indexOf(filter.state) !== -1 ? 0 : 1) - (b.st.indexOf(filter.state) !== -1 ? 0 : 1); 256 }); 257 } 258 // Default view: keep the featured lead story out of the grid (it is already 259 // showcased above â mirrors the server grid's post__not_in). Under an active 260 // filter the grid IS the result set, so the featured post participates and 261 // the count always equals what is actually listed. 262 var gridList = (!isFiltered && FEATURED) ? list.filter(function (p) { return p.id !== FEATURED; }) : list; 263 if (reset) shown = PAGE; 264 var slice = gridList.slice(0, shown); 265 elResults.innerHTML = slice.map(cardHTML).join('') || '<p class="bx-empty">No articles match those filters. Clear one to widen the search.</p>'; 266 if (filter.state) { 267 var spec = list.filter(function (p) { return p.st.indexOf(filter.state) !== -1; }).length; 268 elCount.textContent = list.length + ' guides relevant to ' + AB2NAME[filter.state] + (spec ? ' · ' + spec + ' ' + AB2NAME[filter.state] + '-specific' : ' (nationwide)'); 269 } else { 270 elCount.textContent = list.length + (list.length === 1 ? ' article' : ' articles'); 271 } 272 elMore.hidden = gridList.length <= shown; 273 // active filter pills 274 var pills = ''; 275 if (filter.state) pills += pill('state', AB2NAME[filter.state]); 276 if (filter.cat) { var nm = (POSTS.find(function (p) { return p.cs === filter.cat; }) || {}).c || filter.cat; pills += pill('cat', nm); } 277 if (filter.q) pills += pill('q', 'â' + esc(filter.q) + 'â'); 278 if ((filter.state ? 1 : 0) + (filter.cat ? 1 : 0) + (filter.q ? 1 : 0) >= 2) pills += '<button class="bx-clearall" data-clearall="1">Clear all</button>'; 279 elActive.innerHTML = pills; 280 elActive.querySelectorAll('[data-clear]').forEach(function (b) { b.addEventListener('click', function () { filter[b.dataset.clear] = ''; if (b.dataset.clear === 'q') elSearch.value = ''; sync(); }); }); 281 var clearAll = elActive.querySelector('[data-clearall]'); 282 if (clearAll) clearAll.addEventListener('click', function () { filter.state = ''; filter.cat = ''; filter.q = ''; elSearch.value = ''; sync(); }); 283 // reflect chip + map active state 284 elChips.querySelectorAll('.bx-chip').forEach(function (c) { var on = c.dataset.cat === filter.cat; c.classList.toggle('is-active', on); c.setAttribute('aria-pressed', on); }); 285 elMap.querySelectorAll('.bx-st').forEach(function (s) { s.classList.toggle('sel', s.getAttribute('data-ab') === filter.state); }); 286 }
287 function pill(kind, label) { return '<button class="bx-pill" data-clear="' + kind + '">' + label + ' <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M6 6l12 12M18 6L6 18"/></svg></button>'; } 288 function sync() { render(true); } 289 290 /* âââââââââ boot âââââââââ */ 291 loadPosts().then(function () { 292 buildChips(); 293 render(true); 294 // The JS grid pages with "Show more", so the server pagination becomes a 295 // second, competing pager the moment we boot. Hide it VISUALLY only: the 296 // links must stay in the served HTML because they are the crawl path to 297 // the whole library for anything that does not execute JS (see 298 // template-blog.blade.php). display:none via attribute, not removal. 299 var pag = document.querySelector('.bx-pagination'); 300 if (pag) pag.setAttribute('data-enhanced', ''); 301 return buildMap(); 302 }).then(function () { 303 render(false); 304 }).catch(function (e) { /* leave server-rendered fallback in place */ }); 305 306 var t; 307 if (elSearch) elSearch.addEventListener('input', function () { clearTimeout(t); t = setTimeout(function () { filter.q = elSearch.value.trim(); sync(); }, 140); }); 308 if (elMore) elMore.addEventListener('click', function () { shown += PAGE; render(false); }); 309})();
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.