|
1 | 1 | (function() { |
2 | | - const BASE = 'https://processing-cpp.github.io'; |
| 2 | + const SITE = 'https://processing-cpp.github.io'; |
3 | 3 |
|
4 | | - // Inject search bar into nav after nav.js runs |
5 | 4 | function injectSearch() { |
6 | 5 | const nav = document.getElementById('site-nav'); |
7 | 6 | if (!nav) return; |
| 7 | + if (document.getElementById('search-wrap')) return; // already injected |
8 | 8 |
|
9 | 9 | const wrap = document.createElement('div'); |
10 | 10 | wrap.id = 'search-wrap'; |
|
19 | 19 | const style = document.createElement('style'); |
20 | 20 | style.textContent = ` |
21 | 21 | #search-wrap { position: relative; margin-left: auto; padding-right: 1rem; } |
22 | | - #search-box { position: relative; } |
23 | 22 | #search-input { |
24 | | - width: 220px; |
25 | | - padding: 6px 12px; |
26 | | - border: 1px solid #e0e0e0; |
27 | | - border-radius: 20px; |
28 | | - font-size: 13px; |
29 | | - font-family: inherit; |
30 | | - background: #f8f8f8; |
31 | | - outline: none; |
| 23 | + width: 220px; padding: 6px 12px; |
| 24 | + border: 1px solid #e0e0e0; border-radius: 20px; |
| 25 | + font-size: 13px; font-family: inherit; |
| 26 | + background: #f8f8f8; outline: none; |
32 | 27 | transition: border-color 0.15s, width 0.2s; |
33 | 28 | } |
34 | | - #search-input:focus { |
35 | | - border-color: #aaa; |
36 | | - background: #fff; |
37 | | - width: 280px; |
38 | | - } |
| 29 | + #search-input:focus { border-color: #aaa; background: #fff; width: 280px; } |
39 | 30 | #search-results { |
40 | | - position: absolute; |
41 | | - top: calc(100% + 8px); |
42 | | - right: 0; |
43 | | - width: 360px; |
44 | | - background: #fff; |
45 | | - border: 1px solid #e0e0e0; |
46 | | - border-radius: 8px; |
| 31 | + position: absolute; top: calc(100% + 8px); right: 0; |
| 32 | + width: 360px; background: #fff; |
| 33 | + border: 1px solid #e0e0e0; border-radius: 8px; |
47 | 34 | box-shadow: 0 8px 24px rgba(0,0,0,0.1); |
48 | | - max-height: 400px; |
49 | | - overflow-y: auto; |
50 | | - z-index: 1000; |
51 | | - } |
52 | | - .search-result { |
53 | | - display: block; |
54 | | - padding: 10px 14px; |
55 | | - border-bottom: 1px solid #f0f0f0; |
56 | | - text-decoration: none; |
57 | | - color: #111; |
58 | | - transition: background 0.1s; |
59 | | - } |
60 | | - .search-result:last-child { border-bottom: none; } |
61 | | - .search-result:hover { background: #f8f8f8; } |
62 | | - .search-result-name { |
63 | | - font-family: "SF Mono","Fira Code",monospace; |
64 | | - font-size: 13px; |
65 | | - font-weight: 600; |
66 | | - color: #111; |
67 | | - } |
68 | | - .search-result-name mark { |
69 | | - background: #fff3b0; |
70 | | - color: #111; |
71 | | - border-radius: 2px; |
72 | | - padding: 0 1px; |
73 | | - } |
74 | | - .search-result-cat { |
75 | | - font-size: 11px; |
76 | | - color: #aaa; |
77 | | - margin-top: 1px; |
78 | | - } |
79 | | - .search-result-desc { |
80 | | - font-size: 12px; |
81 | | - color: #666; |
82 | | - margin-top: 3px; |
83 | | - white-space: nowrap; |
84 | | - overflow: hidden; |
85 | | - text-overflow: ellipsis; |
86 | | - } |
87 | | - .search-no-results { |
88 | | - padding: 16px 14px; |
89 | | - font-size: 13px; |
90 | | - color: #aaa; |
91 | | - text-align: center; |
| 35 | + max-height: 400px; overflow-y: auto; z-index: 9999; |
92 | 36 | } |
| 37 | + .sr { display: block; padding: 10px 14px; border-bottom: 1px solid #f0f0f0; text-decoration: none; color: #111; } |
| 38 | + .sr:last-child { border-bottom: none; } |
| 39 | + .sr:hover, .sr:focus { background: #f8f8f8; outline: none; } |
| 40 | + .sr-name { font-family: "SF Mono","Fira Code",monospace; font-size: 13px; font-weight: 600; } |
| 41 | + .sr-name mark { background: #fff3b0; border-radius: 2px; padding: 0 1px; color: #111; } |
| 42 | + .sr-cat { font-size: 11px; color: #aaa; margin-top: 1px; } |
| 43 | + .sr-desc { font-size: 12px; color: #666; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| 44 | + .sr-empty { padding: 16px 14px; font-size: 13px; color: #aaa; text-align: center; } |
93 | 45 | @media (max-width: 768px) { |
94 | | - #search-wrap { padding-right: 0.5rem; } |
95 | | - #search-input { width: 140px; } |
96 | | - #search-input:focus { width: 180px; } |
97 | | - #search-results { width: 280px; right: 0; } |
| 46 | + #search-input { width: 130px; } |
| 47 | + #search-input:focus { width: 170px; } |
| 48 | + #search-results { width: 280px; } |
98 | 49 | } |
99 | 50 | `; |
100 | 51 | document.head.appendChild(style); |
|
103 | 54 | const results = document.getElementById('search-results'); |
104 | 55 | let index = null; |
105 | 56 |
|
106 | | - // Load index |
107 | | - fetch(BASE + '/assets/search-index.json') |
| 57 | + // Always use absolute URL so it works from any page depth |
| 58 | + fetch(SITE + '/assets/search-index.json') |
108 | 59 | .then(r => r.json()) |
109 | | - .then(data => { index = data; }); |
| 60 | + .then(d => { index = d; }); |
110 | 61 |
|
111 | | - function highlight(text, query) { |
112 | | - const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
113 | | - return text.replace(new RegExp(`(${escaped})`, 'gi'), '<mark>$1</mark>'); |
| 62 | + function esc(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } |
| 63 | + |
| 64 | + function highlight(text, q) { |
| 65 | + return text.replace(new RegExp(`(${esc(q)})`, 'gi'), '<mark>$1</mark>'); |
114 | 66 | } |
115 | 67 |
|
116 | | - function search(q) { |
117 | | - if (!index || q.length < 1) { results.hidden = true; return; } |
| 68 | + function doSearch(q) { |
| 69 | + if (!index || !q) { results.hidden = true; return; } |
118 | 70 | const ql = q.toLowerCase(); |
119 | | - const matches = index.filter(e => |
| 71 | + const hits = index.filter(e => |
120 | 72 | e.name.toLowerCase().includes(ql) || |
121 | 73 | e.cat.toLowerCase().includes(ql) || |
122 | 74 | e.desc.toLowerCase().includes(ql) |
123 | 75 | ).slice(0, 12); |
124 | 76 |
|
125 | | - if (!matches.length) { |
126 | | - results.innerHTML = `<div class="search-no-results">No results for "${q}"</div>`; |
127 | | - results.hidden = false; |
128 | | - return; |
| 77 | + if (!hits.length) { |
| 78 | + results.innerHTML = `<div class="sr-empty">No results for "${q}"</div>`; |
| 79 | + } else { |
| 80 | + results.innerHTML = hits.map(e => { |
| 81 | + const cat = e.subcat ? `${e.cat} / ${e.subcat}` : e.cat; |
| 82 | + return `<a class="sr" href="${SITE}${e.url}"> |
| 83 | + <div class="sr-name">${highlight(e.name, q)}</div> |
| 84 | + <div class="sr-cat">${cat}</div> |
| 85 | + <div class="sr-desc">${e.desc}</div> |
| 86 | + </a>`; |
| 87 | + }).join(''); |
129 | 88 | } |
130 | | - |
131 | | - results.innerHTML = matches.map(e => { |
132 | | - const cat = e.subcat ? `${e.cat} / ${e.subcat}` : e.cat; |
133 | | - return `<a class="search-result" href="${BASE}${e.url}"> |
134 | | - <div class="search-result-name">${highlight(e.name, q)}</div> |
135 | | - <div class="search-result-cat">${cat}</div> |
136 | | - <div class="search-result-desc">${e.desc}</div> |
137 | | - </a>`; |
138 | | - }).join(''); |
139 | 89 | results.hidden = false; |
140 | 90 | } |
141 | 91 |
|
142 | | - input.addEventListener('input', e => search(e.target.value.trim())); |
| 92 | + input.addEventListener('input', e => doSearch(e.target.value.trim())); |
143 | 93 |
|
144 | 94 | input.addEventListener('keydown', e => { |
145 | 95 | if (e.key === 'Escape') { results.hidden = true; input.blur(); } |
146 | 96 | if (e.key === 'Enter') { |
147 | | - const first = results.querySelector('.search-result'); |
| 97 | + const first = results.querySelector('.sr'); |
148 | 98 | if (first) window.location.href = first.href; |
149 | 99 | } |
150 | 100 | if (e.key === 'ArrowDown') { |
151 | 101 | e.preventDefault(); |
152 | | - const items = [...results.querySelectorAll('.search-result')]; |
153 | | - if (items.length) items[0].focus(); |
| 102 | + const first = results.querySelector('.sr'); |
| 103 | + if (first) first.focus(); |
154 | 104 | } |
155 | 105 | }); |
156 | 106 |
|
157 | 107 | results.addEventListener('keydown', e => { |
158 | | - const items = [...results.querySelectorAll('.search-result')]; |
| 108 | + const items = [...results.querySelectorAll('.sr')]; |
159 | 109 | const idx = items.indexOf(document.activeElement); |
160 | 110 | if (e.key === 'ArrowDown' && idx < items.length - 1) { e.preventDefault(); items[idx+1].focus(); } |
161 | 111 | if (e.key === 'ArrowUp') { e.preventDefault(); idx > 0 ? items[idx-1].focus() : input.focus(); } |
|
0 commit comments