1/** 2 * Pixelfield site search (v1.2.1) â styl podle pf-menu design systému 3 * Additive, self-contained: injects a search button into .pf-header__actions, 4 * renders a full-screen overlay, searches the static page index 5 * (/_assets/2026/search-index.json) plus live blog posts via WP REST. 6 * No dependencies. Remove this <script> tag to remove the feature entirely. 7 */ 8(function () { 9 'use strict'; 10 if (window.__pfSearchLoaded) return; 11 window.__pfSearchLoaded = true; 12 13 var INDEX_URL = '/_assets/2026/search-index.json'; 14 var BLOG_API = '/blog/wp-json/wp/v2/posts'; 15 var index = null; 16 17 function norm(s) { 18 return (s || '') 19 .toLowerCase() 20 .normalize('NFD') 21 .replace(/[Ì-ͯ]/g, ''); 22 } 23 24 var MONO = '"Tronica Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'; 25 var SANS = '"PP Neue Montreal", "grotesk-w02-fallback-sans-serif", system-ui, sans-serif'; 26 var css = 27 '.pf-search-btn{display:flex;align-items:center;gap:4px;background:none;border:0;padding:0;cursor:pointer;' + 28 'font-size:14px;font-family:' + MONO + ';font-weight:400;text-transform:uppercase;line-height:22px;transition:color .3s ease}' + 29 '.pf-search-btn:hover{color:#dcfe7e!important}' + 30 '.pf-search-overlay{position:fixed;inset:0;z-index:9999;background:#141e0a;overflow-y:auto;-webkit-overflow-scrolling:touch;' + 31 'visibility:hidden;opacity:0;pointer-events:none;transition:visibility .3s ease,opacity .3s ease}' + 32 '.pf-search-overlay.is-open{visibility:visible;opacity:1;pointer-events:auto}' + 33 '.pf-search-watermark{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);font-size:clamp(80px,15vw,160px);' + 34 'font-family:' + MONO + ';font-weight:400;text-transform:uppercase;color:rgba(243,243,239,.05);pointer-events:none;white-space:nowrap}' + 35 '.pf-search-inner{min-height:100%;padding:120px 40px 80px;max-width:1120px;margin:0 auto;position:relative}' + 36 '.pf-search-row{display:flex;align-items:center;gap:24px}' + 37 '.pf-search-input{flex:1;background:none;border:0;border-bottom:1px solid rgba(243,243,239,.3);outline:0;border-radius:0;' + 38 'color:#f3f3ef;caret-color:#dcfe7e;font-size:32px;line-height:1.4;min-width:0;padding:0 0 12px;' + 39 'font-family:' + SANS + ';font-weight:400;transition:border-color .2s ease}' + 40 '.pf-search-input:focus{border-bottom-color:#dcfe7e}' + 41 '.pf-search-input::placeholder{color:rgba(243,243,239,.4)}' + 42 '.pf-search-input::-webkit-search-cancel-button{display:none}' + 43 '.pf-search-close{background:none;border:0;cursor:pointer;color:rgba(243,243,239,.7);font-size:14px;' + 44 'font-family:' + MONO + ';font-weight:400;text-transform:uppercase;line-height:22px;padding:0;transition:color .2s ease}' + 45 '.pf-search-close:hover{color:#dcfe7e}' + 46 '.pf-search-hint{margin-top:12px;color:rgba(243,243,239,.4);font-size:12px;font-family:' + MONO + ';text-transform:uppercase;letter-spacing:.08em}' + 47 '.pf-search-results{margin-top:36px;position:relative}' + 48 '.pf-search-group{display:block;color:rgba(243,243,239,.7);font-size:12px;font-family:' + MONO + ';font-weight:400;' + 49 'text-transform:uppercase;letter-spacing:.08em;margin:40px 0 20px}' + 50 '.pf-search-group:first-child{margin-top:0}' + 51 '.pf-search-item{display:block;padding:10px 0;color:#f3f3ef;text-decoration:none;font-family:' + SANS + ';transition:color .2s ease}' + 52 '.pf-search-item b{font-weight:400;font-size:18px;line-height:1.4}' + 53 '.pf-search-item:hover,.pf-search-item.is-active{color:#dcfe7e}' + 54 '.pf-search-item span{display:block;font-size:13px;color:rgba(243,243,239,.5);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' + 55 '.pf-search-item--all{font-family:' + MONO + ';font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#dcfe7e}' + 56 '.pf-search-item--all:hover{color:#f3f3ef}' + 57 '.pf-search-item--all span{display:inline;color:inherit;font-size:12px}' + 58 '.pf-search-empty{margin-top:48px;color:rgba(243,243,239,.5);font-family:' + SANS + '}' + 59 '@media (max-width:600px){.pf-search-inner{padding:96px 20px 60px}.pf-search-input{font-size:20px;padding-bottom:8px}}'; 60 61 var style = document.createElement('style'); 62 style.textContent = css; 63 document.head.appendChild(style); 64
65 var overlay = document.createElement('div'); 66 overlay.className = 'pf-search-overlay'; 67 overlay.setAttribute('role', 'dialog'); 68 overlay.setAttribute('aria-modal', 'true'); 69 overlay.setAttribute('aria-label', 'Site search'); 70 overlay.innerHTML = 71 '<div class="pf-search-watermark" aria-hidden="true">Search</div>' + 72 '<div class="pf-search-inner">' + 73 '<div class="pf-search-row">' + 74 '<input class="pf-search-input" type="search" placeholder="Search services and articlesâ¦" autocomplete="off" spellcheck="false">' + 75 '<button class="pf-search-close" type="button" aria-label="Close search">Close</button>' + 76 '</div><div class="pf-search-hint">Enter \u2192 open best match \u00b7 \u2191\u2193 pick \u00b7 Esc close</div>' + 77 '<div class="pf-search-results" aria-live="polite"></div></div>'; 78 79 var input, results; 80 81 function ensureMounted() { 82 if (!overlay.parentNode) { 83 document.body.appendChild(overlay); 84 input = overlay.querySelector('.pf-search-input'); 85 results = overlay.querySelector('.pf-search-results'); 86 input.addEventListener('input', onType); 87 input.addEventListener('keydown', onKey); 88 overlay.querySelector('.pf-search-close').addEventListener('click', close); 89 overlay.addEventListener('click', function (e) { 90 if (e.target === overlay) close(); 91 }); 92 } 93 } 94 95 function open() { 96 ensureMounted(); 97 overlay.classList.add('is-open'); 98 document.documentElement.style.overflow = 'hidden'; 99 input.focus(); 100 if (!index) { 101 fetch(INDEX_URL).then(function (r) { return r.json(); }).then(function (d) { index = d; if (input.value.trim().length > 1) onType(); }).catch(function () { index = []; }); 102 } 103 } 104 105 function close() { 106 overlay.classList.remove('is-open'); 107 document.documentElement.style.overflow = ''; 108 } 109 110 document.addEventListener('keydown', function (e) { 111 if (e.key === 'Escape' && overlay.classList.contains('is-open')) close(); 112 }); 113 114 function esc(s) { 115 var d = document.createElement('div'); 116 d.textContent = s || ''; 117 return d.innerHTML; 118 } 119 120 var STOP = { the: 1, a: 1, an: 1, and: 1, of: 1, for: 1, in: 1, to: 1, uk: 1, london: 1, company: 0, agency: 0 }; 121 122 function scorePage(it, qWords, qPhrase) { 123 var t = norm(it.t), h = norm(it.h), d = norm(it.d), u = norm(it.u); 124 var s = 0; 125 // 1) presna shoda cele fraze 126 if (t === qPhrase || h === qPhrase) s += 100; 127 else if (t.indexOf(qPhrase) === 0 || h.indexOf(qPhrase) === 0) s += 60; 128 else if (t.indexOf(qPhrase) !== -1 || h.indexOf(qPhrase) !== -1) s += 40; 129 else if (d.indexOf(qPhrase) !== -1) s += 15; 130 // 2) jednotliva slova (stop-slova ignorovana) 131 var matched = 0; 132 qWords.forEach(function (w) { 133 if (!w || STOP[w] === 1) return; 134 var hit = false; 135 if (t.indexOf(w) !== -1) { s += 8; hit = true; } 136 if (h.indexOf(w) !== -1) { s += 4; hit = true; } 137 if (d.indexOf(w) !== -1) { s += 2; hit = true; } 138 if (u.indexOf(w) !== -1) { s += 2; hit = true; } 139 if (hit) matched++; 140 }); 141 var meaningful = qWords.filter(function (w) { return w && STOP[w] !== 1; }).length; 142 if (meaningful && matched < meaningful) s = s * (matched / meaningful) * 0.6; // chybi slovo -> penalizace 143 // 2b) fraze primo ve slugu URL (ai-agents-development pro "ai agents") 144 var slugPhrase = qPhrase.replace(/ /g, '-'); 145 if (slugPhrase.length > 3 && u.indexOf(slugPhrase) !== -1) s += 45; 146 // 3) preference: kratsi titul = obecnejsi stranka (app development > android app development) 147 if (s > 0) s += Math.max(0, 40 - t.length) * 0.5; 148 // 4) hlavni servisni stranky (kratke URL) vys nez podstranky a case studies 149 var depth = (u.match(/\//g) || []).length; 150 if (u.indexOf('/our-work/') === 0) s -= 10; 151 if (u.indexOf('/career') === 0) s -= 15; 152 if (depth <= 2) s += 6; 153 return s; 154 } 155 156 var blogTimer = null, lastQuery = ''; 157 158 function onType() { 159 var raw = input.value.trim(); 160 lastQuery = raw; 161 if (raw.length < 2) { results.innerHTML = ''; return; } 162 var qPhrase = norm(raw).replace(/\s+/g, ' '); 163 var q = qPhrase.split(' '); 164 var pages = (index || []) 165 .map(function (it) { return { it: it, s: scorePage(it, q, qPhrase) }; }) 166 .filter(function (x) { return x.s > 0; }) 167 .sort(function (a, b) { return b.s - a.s; }) 168 .slice(0, 7); 169 render(pages, null, raw); 170 clearTimeout(blogTimer); 171 blogTimer = setTimeout(function () { searchBlog(raw, pages); }, 300); 172 } 173 174 function searchBlog(raw, pages) { 175 fetch(BLOG_API + '?search=' + encodeURIComponent(raw) + '&per_page=5&_fields=title,link') 176 .then(function (r) { return r.ok ? r.json() : []; }) 177 .then(function (posts) { 178 if (raw !== lastQuery) return; 179 render(pages, posts, raw); 180 }) 181 .catch(function () {}); 182 } 183 184 function render(pages, posts, raw) { 185 var htmlOut = ''; 186 if (pages.length) { 187 htmlOut += '<div class="pf-search-group">Services & pages</div>';
188 pages.forEach(function (x) { 189 htmlOut += '<a class="pf-search-item" href="' + esc(x.it.u) + '"><b>' + esc(x.it.t) + '</b><span>' + esc(x.it.d || x.it.h) + '</span></a>'; 190 }); 191 } 192 if (posts && posts.length) { 193 htmlOut += '<div class="pf-search-group">From the blog</div>'; 194 posts.forEach(function (p) { 195 var t = p.title && p.title.rendered ? p.title.rendered.replace(/<[^>]+>/g, '') : ''; 196 htmlOut += '<a class="pf-search-item pf-search-item--blog" href="' + esc(p.link) + '"><b>' + t + '</b></a>'; 197 }); 198 } 199 if (posts) { 200 htmlOut += '<a class="pf-search-item pf-search-item--all" href="/blog/?s=' + encodeURIComponent(raw) + '"><span>Search the whole blog for â' + esc(raw) + 'â â</span></a>'; 201 } 202 if (!htmlOut) htmlOut = '<div class="pf-search-empty">No results yetâ¦</div>'; 203 results.innerHTML = htmlOut; 204 } 205 206 function onKey(e) { 207 var items = results.querySelectorAll('.pf-search-item'); 208 if (!items.length && e.key !== 'Enter') return; 209 var active = results.querySelector('.pf-search-item.is-active'); 210 var idx = Array.prototype.indexOf.call(items, active); 211 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { 212 e.preventDefault(); 213 if (active) active.classList.remove('is-active'); 214 idx = e.key === 'ArrowDown' ? Math.min(idx + 1, items.length - 1) : Math.max(idx - 1, 0); 215 items[idx].classList.add('is-active'); 216 items[idx].scrollIntoView({ block: 'nearest' }); 217 } else if (e.key === 'Enter') { 218 e.preventDefault(); 219 // Enter = nejlepsi servisni stranka (aktivni polozka, jinak prvni stranka ze sekce Services) 220 var target = active || results.querySelector('.pf-search-item:not(.pf-search-item--all):not(.pf-search-item--blog)'); 221 if (target) { window.location.href = target.getAttribute('href'); return; } 222 var raw = input.value.trim(); 223 if (raw.length > 1) window.location.href = '/blog/?s=' + encodeURIComponent(raw); 224 } 225 } 226 227 function injectButton() { 228 var actions = document.querySelector('.pf-header__actions'); 229 if (!actions || actions.querySelector('.pf-search-btn')) return; 230 var btn = document.createElement('button'); 231 btn.type = 'button'; 232 btn.className = 'pf-search-btn'; 233 btn.setAttribute('aria-label', 'Search'); 234 btn.innerHTML = '<span class="pf-search-btn__label">Search</span>'; 235 btn.addEventListener('click', open); 236 actions.insertBefore(btn, actions.firstChild); 237 238 // Barvu ikony pÅebÃrej z vedlejÅ¡iho MENU labelu (hlavicka meni tema pri scrollu) 239 var label = document.querySelector('.pf-header__menu-label') || actions; 240 var ticking = false; 241 function syncColor() { 242 btn.style.color = getComputedStyle(label).color; 243 ticking = false; 244 } 245 syncColor(); 246 window.addEventListener('scroll', function () { 247 if (!ticking) { ticking = true; requestAnimationFrame(syncColor); } 248 }, { passive: true }); 249 window.addEventListener('resize', function () { 250 if (!ticking) { ticking = true; requestAnimationFrame(syncColor); } 251 }, { passive: true }); 252 } 253 254 if (document.readyState === 'loading') { 255 document.addEventListener('DOMContentLoaded', injectButton); 256 } else { 257 injectButton(); 258 } 259})();
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.