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 ( / \/ p e o p l e \/ ( [ ^ \/ ] + ) \/ / ) ;
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
+ } ) ;
0 commit comments