1/* FK Blog Search â Live-Dropdown (Rezepte + Kategorien; bei Typ "Produkte" Shop-Suche) */ 2(function () { 3 'use strict'; 4 if (typeof FK_BSEARCH === 'undefined') return; 5 var CFG = FK_BSEARCH, L = CFG.labels; 6 var inputs = document.querySelectorAll('form.search-form input[name="s"]'); 7 if (!inputs.length) return; 8 9 function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } 10 function el(html) { var d = document.createElement('div'); d.innerHTML = html; return d.firstElementChild; } 11 function section(label, inner, cls) { return '<div class="fk-s-section ' + (cls || '') + '"><span class="fk-s-label">' + esc(label) + '</span>' + inner + '</div>'; } 12 function chips(items) { return '<div class="fk-chips">' + items.map(function (i) { return '<a href="' + esc(i.url) + '">' + esc(i.label) + '</a>'; }).join('') + '</div>'; } 13 function catList(cats) { 14 return '<ul class="fk-s-cats">' + cats.map(function (c) { 15 return '<li><a href="' + esc(c.url) + '">' + (c.parent ? '<span class="fk-s-parent">' + esc(c.parent) + ' âº</span> ' : '') + esc(c.name) + '</a></li>'; 16 }).join('') + '</ul>'; 17 } 18 function itemList(items, cls) { 19 return '<ul class="fk-s-products ' + cls + '">' + items.map(function (p) { 20 return '<li><a href="' + esc(p.url) + '"><span class="fk-s-img">' + (p.img ? '<img src="' + esc(p.img) + '" alt="" loading="lazy">' : '') + '</span><span class="fk-s-title">' + p.title + (p.desc ? '<span class="fk-s-desc">' + esc(p.desc) + '</span>' : '') + '</span></a></li>'; 21 }).join('') + '</ul>'; 22 } 23 24 function initialHtml() { 25 var h = ''; 26 if (CFG.pageCats && CFG.pageCats.length) h += section(L.cats, catList(CFG.pageCats)); 27 else if (CFG.popular && CFG.popular.length) h += section(L.popular, chips(CFG.popular)); 28 if (CFG.help && CFG.help.length) h += section(L.help, chips(CFG.help), 'fk-s-help'); 29 return h; 30 } 31 32 function recipesHtml(d) { 33 var h = ''; 34 if (d.recipes && d.recipes.length) { 35 h += section(L.recipes, itemList(d.recipes, 'fk-s-recipes')); 36 h += '<a class="fk-s-all" href="' + esc(d.more) + '">' + esc(L.all) + (d.total > d.recipes.length ? ' (' + d.total + ')' : '') + '</a>'; 37 if (d.categories && d.categories.length) h += section(L.cats, catList(d.categories)); 38 } else { 39 h += '<p class="fk-s-none">' + esc(L.none) + '</p>'; 40 if (d.categories && d.categories.length) h += section(L.cats, catList(d.categories)); 41 if (CFG.popular && CFG.popular.length) h += section(L.popular, chips(CFG.popular)); 42 } 43 return h; 44 } 45 46 function productsHtml(d) { 47 var h = ''; 48 if (d.brand) h += section(L.brand, '<a class="fk-s-brand" href="' + esc(d.brand.url) + '">Alle Produkte von <strong>' + esc(d.brand.name) + '</strong> <span class="fk-s-count">' + d.brand.count + '</span></a>'); 49 if (d.products && d.products.length) { 50 h += section(L.products, itemList(d.products, '')); 51 h += '<a class="fk-s-all" href="' + esc(d.more) + '">' + esc(L.all) + (d.total > d.products.length ? ' (' + d.total + ')' : '') + '</a>'; 52 if (d.categories && d.categories.length) h += section(L.cats, catList(d.categories)); 53 } else { 54 h += '<p class="fk-s-none">' + esc(L.noneProducts) + '</p>'; 55 if (d.categories && d.categories.length) h += section(L.cats, catList(d.categories)); 56 } 57 return h; 58 } 59 60 61 /* ---- Such-Logging: nur "abgeschlossene" Suchen (1,5 s Ruhe, Klick, Verlassen) ---- */ 62 function makeLogger(input, src) { 63 var pending = null, timer = null, sent = ''; 64 function send(click) { 65 if (!pending || !CFG.logUrl) return; 66 var key = pending.t + '|' + (click || ''); 67 if (key === sent) return; sent = key; 68 var body = JSON.stringify({ t: pending.t, src: pending.src || src, ctx: 'dropdown', hits: pending.hits, click: click || '' }); 69 try { if (navigator.sendBeacon) navigator.sendBeacon(CFG.logUrl, new Blob([body], { type: 'application/json' })); else fetch(CFG.logUrl, { method: 'POST', body: body, keepalive: true, headers: { 'Content-Type': 'application/json' } }); } catch (e) {} 70 clearTimeout(timer); 71 } 72 return { 73 shown: function (q, hits, s2) { pending = { t: q, hits: hits, src: s2 }; sent = ''; clearTimeout(timer); timer = setTimeout(function () { send(''); }, 1500); }, 74 clear: function () { pending = null; clearTimeout(timer); }, 75 click: function (type) { send(type); }, 76 flush: function () { send(''); } 77 }; 78 } 79 80 Array.prototype.forEach.call(inputs, function (input) { 81 var form = input.form; if (!form) return; 82 input.setAttribute('autocomplete', 'off'); 83 form.classList.add('fk-s-wrap'); 84 // Panel als "Portal" an <body>: unabhängig von overflow:hidden / display-Regeln der Header-Wrapper 85 var panel = el('<div class="fk-s-panel fk-s-portal" role="listbox" hidden></div>'); 86 document.body.appendChild(panel); 87 var select = form.querySelector('.selecttype'); 88 var logger = makeLogger(input, 'blog'); 89 var timer = null, ctrl = null, lastKey = null, active = -1; 90 91 function mode() { return (select && select.value === 'produkt') ? 'produkt' : 'rezept'; } 92 function place() { 93 var r = form.getBoundingClientRect(), ir = input.getBoundingClientRect(); 94 var left = Math.max(8, r.left), width = Math.min(window.innerWidth - left - 8, Math.max(r.width, 280)); 95 panel.style.left = Math.round(left + window.scrollX) + 'px'; 96 panel.style.top = Math.round(ir.bottom + window.scrollY + 4) + 'px'; 97 panel.style.width = Math.round(width) + 'px'; 98 panel.style.maxHeight = Math.max(220, window.innerHeight - ir.bottom - 16) + 'px'; 99 } 100 function show(html) { panel.innerHTML = '<button type="button" class="fk-s-close" aria-label="SchlieÃen">Ã</button>' + html; panel.hidden = false; active = -1; place(); } 101 window.addEventListener('resize', function () { if (!panel.hidden) place(); }); 102 window.addEventListener('scroll', function () { if (!panel.hidden) place(); }, { passive: true }); 103 function hide() { panel.hidden = true; } 104 function items() { return panel.querySelectorAll('.fk-s-products a, .fk-s-cats a, .fk-s-brand, .fk-s-all'); } 105 106 function fetchQ(q, m) { 107 if (ctrl) ctrl.abort(); 108 ctrl = ('AbortController' in window) ? new AbortController() : null; 109 var url = (m === 'produkt' ? CFG.shopEndpoint : CFG.endpoint) + '?q=' + encodeURIComponent(q); 110 fetch(url, { signal: ctrl ? ctrl.signal : undefined, credentials: 'same-origin' }) 111 .then(function (r) { return r.json(); }) 112 .then(function (d) { if (input.value.trim() === q && mode() === m) { show(m === 'produkt' ? productsHtml(d) : recipesHtml(d)); logger.shown(q, d.total || 0, m === 'produkt' ? 'shop' : 'blog'); } }) 113 .catch(function (e) { if (e.name !== 'AbortError') hide(); }); 114 } 115 function update(force) { 116 var q = input.value.trim(), m = mode(), key = m + '|' + q; 117 if (!force && key === lastKey) return; lastKey = key; 118 if (!q) { logger.clear(); show(initialHtml()); return; } 119 clearTimeout(timer); 120 timer = setTimeout(function () { fetchQ(q, m); }, 120); 121 } 122 123 input.addEventListener('focus', function () { update(true); }); 124 input.addEventListener('blur', function () { logger.flush(); }); 125 input.addEventListener('input', function () { update(false); }); 126 if (select) select.addEventListener('change', function () { if (!panel.hidden) update(true); }); 127 input.addEventListener('keydown', function (e) { 128 if (panel.hidden) return; 129 var list = items(); 130 if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { 131 if (!list.length) return; 132 e.preventDefault(); 133 active = (active + (e.key === 'ArrowDown' ? 1 : -1) + list.length) % list.length; 134 Array.prototype.forEach.call(list, function (a, i) { a.classList.toggle('fk-s-active', i === active); }); 135 list[active].scrollIntoView({ block: 'nearest' }); 136 } else if (e.key === 'Enter' && active >= 0) { e.preventDefault(); window.location.href = list[active].href; } 137 else if (e.key === 'Escape') { hide(); input.blur(); } 138 }); 139 document.addEventListener('click', function (e) { if (!form.contains(e.target) && !panel.contains(e.target)) hide(); }); 140 panel.addEventListener('mousedown', function (e) { if (e.target.tagName !== 'BUTTON') e.preventDefault(); }); 141 panel.addEventListener('click', function (e) { 142 var link = e.target.closest && e.target.closest('a'); 143 if (link) { var ty = link.classList.contains('fk-s-all') ? 'all' : link.classList.contains('fk-s-brand') ? 'brand' : link.closest('.fk-s-cats') ? 'category' : link.closest('.fk-s-recipes') ? 'recipe' : link.closest('.fk-s-products') ? 'product' : link.closest('.fk-chips') ? 'popular' : 'other'; logger.click(ty); } if (e.target.classList.contains('fk-s-close')) { hide(); input.blur(); } }); 144 }); 145})();
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.