@@ -32,6 +32,69 @@ class EventBus {
3232 }
3333}
3434
35+ class SearchService {
36+ constructor ( eventBus , indexService ) {
37+ this . eventBus = eventBus ;
38+ this . indexService = indexService ;
39+ this . searchIndex = [ ] ;
40+ }
41+
42+ buildSearchIndex ( documents ) {
43+ this . searchIndex = [ ] ;
44+ this . processDocuments ( documents ) ;
45+ }
46+
47+ processDocuments ( documents , parentPath = '' ) {
48+ documents . forEach ( doc => {
49+ if ( doc . type === 'folder' ) {
50+ const currentPath = parentPath ? `${ parentPath } / ${ doc . title } ` : doc . title ;
51+ if ( doc . path ) {
52+ this . searchIndex . push ( {
53+ title : doc . title ,
54+ path : doc . path ,
55+ slug : doc . slug ,
56+ location : currentPath ,
57+ type : 'folder'
58+ } ) ;
59+ }
60+ if ( doc . items ) {
61+ this . processDocuments ( doc . items , currentPath ) ;
62+ }
63+ } else {
64+ this . searchIndex . push ( {
65+ title : doc . title ,
66+ path : doc . path ,
67+ slug : doc . slug ,
68+ location : parentPath ,
69+ type : 'file'
70+ } ) ;
71+ }
72+ } ) ;
73+ }
74+
75+ search ( query ) {
76+ if ( ! query ) return [ ] ;
77+ query = query . toLowerCase ( ) ;
78+
79+ return this . searchIndex
80+ . filter ( item => {
81+ const titleMatch = item . title . toLowerCase ( ) . includes ( query ) ;
82+ const pathMatch = item . path . toLowerCase ( ) . includes ( query ) ;
83+ const locationMatch = item . location . toLowerCase ( ) . includes ( query ) ;
84+ return titleMatch || pathMatch || locationMatch ;
85+ } )
86+ . sort ( ( a , b ) => {
87+ // Prioritize exact matches in title
88+ const aTitle = a . title . toLowerCase ( ) ;
89+ const bTitle = b . title . toLowerCase ( ) ;
90+ if ( aTitle === query ) return - 1 ;
91+ if ( bTitle === query ) return 1 ;
92+ return aTitle . localeCompare ( bTitle ) ;
93+ } )
94+ . slice ( 0 , 10 ) ; // Limit to 10 results
95+ }
96+ }
97+
3598class IndexService {
3699 findDocumentBySlug ( documents , slug ) {
37100 for ( const doc of documents ) {
@@ -93,7 +156,10 @@ class DOMService {
93156 titleText : document . querySelector ( '.title-text .page-title' ) ,
94157 leftSidebar : document . querySelector ( '.left-sidebar' ) ,
95158 menuButton : document . querySelector ( '.menu-button' ) ,
96- header : document . querySelector ( 'title-bar' ) // Add header element
159+ header : document . querySelector ( 'title-bar' ) , // Add header element
160+ searchInput : document . getElementById ( 'search-input' ) ,
161+ searchResults : document . getElementById ( 'search-results' ) ,
162+ clearSearch : document . getElementById ( 'clear-search' )
97163 } ;
98164 this . headerOffset = 60 ; // Fixed header heightfsetHeight || 0; // Get header height
99165 }
@@ -301,6 +367,67 @@ class DOMService {
301367
302368 heading . appendChild ( toggleBtn ) ;
303369 }
370+
371+ setupSearch ( searchService ) {
372+ let searchTimeout ;
373+
374+ this . elements . searchInput . addEventListener ( 'input' , ( e ) => {
375+ clearTimeout ( searchTimeout ) ;
376+ const query = e . target . value ;
377+
378+ searchTimeout = setTimeout ( ( ) => {
379+ const results = searchService . search ( query ) ;
380+ this . renderSearchResults ( results ) ;
381+ } , 200 ) ;
382+ } ) ;
383+
384+ this . elements . clearSearch . addEventListener ( 'click' , ( ) => {
385+ this . elements . searchInput . value = '' ;
386+ this . elements . searchResults . innerHTML = '' ;
387+ this . elements . searchResults . style . display = 'none' ;
388+ } ) ;
389+ }
390+
391+ renderSearchResults ( results ) {
392+ const container = this . elements . searchResults ;
393+ container . innerHTML = '' ;
394+
395+ if ( results . length === 0 || ! this . elements . searchInput . value ) {
396+ container . style . display = 'none' ;
397+ return ;
398+ }
399+
400+ results . forEach ( result => {
401+ const div = document . createElement ( 'div' ) ;
402+ div . className = 'search-result' ;
403+
404+ const icon = document . createElement ( 'i' ) ;
405+ icon . className = result . type === 'folder' ? 'fas fa-folder' : 'fas fa-file-alt' ;
406+
407+ const link = document . createElement ( 'a' ) ;
408+ link . href = `?${ result . slug } ` ;
409+ link . innerHTML = `
410+ ${ icon . outerHTML }
411+ <div class="search-result-content">
412+ <div class="search-result-title">${ result . title } </div>
413+ <div class="search-result-path">${ result . location } </div>
414+ </div>
415+ ` ;
416+
417+ link . addEventListener ( 'click' , ( e ) => {
418+ e . preventDefault ( ) ;
419+ this . elements . searchInput . value = '' ;
420+ container . style . display = 'none' ;
421+ history . pushState ( null , '' , link . href ) ;
422+ this . eventBus . emit ( 'navigation:requested' , { slug : result . slug } ) ;
423+ } ) ;
424+
425+ div . appendChild ( link ) ;
426+ container . appendChild ( div ) ;
427+ } ) ;
428+
429+ container . style . display = 'block' ;
430+ }
304431}
305432
306433class DocumentService {
@@ -511,6 +638,7 @@ class Documentation {
511638 constructor ( ) {
512639 this . eventBus = new EventBus ( ) ;
513640 this . indexService = new IndexService ( ) ;
641+ this . searchService = new SearchService ( this . eventBus , this . indexService ) ;
514642 this . domService = new DOMService ( this . eventBus ) ;
515643 this . documentService = new DocumentService ( this . eventBus , this . indexService ) ;
516644 this . navigationService = new NavigationService ( this . eventBus , this . documentService ) ;
@@ -553,6 +681,9 @@ class Documentation {
553681 this . indexData = data ;
554682 window . _indexData = data ;
555683
684+ this . searchService . buildSearchIndex ( this . indexData . documents ) ;
685+ this . domService . setupSearch ( this . searchService ) ;
686+
556687 this . populateAuthorInfo ( data . author ) ;
557688 window . originalDocTitle = data . metadata . site_name || 'Documentation' ;
558689 document . title = window . originalDocTitle ;
0 commit comments