1 |
|
2 | globalThis.tsminibar = function tsminibar (form) {
|
3 | const { origin, collection } = form.dataset;
|
4 | const group = !!form.dataset.group;
|
5 | const cache = new Map();
|
6 | const state = { query: '', cursor: -1, open: false, hits: [] };
|
7 | const searchParams = 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 | 'x-typesense-api-key': form.dataset.key,
|
18 | ...Object.fromEntries(new URLSearchParams(form.dataset.searchParams))
|
19 | });
|
20 | const noResults = form.dataset.noResults || "No results for '{}'.";
|
21 |
|
22 | const input = form.querySelector('input[type=search]');
|
23 | const listbox = document.createElement('div');
|
24 | listbox.setAttribute('role', 'listbox');
|
25 | listbox.hidden = true;
|
26 | input.after(listbox);
|
27 |
|
28 | let preconnect = null;
|
29 | input.addEventListener('focus', function () {
|
30 | if (!preconnect) {
|
31 | preconnect = document.createElement('link');
|
32 | preconnect.rel = 'preconnect';
|
33 | preconnect.crossOrigin = 'anonymous';
|
34 | preconnect.href = origin;
|
35 | document.head.append(preconnect);
|
36 | }
|
37 | if (!state.open && state.hits.length) {
|
38 | state.open = true;
|
39 | render();
|
40 | }
|
41 | });
|
42 | input.addEventListener('input', async function () {
|
43 | const query = state.query = input.value;
|
44 | if (!query) {
|
45 | state.hits = [];
|
46 | state.cursor = -1;
|
47 | return close();
|
48 | }
|
49 | const hits = await search(query);
|
50 | if (state.query === query) {
|
51 | state.hits = hits;
|
52 | state.cursor = -1;
|
53 | state.open = true;
|
54 | render();
|
55 | }
|
56 | });
|
57 | input.addEventListener('click', function () {
|
58 | if (!state.open && state.hits.length) {
|
59 | state.open = true;
|
60 | render();
|
61 | }
|
62 | });
|
63 | input.addEventListener('keydown', function (e) {
|
64 | if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
65 | if (e.code === 'ArrowDown') moveCursor(1);
|
66 | if (e.code === 'ArrowUp') moveCursor(-1);
|
67 | if (e.code === 'Escape') close();
|
68 | if (e.code === 'Enter') {
|
69 | const url = state.hits[state.cursor]?.url;
|
70 | if (url) location.href = url;
|
71 | }
|
72 | }
|
73 | });
|
74 | form.addEventListener('submit', function (e) {
|
75 | e.preventDefault();
|
76 | });
|
77 | form.insertAdjacentHTML('beforeend', '<svg viewBox="0 0 12 12" width="20" height="20" aria-hidden="true" class="tsmb-icon-close" style="display: none;"><path d="M9 3L3 9M3 3L9 9"/></svg>');
|
78 | form.querySelector('.tsmb-icon-close').addEventListener('click', function () {
|
79 | input.value = '';
|
80 | input.focus();
|
81 | close();
|
82 | });
|
83 | connect();
|
84 |
|
85 | function close () {
|
86 | if (state.open) {
|
87 | state.cursor = -1;
|
88 | state.open = false;
|
89 | render();
|
90 | }
|
91 | }
|
92 |
|
93 | function connect () {
|
94 | document.addEventListener('click', onDocClick);
|
95 | if (form.dataset.slash !== 'false') {
|
96 | document.addEventListener('keydown', onDocSlash);
|
97 | form.classList.add('tsmb-form--slash');
|
98 | }
|
99 | }
|
100 |
|
101 | function disconnect () {
|
102 | document.removeEventListener('click', onDocClick);
|
103 | document.removeEventListener('keydown', onDocSlash);
|
104 | }
|
105 |
|
106 | function onDocClick (e) {
|
107 | if (!form.contains(e.target)) close();
|
108 | }
|
109 |
|
110 | function onDocSlash (e) {
|
111 | if (e.key === '/' && !/^(INPUT|TEXTAREA)$/.test(document.activeElement?.tagName)) {
|
112 | input.focus();
|
113 | e.preventDefault();
|
114 | }
|
115 | }
|
116 |
|
117 | async function search (query) {
|
118 | let lvl0;
|
119 | let hits = cache.get(query);
|
120 | if (hits) {
|
121 | cache.delete(query);
|
122 | cache.set(query, hits);
|
123 | return hits;
|
124 | }
|
125 | searchParams.set('q', query);
|
126 | const resp = await fetch(
|
127 | `${origin}/collections/${collection}/documents/search?` + searchParams,
|
128 | { mode: 'cors', credentials: 'omit', method: 'GET' }
|
129 | );
|
130 | const data = await resp.json();
|
131 | hits = data?.grouped_hits?.map(ghit => {
|
132 | const hit = ghit.hits[0];
|
133 | return {
|
134 | lvl0: group && lvl0 !== hit.document.hierarchy.lvl0 && (lvl0 = hit.document.hierarchy.lvl0),
|
135 | 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,
|
136 | url: hit.document.url,
|
137 | content: hit.highlights[0]?.snippet || hit.document.content || ''
|
138 | };
|
139 | }) || [];
|
140 | cache.set(query, hits);
|
141 | if (cache.size > 100) {
|
142 | cache.delete(cache.keys().next().value);
|
143 | }
|
144 | return hits;
|
145 | }
|
146 |
|
147 | function escape (s) {
|
148 | return s.replace(/['"<>&]/g, c => ({ "'": ''', '"': '"', '<': '<', '>': '>', '&': '&' }[c]));
|
149 | }
|
150 |
|
151 | function render () {
|
152 | listbox.hidden = !state.open;
|
153 | form.classList.toggle('tsmb-form--open', state.open);
|
154 | if (state.open) {
|
155 | 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('{}', escape(state.query))}</div>`) + (form.dataset.foot ? '<a href="https://typesense.org" class="tsmb-foot" title="Search by Typesense"></a>' : '');
|
156 | }
|
157 | }
|
158 |
|
159 | function moveCursor (offset) {
|
160 | state.cursor += offset;
|
161 |
|
162 | if (state.cursor >= state.hits.length) state.cursor = -1;
|
163 | if (state.cursor < -1) state.cursor = state.hits.length - 1;
|
164 | render();
|
165 | }
|
166 |
|
167 | return { form, connect, disconnect };
|
168 | };
|
169 | document.querySelectorAll('.tsmb-form[data-origin]').forEach(form => tsminibar(form));
|