1/*! https://github.com/jquery/typesense-minibar 1.3.4 (TrailSports custom header build) */ 2/* eslint-disable no-console */ 3(function () { 4 const HEADER_KEY = 'X-TYPESENSE-API-KEY'; 5 6 const mergeSearchParams = (dataset) => { 7 const params = new URLSearchParams({ 8 per_page: '5', 9 query_by: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,content', 10 include_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,content,url_without_anchor,url,id', 11 highlight_full_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,content', 12 group_by: 'url_without_anchor', 13 group_limit: '1', 14 sort_by: 'item_priority:desc', 15 snippet_threshold: '8', 16 highlight_affix_num_tokens: '12', 17 }); 18 19 if (dataset.searchParams) { 20 try { 21 new URLSearchParams(dataset.searchParams).forEach((value, key) => { 22 if (key.toLowerCase() !== 'x-typesense-api-key') { 23 params.set(key, value); 24 } 25 }); 26 } catch (err) { 27 console.warn('[TrailSportsTypesense] Failed to parse data-search-params:', err); 28 } 29 } 30 31 return params; 32 }; 33 34 globalThis.tsminibar = function tsminibar(form, dataset = form.dataset) { 35 const cache = new Map(); 36 const state = { query: '', cursor: -1, open: false, hits: [] }; 37 const searchParams = mergeSearchParams(dataset); 38 const noResults = dataset.noResults || "No results for '{}'."; 39 40 const apiKey = dataset.key; 41 if (!apiKey) { 42 console.warn('[TrailSportsTypesense] Missing data-key for Typesense minibar.'); 43 return { form, connect: () => {}, disconnect: () => {} }; 44 } 45 46 const input = form.querySelector('input[type=search]'); 47 const listbox = document.createElement('div'); 48 listbox.setAttribute('role', 'listbox'); 49 listbox.hidden = true; 50 input.after(listbox); 51 52 let preconnect = null; 53 input.addEventListener('focus', function () { 54 if (!preconnect) { 55 preconnect = document.createElement('link'); 56 preconnect.rel = 'preconnect'; 57 preconnect.crossOrigin = 'anonymous'; // match fetch cors,credentials:omit 58 preconnect.href = dataset.origin; 59 document.head.append(preconnect); 60 } 61 if (!state.open && state.hits.length) { 62 state.open = true; 63 render(); 64 } 65 }); 66 input.addEventListener('input', async function () { 67 const query = state.query = input.value; 68 if (!query) { 69 state.hits = []; 70 return close(); 71 } 72 const hits = await search(query); 73 if (state.query === query) { // ignore non-current query 74 state.hits = hits; 75 state.cursor = -1; 76 state.open = true; 77 render(); 78 } 79 }); 80 input.addEventListener('click', function () { 81 if (!state.open && state.hits.length) { 82 state.open = true; 83 render(); 84 } 85 }); 86 input.addEventListener('keydown', function (e) { 87 if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 88 if (e.code === 'ArrowDown') moveCursor(1); 89 if (e.code === 'ArrowUp') moveCursor(-1); 90 if (e.code === 'Escape') { 91 close(); 92 input.blur(); 93 } 94 if (e.code === 'Enter') { 95 const url = state.hits[state.cursor]?.url; 96 if (url) location.href = url; 97 } 98 } 99 }); 100 form.addEventListener('submit', function (e) { 101 e.preventDefault(); 102 }); 103 form.insertAdjacentHTML('beforeend', '<svg viewBox="0 0 12 12" width="20" height="20" aria-hidden="true" tabindex="-1" class="tsmb-icon-close"><path d="M9 3L3 9M3 3L9 9"/></svg>'); 104 form.querySelector('.tsmb-icon-close').addEventListener('click', function () { 105 input.value = ''; 106 state.hits = []; 107 close(); 108 input.focus(); 109 }); 110 connect(); 111 112 function close () { 113 if (state.open) { 114 state.cursor = -1; 115 state.open = false; 116 render(); 117 } 118 } 119 120 function connect () { 121 document.addEventListener('click', onDocClick); 122 if (dataset.slash !== 'false') { 123 document.addEventListener('keydown', onDocSlash); 124 form.classList.add('tsmb-form--slash'); 125 } 126 } 127 128 function disconnect () { 129 document.removeEventListener('click', onDocClick); 130 document.removeEventListener('keydown', onDocSlash); 131 } 132 133 function onDocClick (e) { 134 if (!form.contains(e.target)) close(); 135 } 136 137 function onDocSlash (e) { 138 if (e.key === '/' && !/^(INPUT|TEXTAREA)$/.test(document.activeElement?.tagName)) { 139 input.focus(); 140 e.preventDefault(); 141 } 142 } 143 144 async function search (query) { 145 let lvl0; 146 let hits = cache.get(query); 147 if (hits) { 148 cache.delete(query); 149 cache.set(query, hits); 150 return hits; 151 } 152 searchParams.set('q', query); 153 const url = `${dataset.origin}/collections/${dataset.collection}/documents/search?${searchParams}`; 154 const resp = await fetch( 155 url, 156 { 157 mode: 'cors', 158 credentials: 'omit', 159 method: 'GET', 160 headers: { 161 [HEADER_KEY]: apiKey, 162 }, 163 } 164 ); 165 const group = !!dataset.group; 166 const data = await resp.json(); 167 hits = data?.grouped_hits?.map(ghit => { 168 const hit = ghit.hits[0]; 169 return { 170 lvl0: group && lvl0 !== hit.document.hierarchy.lvl0 && (lvl0 = hit.document.hierarchy.lvl0), 171 title: [!group && hit.document.hierarchy.lvl0, hit.document.hierarchy.lvl1, hit.document.hierarchy.lvl2, hit.document.hierarchy.lvl3, hit.document.hierarchy.lvl4, hit.document.hierarchy.lvl5].filter(lvl => !!lvl).join(' ⺠') || hit.document.hierarchy.lvl0, 172 url: hit.document.url, 173 content: hit.highlights[0]?.snippet || hit.document.content || '', 174 }; 175 }) || []; 176 cache.set(query, hits); 177 if (cache.size > 100) { 178 cache.delete(cache.keys().next().value); 179 } 180 return hits; 181 } 182 183 function escapeHtml (s) { 184 return s.replace(/['"<>&]/g, c =>
184 ({ "'": ''', '"': '"', '<': '<', '>': '>', '&': '&' }[c])); 185 } 186 187 function render () { 188 listbox.hidden = !state.open; 189 form.classList.toggle('tsmb-form--open', state.open); 190 listbox.innerHTML = (state.hits.map((hit, i) => `<div role="option"${i === state.cursor ? ' aria-selected="true"' : ''}>${hit.lvl0 ? `<div class="tsmb-suggestion_group">${hit.lvl0}</div>` : ''}<a href="${hit.url}" tabindex="-1"><div class="tsmb-suggestion_title">${hit.title}</div><div class="tsmb-suggestion_content">${hit.content}</div></a></div>`).join('') || `<div class="tsmb-empty">${noResults.replace('{}', escapeHtml(state.query))}</div>`) + (dataset.foot ? '<a href="https://typesense.org" class="tsmb-foot" title="Search by Typesense"></a>' : ''); 191 } 192 193 function moveCursor (offset) { 194 state.cursor += offset; 195 if (state.cursor >= state.hits.length) state.cursor = -1; 196 if (state.cursor < -1) state.cursor = state.hits.length - 1; 197 render(); 198 } 199 200 return { form, connect, disconnect }; 201 }; 202 203 window.customElements.define('typesense-minibar', class extends HTMLElement { 204 connectedCallback () { 205 const form = this.querySelector('form'); 206 if (form && this.dataset.origin) tsminibar(form, this.dataset); 207 } 208 }); 209 210 document.querySelectorAll('.tsmb-form[data-origin]').forEach(form => tsminibar(form)); 211})(); 212
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.