PageSourceSearch

https://trailsports.com/wp-content/plugins/trailsports-typesense-minibar/assets/js/typesense-minibar.js

js trailsports.com collected 2026-10-08 15:56:16 UTC 7,547 bytes, 212 lines download raw bytes

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 ({ "'": '&#039;', '"': '&quot;', '<': '&lt;', '>': '&gt;', '&': '&amp;' }[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.