Skip to content

Commit 87190b2

Browse files
committed
search on people pages!
1 parent a0790f3 commit 87190b2

File tree

10 files changed

+387
-5
lines changed

10 files changed

+387
-5
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!-- Search Bar -->
2+
<div id="people-search-group" class="input-group mb-4">
3+
<input type="text" id="people-search-input" class="form-control" placeholder="Search people… (Esc to clear)" aria-label="Search people">
4+
<button class="btn btn-secondary" type="button" id="people-search-clear">Clear</button>
5+
</div>
6+
7+
<!-- Category Buttons (only show on pages with multiple types) -->
8+
{% if include.show_categories %}
9+
<div id="people-cat-buttons" class="d-none d-md-flex flex-wrap gap-2 mb-4">
10+
<!-- Categories will be dynamically generated -->
11+
</div>
12+
{% endif %}
13+
14+
<!-- Include the original people roll -->
15+
{% include people_roll.html type=include.type %}
16+
17+
<!-- Include search script -->
18+
<script src="{{ site.url }}/css/people-search.js"></script>

css/people-search.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
document.addEventListener('DOMContentLoaded', function() {
2+
const searchInput = document.getElementById('people-search-input');
3+
const searchClear = document.getElementById('people-search-clear');
4+
const categoryButtons = document.getElementById('people-cat-buttons');
5+
const peopleContainers = document.querySelectorAll('.my-row-zebra');
6+
7+
let activeCategory = 'all';
8+
9+
// Highlight matching text
10+
function highlightText(element, searchTerm) {
11+
if (!searchTerm) return;
12+
13+
const walker = document.createTreeWalker(
14+
element,
15+
NodeFilter.SHOW_TEXT,
16+
null,
17+
false
18+
);
19+
20+
const textNodes = [];
21+
let node;
22+
while (node = walker.nextNode()) {
23+
textNodes.push(node);
24+
}
25+
26+
textNodes.forEach(textNode => {
27+
const parent = textNode.parentNode;
28+
if (parent.tagName === 'SCRIPT' || parent.tagName === 'STYLE') return;
29+
30+
const text = textNode.textContent;
31+
let regex;
32+
33+
// Smart case matching for highlighting
34+
if (searchTerm !== searchTerm.toLowerCase()) {
35+
// Case sensitive
36+
regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'g');
37+
} else {
38+
// Case insensitive
39+
regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
40+
}
41+
42+
if (regex.test(text)) {
43+
const highlightedText = text.replace(regex, '<mark class="search-highlight" style="padding:0; margin:0; background-color:yellow; color:black;">$1</mark>');
44+
const span = document.createElement('span');
45+
span.innerHTML = highlightedText;
46+
parent.replaceChild(span, textNode);
47+
}
48+
});
49+
}
50+
51+
// Remove highlighting
52+
function removeHighlighting() {
53+
const highlights = document.querySelectorAll('.search-highlight');
54+
highlights.forEach(highlight => {
55+
const parent = highlight.parentNode;
56+
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
57+
parent.normalize();
58+
});
59+
}
60+
61+
// Add data attributes to people rows for easier searching
62+
function initializePeopleData() {
63+
peopleContainers.forEach(container => {
64+
const rows = container.querySelectorAll('.row');
65+
rows.forEach(row => {
66+
// Extract person information
67+
const nameLink = row.querySelector('a.nonupper-h5');
68+
if (!nameLink) return;
69+
70+
// Extract UVA ID from the link href
71+
const href = nameLink.getAttribute('href');
72+
const uvaIdMatch = href?.match(/\/people\/([^\/]+)\//);
73+
const uvaId = uvaIdMatch ? uvaIdMatch[1] : '';
74+
75+
const fullName = nameLink.textContent.trim();
76+
const position = row.querySelector('div i')?.parentElement?.textContent?.trim() || '';
77+
const specialty = row.querySelector('div[style*="font-size:0.9em"]')?.textContent?.trim() || '';
78+
const office = row.querySelector('.fa-building')?.nextSibling?.textContent?.trim() || '';
79+
const email = row.querySelector('a[href^="mailto:"]')?.textContent?.trim() || '';
80+
const phone = row.querySelector('a[href^="tel:"]')?.textContent?.trim() || '';
81+
const officeHours = row.querySelector('.fa-clock')?.parentElement?.textContent?.replace('Office hours:', '').trim() || '';
82+
const researchTags = Array.from(row.querySelectorAll('.btn-secondary')).map(btn => btn.textContent.trim()).join(' ');
83+
84+
// Determine category based on section header
85+
let category = 'other';
86+
let currentElement = container.previousElementSibling;
87+
while (currentElement) {
88+
if (currentElement.tagName === 'H2') {
89+
const headerText = currentElement.textContent.toLowerCase();
90+
if (headerText.includes('faculty') && !headerText.includes('emeritus')) {
91+
category = 'faculty';
92+
} else if (headerText.includes('postdoc')) {
93+
category = 'postdoc';
94+
} else if (headerText.includes('lecturer')) {
95+
category = 'lecturer';
96+
} else if (headerText.includes('emeritus')) {
97+
category = 'emeritus';
98+
} else if (headerText.includes('graduate student')) {
99+
category = 'gradstudent';
100+
} else if (headerText.includes('staff')) {
101+
category = 'staff';
102+
}
103+
break;
104+
}
105+
currentElement = currentElement.previousElementSibling;
106+
}
107+
108+
// Store data in attributes - include ALL fields especially UVA ID
109+
row.dataset.personName = fullName.toLowerCase();
110+
row.dataset.personCategory = category;
111+
row.dataset.searchData = `${uvaId} ${fullName} ${position} ${specialty} ${office} ${email} ${phone} ${officeHours} ${researchTags}`.toLowerCase();
112+
});
113+
});
114+
}
115+
116+
// Filter function
117+
function filterPeople() {
118+
const searchTerm = searchInput.value.toLowerCase();
119+
const originalSearchTerm = searchInput.value;
120+
let visibleCount = 0;
121+
122+
// Remove previous highlighting
123+
removeHighlighting();
124+
125+
// Track which sections have visible items
126+
const sectionVisibility = {};
127+
128+
peopleContainers.forEach(container => {
129+
const rows = container.querySelectorAll('.row');
130+
let sectionHasVisibleItems = false;
131+
132+
rows.forEach(row => {
133+
if (!row.dataset.personName) return; // Skip rows without person data
134+
135+
const category = row.dataset.personCategory || 'other';
136+
137+
let matchesSearch = false;
138+
if (searchTerm === '') {
139+
matchesSearch = true;
140+
} else {
141+
const searchData = row.dataset.searchData;
142+
143+
// Smart case matching
144+
if (originalSearchTerm !== originalSearchTerm.toLowerCase()) {
145+
// Contains uppercase letters - case sensitive search
146+
matchesSearch = row.textContent.includes(originalSearchTerm);
147+
} else {
148+
// All lowercase - case insensitive search
149+
matchesSearch = searchData.includes(searchTerm);
150+
}
151+
}
152+
153+
const matchesCategory = activeCategory === 'all' || category === activeCategory;
154+
155+
if (matchesSearch && matchesCategory) {
156+
row.style.display = '';
157+
visibleCount++;
158+
sectionHasVisibleItems = true;
159+
160+
// Highlight matching text in visible items
161+
if (searchTerm !== '') {
162+
highlightText(row, originalSearchTerm);
163+
}
164+
} else {
165+
row.style.display = 'none';
166+
}
167+
});
168+
169+
// Hide/show section header based on visibility
170+
const sectionHeader = container.previousElementSibling;
171+
if (sectionHeader && sectionHeader.tagName === 'H2') {
172+
if (sectionHasVisibleItems) {
173+
sectionHeader.style.display = '';
174+
} else {
175+
sectionHeader.style.display = 'none';
176+
}
177+
}
178+
});
179+
180+
// Update no results message if needed
181+
updateNoResultsMessage(visibleCount);
182+
}
183+
184+
// Update no results message
185+
function updateNoResultsMessage(count) {
186+
let noResultsMsg = document.getElementById('no-results-message');
187+
188+
if (count === 0 && searchInput.value !== '') {
189+
if (!noResultsMsg) {
190+
noResultsMsg = document.createElement('div');
191+
noResultsMsg.id = 'no-results-message';
192+
noResultsMsg.className = 'alert alert-info mt-4';
193+
noResultsMsg.textContent = 'No people found. Try adjusting your search or filters.';
194+
195+
// Insert after search area
196+
const searchGroup = document.getElementById('people-search-group');
197+
if (searchGroup && searchGroup.parentElement) {
198+
searchGroup.parentElement.appendChild(noResultsMsg);
199+
}
200+
}
201+
} else if (noResultsMsg) {
202+
noResultsMsg.remove();
203+
}
204+
}
205+
206+
// Clear search
207+
function clearSearch() {
208+
searchInput.value = '';
209+
activeCategory = 'all';
210+
updateCategoryButtons();
211+
removeHighlighting();
212+
filterPeople();
213+
searchInput.focus();
214+
}
215+
216+
// Update category button states
217+
function updateCategoryButtons() {
218+
const buttons = categoryButtons?.querySelectorAll('.category-btn') || [];
219+
buttons.forEach(btn => {
220+
if (btn.dataset.category === activeCategory) {
221+
btn.classList.add('active');
222+
btn.style.backgroundColor = '#002F6C';
223+
btn.style.borderColor = '#002F6C';
224+
btn.style.color = 'white';
225+
} else {
226+
btn.classList.remove('active');
227+
btn.style.backgroundColor = '';
228+
btn.style.borderColor = '';
229+
btn.style.color = '';
230+
}
231+
});
232+
}
233+
234+
235+
// Event listeners
236+
if (searchInput) {
237+
searchInput.addEventListener('input', filterPeople);
238+
searchInput.focus();
239+
}
240+
241+
if (searchClear) {
242+
searchClear.addEventListener('click', clearSearch);
243+
}
244+
245+
246+
// ESC key to clear
247+
document.addEventListener('keydown', function(e) {
248+
if (e.key === 'Escape' && searchInput) {
249+
clearSearch();
250+
}
251+
});
252+
253+
// Category button clicks
254+
if (categoryButtons) {
255+
categoryButtons.addEventListener('click', function(e) {
256+
if (e.target.classList.contains('category-btn')) {
257+
activeCategory = e.target.dataset.category;
258+
updateCategoryButtons();
259+
filterPeople();
260+
}
261+
});
262+
}
263+
264+
// Initialize categories from data
265+
function initializeCategories() {
266+
if (!categoryButtons) return;
267+
268+
const categories = new Set(['all']);
269+
270+
peopleContainers.forEach(container => {
271+
const rows = container.querySelectorAll('.row');
272+
rows.forEach(row => {
273+
const category = row.dataset.personCategory;
274+
if (category) categories.add(category);
275+
});
276+
});
277+
278+
// Clear existing buttons
279+
categoryButtons.innerHTML = '';
280+
281+
// Create category buttons with proper labels
282+
const categoryLabels = {
283+
'all': 'All',
284+
'faculty': 'Faculty',
285+
'postdoc': 'Postdocs',
286+
'lecturer': 'Lecturers',
287+
'emeritus': 'Emeritus',
288+
'gradstudent': 'Grad Students',
289+
'staff': 'Staff'
290+
};
291+
292+
categories.forEach(category => {
293+
const btn = document.createElement('button');
294+
btn.className = 'btn btn-secondary category-btn' + (category === 'all' ? ' active' : '');
295+
btn.dataset.category = category;
296+
btn.textContent = categoryLabels[category] || category.charAt(0).toUpperCase() + category.slice(1);
297+
btn.style.fontSize = '0.9em';
298+
btn.style.marginRight = '0.5em';
299+
btn.style.marginBottom = '0.5em';
300+
301+
categoryButtons.appendChild(btn);
302+
});
303+
}
304+
305+
// Initialize
306+
initializePeopleData();
307+
initializeCategories();
308+
filterPeople();
309+
});

people/all-people.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: All People
3+
layout: static_page_no_right_menu
4+
permalink: /people/
5+
nav_id: All People
6+
nav_weight: 5
7+
nav_nesting: true
8+
nav_parent: People
9+
---
10+
11+
<h1 class="mb-4">All People</h1>
12+
13+
<!-- Search interface with category buttons -->
14+
{% include people_roll_with_search.html show_categories=true %}
15+
16+
<h2 class="mb-4 mt-5">Faculty</h2>
17+
18+
{% include people_roll.html type='faculty' %}
19+
20+
<h2 class="mb-4 mt-5">Postdoctoral Scholars</h2>
21+
22+
{% include people_roll.html type='postdoc' %}
23+
24+
<h2 class="mb-4 mt-5">Graduate Students</h2>
25+
26+
{% include people_roll.html type='gradstudent' %}
27+
28+
<h2 class="mb-4 mt-5">Lecturers</h2>
29+
30+
{% include people_roll.html type='lecturer' %}
31+
32+
<h2 class="mb-4 mt-5">Staff</h2>
33+
34+
{% include people_roll.html type='staff' %}
35+
36+
<h2 class="mb-4 mt-5">Emeritus Faculty</h2>
37+
38+
{% include people_roll.html type='emeritus' %}

people/directory.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
<h1 class="mb-4">Alphabetic list of all members of the Department</h1>
1111

12+
<!-- Search Bar -->
13+
<div id="people-search-group" class="input-group mb-4">
14+
<input type="text" id="people-search-input" class="form-control" placeholder="Search people… (Esc to clear)" aria-label="Search people">
15+
<button class="btn btn-secondary" type="button" id="people-search-clear">Clear</button>
16+
</div>
17+
1218
{% assign sorted_people = site.departmentpeople | sort: 'lastname' %}
1319

1420
<div class="my-row-zebra">
@@ -68,3 +74,6 @@ <h1 class="mb-4">Alphabetic list of all members of the Department</h1>
6874
</div>
6975
{% endfor %}
7076
</div>
77+
78+
<!-- Include search script -->
79+
<script src="{{ site.url }}/css/people-search.js"></script>

people/emeritus-faculty.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010

1111
<h1 class="mb-4">Emeritus Faculty</h1>
1212

13-
{% include people_roll.html type='emeritus' %}
13+
<!-- Search interface for emeritus faculty -->
14+
{% include people_roll_with_search.html type='emeritus' %}

0 commit comments

Comments
 (0)