|
71 | 71 | opacity: .8;
|
72 | 72 | }
|
73 | 73 | .osl-search-mark { background: rgba(255, 208, 0, .35); border-radius: .2rem; }
|
| 74 | +
|
| 75 | + /* Added outline and keyboard focus enhancements */ |
| 76 | + #mkdocs-search, |
| 77 | + #mkdocs-search-mobile { |
| 78 | + outline: 1px solid #e0e0e0; |
| 79 | + outline-offset: 2px; |
| 80 | + transition: all 0.2s ease; |
| 81 | + border: 1px solid var(--border-color, rgba(255,255,255,.12)); |
| 82 | + border-radius: 6px; |
| 83 | + } |
| 84 | +
|
| 85 | + #mkdocs-search:hover, |
| 86 | + #mkdocs-search-mobile:hover { |
| 87 | + border-color: var(--md-primary-fg-color, #1976d2); |
| 88 | + } |
| 89 | +
|
| 90 | + #mkdocs-search:focus, |
| 91 | + #mkdocs-search-mobile:focus { |
| 92 | + outline: 2px solid var(--md-primary-fg-color, #1976d2); |
| 93 | + border-color: var(--md-primary-fg-color, #1976d2); |
| 94 | + box-shadow: 0 0 0 4px var(--md-primary-fg-color-transparent, rgba(25, 118, 210, 0.1)); |
| 95 | + } |
| 96 | +
|
| 97 | + /* Search container styles */ |
| 98 | + .search-container { |
| 99 | + position: relative; |
| 100 | + display: inline-block; |
| 101 | + } |
| 102 | +
|
| 103 | + .search-container:focus-within .search-hint { |
| 104 | + opacity: 1; |
| 105 | + } |
| 106 | +
|
| 107 | + /* Add keyboard shortcut hint */ |
| 108 | + .search-hint { |
| 109 | + position: absolute; |
| 110 | + right: 10px; |
| 111 | + top: 50%; |
| 112 | + transform: translateY(-50%); |
| 113 | + font-size: 12px; |
| 114 | + opacity: 0.6; |
| 115 | + pointer-events: none; |
| 116 | + transition: opacity 0.2s ease; |
| 117 | + } |
| 118 | +
|
| 119 | + /* Hide hint when input has content */ |
| 120 | + .search-container input:not(:placeholder-shown) + .search-hint, |
| 121 | + .search-container input:focus + .search-hint { |
| 122 | + opacity: 0; |
| 123 | + visibility: hidden; |
| 124 | + } |
74 | 125 | `;
|
75 | 126 | const style = document.createElement('style');
|
76 | 127 | style.id = 'osl-search-styles';
|
|
95 | 146 | if (!res.ok) throw new Error(`Failed to fetch ${INDEX_URL}: ${res.status}`);
|
96 | 147 | const json = await res.json();
|
97 | 148 | // MkDocs "search" plugin typically returns { docs: [...] }
|
98 |
| - return Array.isArray(json) ? json : (json.docs || []); |
| 149 | + return Array.isArray(json) ? json : json.docs || []; |
99 | 150 | }
|
100 | 151 |
|
101 | 152 | function normalizeDocs(arr) {
|
102 |
| - return arr.map(d => ({ |
| 153 | + return arr.map((d) => ({ |
103 | 154 | title: d.title || '',
|
104 | 155 | text: d.text || '',
|
105 |
| - location: d.location || '' |
| 156 | + location: d.location || '', |
106 | 157 | }));
|
107 | 158 | }
|
108 | 159 |
|
|
114 | 165 |
|
115 | 166 | // find first occurrence of any term (basic, case-insensitive)
|
116 | 167 | const terms = q.split(/\s+/).filter(Boolean);
|
117 |
| - let hit = -1, termUsed = ''; |
| 168 | + let hit = -1, |
| 169 | + termUsed = ''; |
118 | 170 | for (const t of terms) {
|
119 | 171 | const idx = text.toLowerCase().indexOf(t.toLowerCase());
|
120 |
| - if (idx !== -1 && (hit === -1 || idx < hit)) { hit = idx; termUsed = t; } |
| 172 | + if (idx !== -1 && (hit === -1 || idx < hit)) { |
| 173 | + hit = idx; |
| 174 | + termUsed = t; |
| 175 | + } |
121 | 176 | }
|
122 | 177 | if (hit === -1) {
|
123 | 178 | return text.slice(0, MAX) + (text.length > MAX ? '…' : '');
|
124 | 179 | }
|
125 | 180 | const start = Math.max(0, hit - 40);
|
126 | 181 | const end = Math.min(text.length, hit + 120);
|
127 |
| - let snip = (start > 0 ? '…' : '') + text.slice(start, end) + (end < text.length ? '…' : ''); |
| 182 | + let snip = |
| 183 | + (start > 0 ? '…' : '') + |
| 184 | + text.slice(start, end) + |
| 185 | + (end < text.length ? '…' : ''); |
128 | 186 |
|
129 | 187 | // simple highlight for all terms
|
130 |
| - terms.forEach(t => { |
| 188 | + terms.forEach((t) => { |
131 | 189 | if (!t) return;
|
132 | 190 | const re = new RegExp(`(${escapeRegExp(t)})`, 'ig');
|
133 | 191 | snip = snip.replace(re, '<span class="osl-search-mark">$1</span>');
|
|
148 | 206 |
|
149 | 207 | const raw = await fetchIndexJSON();
|
150 | 208 | _docs = normalizeDocs(raw);
|
151 |
| - _byRef = new Map(_docs.map(d => [d.location, d])); |
| 209 | + _byRef = new Map(_docs.map((d) => [d.location, d])); |
152 | 210 |
|
153 | 211 | // Build index
|
154 | 212 | const hasMulti = typeof lunr.multiLanguage === 'function';
|
|
161 | 219 | this.field('title', { boost: 10 });
|
162 | 220 | this.field('text');
|
163 | 221 |
|
164 |
| - _docs.forEach(doc => this.add(doc)); |
| 222 | + _docs.forEach((doc) => this.add(doc)); |
165 | 223 | });
|
166 | 224 |
|
167 | 225 | return _idx;
|
|
180 | 238 | // Keep it simple; allow prefix matches
|
181 | 239 | let q = query.trim();
|
182 | 240 | // Improve small queries a bit: foo -> foo*
|
183 |
| - if (!/[~^*]/.test(q)) q = q.split(/\s+/).map(t => t + '*').join(' '); |
| 241 | + if (!/[~^*]/.test(q)) |
| 242 | + q = q |
| 243 | + .split(/\s+/) |
| 244 | + .map((t) => t + '*') |
| 245 | + .join(' '); |
184 | 246 | let hits = [];
|
185 | 247 | try {
|
186 | 248 | hits = _idx.search(q);
|
187 | 249 | } catch (e) {
|
188 | 250 | // fallback: plain search without wildcard if syntax error
|
189 |
| - try { hits = _idx.search(query); } catch (_e) { hits = []; } |
| 251 | + try { |
| 252 | + hits = _idx.search(query); |
| 253 | + } catch (_e) { |
| 254 | + hits = []; |
| 255 | + } |
190 | 256 | }
|
191 |
| - return hits.slice(0, MAX_RESULTS).map(h => { |
192 |
| - const doc = _byRef.get(h.ref); |
193 |
| - return doc ? { doc, score: h.score } : null; |
194 |
| - }).filter(Boolean); |
| 257 | + return hits |
| 258 | + .slice(0, MAX_RESULTS) |
| 259 | + .map((h) => { |
| 260 | + const doc = _byRef.get(h.ref); |
| 261 | + return doc ? { doc, score: h.score } : null; |
| 262 | + }) |
| 263 | + .filter(Boolean); |
195 | 264 | }
|
196 | 265 |
|
197 | 266 | // --- UI (panel) ---------------------------------------------------------
|
|
220 | 289 | if (!items.length) {
|
221 | 290 | const empty = document.createElement('div');
|
222 | 291 | empty.className = 'osl-search-empty';
|
223 |
| - empty.textContent = (rawQuery && rawQuery.length >= MIN_QUERY_LEN) ? 'No results' : 'Type to search…'; |
| 292 | + empty.textContent = |
| 293 | + rawQuery && rawQuery.length >= MIN_QUERY_LEN |
| 294 | + ? 'No results' |
| 295 | + : 'Type to search…'; |
224 | 296 | panel.appendChild(empty);
|
225 | 297 | return;
|
226 | 298 | }
|
|
247 | 319 | function activateItem(panel, nextIndex) {
|
248 | 320 | const items = Array.from(panel.querySelectorAll('.osl-search-item'));
|
249 | 321 | if (!items.length) return -1;
|
250 |
| - items.forEach(el => el.classList.remove('is-active')); |
| 322 | + items.forEach((el) => el.classList.remove('is-active')); |
251 | 323 | const idx = Math.max(0, Math.min(nextIndex, items.length - 1));
|
252 | 324 | items[idx].classList.add('is-active');
|
253 | 325 | items[idx].scrollIntoView({ block: 'nearest' });
|
|
260 | 332 | }
|
261 | 333 |
|
262 | 334 | function navigateActive(panel) {
|
263 |
| - const active = panel.querySelector('.osl-search-item.is-active') || |
264 |
| - panel.querySelector('.osl-search-item'); |
| 335 | + const active = |
| 336 | + panel.querySelector('.osl-search-item.is-active') || |
| 337 | + panel.querySelector('.osl-search-item'); |
265 | 338 | if (active) window.location.assign(active.href);
|
266 | 339 | }
|
267 | 340 |
|
|
276 | 349 | if (!inputEl || inputEl.__oslWired__) return;
|
277 | 350 | inputEl.__oslWired__ = true;
|
278 | 351 |
|
| 352 | + // Add ARIA attributes |
| 353 | + inputEl.setAttribute('role', 'searchbox'); |
| 354 | + inputEl.setAttribute('aria-label', 'Search'); |
| 355 | + inputEl.setAttribute('aria-expanded', 'false'); |
| 356 | + |
279 | 357 | injectBaseStylesOnce();
|
280 | 358 | const panel = mkPanel();
|
281 | 359 | let lastQuery = '';
|
|
284 | 362 | function openPanel() {
|
285 | 363 | positionPanel(panel, inputEl);
|
286 | 364 | panel.style.display = 'block';
|
| 365 | + inputEl.setAttribute('aria-expanded', 'true'); |
287 | 366 | }
|
288 | 367 |
|
289 | 368 | function updatePosition() {
|
290 | 369 | if (panel.style.display !== 'none') positionPanel(panel, inputEl);
|
291 | 370 | }
|
292 | 371 |
|
| 372 | + function closePanel(panel) { |
| 373 | + panel.style.display = 'none'; |
| 374 | + panel.innerHTML = ''; |
| 375 | + inputEl.setAttribute('aria-expanded', 'false'); |
| 376 | + } |
293 | 377 | // Debounce to keep it snappy
|
294 | 378 | let t = null;
|
295 | 379 | function onInput() {
|
|
318 | 402 | }
|
319 | 403 |
|
320 | 404 | function onKey(e) {
|
321 |
| - if (panel.style.display === 'none' && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { |
| 405 | + if ( |
| 406 | + panel.style.display === 'none' && |
| 407 | + (e.key === 'ArrowDown' || e.key === 'ArrowUp') |
| 408 | + ) { |
322 | 409 | openPanel();
|
323 | 410 | }
|
324 | 411 | switch (e.key) {
|
|
397 | 484 | inputEl.addEventListener('blur', onBlur);
|
398 | 485 | }
|
399 | 486 |
|
| 487 | + function setupKeyboardShortcuts() { |
| 488 | + document.addEventListener('keydown', (e) => { |
| 489 | + // Check for Ctrl+K or Cmd+K |
| 490 | + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { |
| 491 | + e.preventDefault(); |
| 492 | + const searchInput = |
| 493 | + document.getElementById('mkdocs-search') || |
| 494 | + document.getElementById('mkdocs-search-mobile'); |
| 495 | + if (searchInput) { |
| 496 | + searchInput.focus(); |
| 497 | + } |
| 498 | + } |
| 499 | + }); |
| 500 | + } |
| 501 | + |
400 | 502 | // Public initializer (used by theme.js)
|
401 | 503 | window.initSearch = function (inputEl) {
|
402 | 504 | if (!inputEl) return;
|
|
407 | 509 | document.addEventListener('DOMContentLoaded', () => {
|
408 | 510 | const desktop = document.getElementById('mkdocs-search');
|
409 | 511 | const mobile = document.getElementById('mkdocs-search-mobile');
|
410 |
| - if (desktop) wireInput(desktop); |
411 |
| - if (mobile) wireInput(mobile); |
412 |
| - }); |
413 | 512 |
|
| 513 | + if (desktop) { |
| 514 | + // Wrap search input in container |
| 515 | + const container = document.createElement('div'); |
| 516 | + container.className = 'search-container'; |
| 517 | + desktop.parentNode.insertBefore(container, desktop); |
| 518 | + container.appendChild(desktop); |
| 519 | + |
| 520 | + wireInput(desktop); |
| 521 | + |
| 522 | + // Add keyboard shortcut hint inside container |
| 523 | + const hint = document.createElement('span'); |
| 524 | + hint.className = 'search-hint'; |
| 525 | + hint.textContent = navigator.platform.includes('Mac') ? '⌘K' : 'Ctrl+K'; |
| 526 | + container.appendChild(hint); |
| 527 | + } |
| 528 | + |
| 529 | + if (mobile) { |
| 530 | + // Wrap mobile search input in container |
| 531 | + const container = document.createElement('div'); |
| 532 | + container.className = 'search-container'; |
| 533 | + mobile.parentNode.insertBefore(container, mobile); |
| 534 | + container.appendChild(mobile); |
| 535 | + |
| 536 | + wireInput(mobile); |
| 537 | + } |
| 538 | + |
| 539 | + setupKeyboardShortcuts(); |
| 540 | + }); |
414 | 541 | })();
|
0 commit comments