Skip to content

Commit 6010ff3

Browse files
committed
Add global search bar across all pages
1 parent 15a6305 commit 6010ff3

3 files changed

Lines changed: 183 additions & 4 deletions

File tree

assets/nav.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
(function() {
22
const path = window.location.pathname;
3-
const isRoot = path === '/' || path === '/index.html';
3+
const isRoot = path === '/' || path.endsWith('/index.html') && path.split('/').length <= 2;
44
const parts = path.replace(/\/$/, '').split('/').filter(Boolean);
55
const prefix = (isRoot || parts.length === 0) ? '/' : '../';
66

7-
function isActive(name) {
8-
return path.includes('/' + name);
9-
}
7+
function isActive(name) { return path.includes('/' + name); }
108

119
function link(name, label) {
1210
const active = isActive(name) ? ' class="active"' : '';
@@ -35,4 +33,9 @@
3533
${link('about', 'About')}
3634
`;
3735
}
36+
37+
// Load search
38+
const script = document.createElement('script');
39+
script.src = prefix + 'assets/search.js';
40+
document.head.appendChild(script);
3841
})();

assets/search-index.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

assets/search.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
(function() {
2+
const BASE = 'https://processing-cpp.github.io';
3+
4+
// Inject search bar into nav after nav.js runs
5+
function injectSearch() {
6+
const nav = document.getElementById('site-nav');
7+
if (!nav) return;
8+
9+
const wrap = document.createElement('div');
10+
wrap.id = 'search-wrap';
11+
wrap.innerHTML = `
12+
<div id="search-box">
13+
<input id="search-input" type="text" placeholder="Search reference…" autocomplete="off" spellcheck="false">
14+
<div id="search-results" hidden></div>
15+
</div>
16+
`;
17+
nav.appendChild(wrap);
18+
19+
const style = document.createElement('style');
20+
style.textContent = `
21+
#search-wrap { position: relative; margin-left: auto; padding-right: 1rem; }
22+
#search-box { position: relative; }
23+
#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;
32+
transition: border-color 0.15s, width 0.2s;
33+
}
34+
#search-input:focus {
35+
border-color: #aaa;
36+
background: #fff;
37+
width: 280px;
38+
}
39+
#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;
47+
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;
92+
}
93+
@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; }
98+
}
99+
`;
100+
document.head.appendChild(style);
101+
102+
const input = document.getElementById('search-input');
103+
const results = document.getElementById('search-results');
104+
let index = null;
105+
106+
// Load index
107+
fetch(BASE + '/assets/search-index.json')
108+
.then(r => r.json())
109+
.then(data => { index = data; });
110+
111+
function highlight(text, query) {
112+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113+
return text.replace(new RegExp(`(${escaped})`, 'gi'), '<mark>$1</mark>');
114+
}
115+
116+
function search(q) {
117+
if (!index || q.length < 1) { results.hidden = true; return; }
118+
const ql = q.toLowerCase();
119+
const matches = index.filter(e =>
120+
e.name.toLowerCase().includes(ql) ||
121+
e.cat.toLowerCase().includes(ql) ||
122+
e.desc.toLowerCase().includes(ql)
123+
).slice(0, 12);
124+
125+
if (!matches.length) {
126+
results.innerHTML = `<div class="search-no-results">No results for "${q}"</div>`;
127+
results.hidden = false;
128+
return;
129+
}
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+
results.hidden = false;
140+
}
141+
142+
input.addEventListener('input', e => search(e.target.value.trim()));
143+
144+
input.addEventListener('keydown', e => {
145+
if (e.key === 'Escape') { results.hidden = true; input.blur(); }
146+
if (e.key === 'Enter') {
147+
const first = results.querySelector('.search-result');
148+
if (first) window.location.href = first.href;
149+
}
150+
if (e.key === 'ArrowDown') {
151+
e.preventDefault();
152+
const items = [...results.querySelectorAll('.search-result')];
153+
if (items.length) items[0].focus();
154+
}
155+
});
156+
157+
results.addEventListener('keydown', e => {
158+
const items = [...results.querySelectorAll('.search-result')];
159+
const idx = items.indexOf(document.activeElement);
160+
if (e.key === 'ArrowDown' && idx < items.length - 1) { e.preventDefault(); items[idx+1].focus(); }
161+
if (e.key === 'ArrowUp') { e.preventDefault(); idx > 0 ? items[idx-1].focus() : input.focus(); }
162+
if (e.key === 'Escape') { results.hidden = true; input.focus(); }
163+
});
164+
165+
document.addEventListener('click', e => {
166+
if (!wrap.contains(e.target)) results.hidden = true;
167+
});
168+
}
169+
170+
if (document.readyState === 'loading') {
171+
document.addEventListener('DOMContentLoaded', injectSearch);
172+
} else {
173+
injectSearch();
174+
}
175+
})();

0 commit comments

Comments
 (0)