|
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