6767 }
6868 }
6969
70+ function isTypingElement ( element ) {
71+ if ( ! element ) return false ;
72+ const tagName = element . tagName ? element . tagName . toLowerCase ( ) : "" ;
73+ return (
74+ [ "input" , "textarea" , "select" ] . includes ( tagName ) ||
75+ element . isContentEditable
76+ ) ;
77+ }
78+
79+ function initSearchInteractions ( ) {
80+ const searchContainer = document . getElementById ( 'resource-search-container' ) ;
81+ const searchInput = document . getElementById ( 'file-search-input' ) ;
82+ const searchResults = document . getElementById ( 'search-results' ) ;
83+ const clearSearchBtn = document . getElementById ( 'clear-search' ) ;
84+ if ( ! searchContainer || ! searchInput || ! searchResults || ! clearSearchBtn ) return ;
85+
86+ let activeIndex = - 1 ;
87+
88+ if ( searchResults . parentNode !== document . body ) {
89+ searchResults . classList . add ( 'search-dropdown-portal' ) ;
90+ document . body . appendChild ( searchResults ) ;
91+ }
92+
93+ function getResultItems ( ) {
94+ return Array . from ( searchResults . querySelectorAll ( '.search-result-item' ) ) ;
95+ }
96+
97+ function updateDropdownPosition ( ) {
98+ if ( searchResults . classList . contains ( 'is-hidden' ) ) return ;
99+
100+ const rect = searchContainer . getBoundingClientRect ( ) ;
101+ const viewportPadding = 8 ;
102+ const baseWidth = Math . max ( rect . width + 300 , window . innerWidth * 0.5 ) ;
103+ const width = Math . min ( baseWidth , window . innerWidth - viewportPadding * 2 ) ;
104+ const left = Math . max (
105+ viewportPadding ,
106+ Math . min ( rect . left , window . innerWidth - width - viewportPadding )
107+ ) ;
108+ const dropdownTop = rect . bottom + 4 ;
109+ const availableHeight = window . innerHeight - dropdownTop - viewportPadding ;
110+ const maxHeight = Math . max ( 180 , Math . min ( window . innerHeight * 0.62 , availableHeight ) ) ;
111+
112+ searchResults . style . left = `${ left } px` ;
113+ searchResults . style . top = `${ dropdownTop } px` ;
114+ searchResults . style . width = `${ width } px` ;
115+ searchResults . style . maxHeight = `${ maxHeight } px` ;
116+ }
117+
118+ function showDropdown ( ) {
119+ if ( searchInput . value . trim ( ) ) {
120+ searchResults . classList . remove ( 'is-hidden' ) ;
121+ updateDropdownPosition ( ) ;
122+ }
123+ }
124+
125+ function hideDropdown ( ) {
126+ searchResults . classList . add ( 'is-hidden' ) ;
127+ setActiveItem ( - 1 ) ;
128+ }
129+
130+ function updateClearButtonVisibility ( ) {
131+ clearSearchBtn . classList . toggle ( 'is-hidden' , ! searchInput . value . trim ( ) ) ;
132+ }
133+
134+ function setActiveItem ( nextIndex ) {
135+ const items = getResultItems ( ) ;
136+ if ( ! items . length ) {
137+ activeIndex = - 1 ;
138+ return ;
139+ }
140+
141+ items . forEach ( item => item . classList . remove ( 'is-active' ) ) ;
142+ if ( nextIndex < 0 ) {
143+ activeIndex = - 1 ;
144+ return ;
145+ }
146+
147+ activeIndex = ( ( nextIndex % items . length ) + items . length ) % items . length ;
148+ const activeItem = items [ activeIndex ] ;
149+ activeItem . classList . add ( 'is-active' ) ;
150+ activeItem . scrollIntoView ( { block : 'nearest' } ) ;
151+ }
152+
153+ function triggerActiveItem ( ) {
154+ const items = getResultItems ( ) ;
155+ if ( ! items . length ) return ;
156+
157+ const index = activeIndex >= 0 ? activeIndex : 0 ;
158+ const activeItem = items [ index ] ;
159+ activeItem . click ( ) ;
160+ }
161+
162+ function clearSearch ( ) {
163+ searchInput . value = '' ;
164+ updateClearButtonVisibility ( ) ;
165+ hideDropdown ( ) ;
166+ searchResults . innerHTML = '' ;
167+ searchInput . focus ( ) ;
168+ }
169+
170+ clearSearchBtn . addEventListener ( 'click' , clearSearch ) ;
171+
172+ searchInput . addEventListener ( 'input' , function ( ) {
173+ activeIndex = - 1 ;
174+ updateClearButtonVisibility ( ) ;
175+ if ( ! searchInput . value . trim ( ) ) {
176+ hideDropdown ( ) ;
177+ } else {
178+ updateDropdownPosition ( ) ;
179+ }
180+ } ) ;
181+
182+ searchInput . addEventListener ( 'focus' , showDropdown ) ;
183+
184+ window . addEventListener ( 'resize' , updateDropdownPosition ) ;
185+ window . addEventListener ( 'scroll' , updateDropdownPosition , true ) ;
186+
187+ searchInput . addEventListener ( 'keydown' , function ( event ) {
188+ if ( event . key === 'Escape' ) {
189+ hideDropdown ( ) ;
190+ searchInput . blur ( ) ;
191+ return ;
192+ }
193+
194+ if ( event . key === 'ArrowDown' ) {
195+ event . preventDefault ( ) ;
196+ const items = getResultItems ( ) ;
197+ if ( ! items . length ) return ;
198+ showDropdown ( ) ;
199+ const nextIndex = activeIndex < 0 ? 0 : activeIndex + 1 ;
200+ setActiveItem ( nextIndex ) ;
201+ return ;
202+ }
203+
204+ if ( event . key === 'ArrowUp' ) {
205+ event . preventDefault ( ) ;
206+ const items = getResultItems ( ) ;
207+ if ( ! items . length ) return ;
208+ showDropdown ( ) ;
209+ const nextIndex = activeIndex < 0 ? items . length - 1 : activeIndex - 1 ;
210+ setActiveItem ( nextIndex ) ;
211+ return ;
212+ }
213+
214+ if ( event . key === 'Enter' ) {
215+ const items = getResultItems ( ) ;
216+ if ( ! items . length || searchResults . classList . contains ( 'is-hidden' ) ) return ;
217+ event . preventDefault ( ) ;
218+ triggerActiveItem ( ) ;
219+ }
220+ } ) ;
221+
222+ document . addEventListener ( 'click' , function ( event ) {
223+ const resultItem = event . target . closest ( '.search-result-item' ) ;
224+ if ( resultItem && searchResults . contains ( resultItem ) ) {
225+ hideDropdown ( ) ;
226+ searchInput . blur ( ) ;
227+ expandToPath ( resultItem . dataset . path ) ;
228+ return ;
229+ }
230+
231+ if ( ! searchContainer . contains ( event . target ) && ! searchResults . contains ( event . target ) ) {
232+ hideDropdown ( ) ;
233+ }
234+ } ) ;
235+
236+ document . addEventListener ( 'keydown' , function ( event ) {
237+ const target = event . target ;
238+ if ( event . key . toLowerCase ( ) === 't' && ! event . metaKey && ! event . ctrlKey && ! event . altKey ) {
239+ if ( isTypingElement ( target ) ) return ;
240+ event . preventDefault ( ) ;
241+ searchInput . focus ( ) ;
242+ }
243+ } ) ;
244+
245+ document . body . addEventListener ( 'htmx:afterSettle' , function ( event ) {
246+ if ( event . target !== searchResults ) return ;
247+ activeIndex = - 1 ;
248+ updateClearButtonVisibility ( ) ;
249+ if ( searchInput . value . trim ( ) ) {
250+ showDropdown ( ) ;
251+ updateDropdownPosition ( ) ;
252+ } else {
253+ hideDropdown ( ) ;
254+ }
255+ } ) ;
256+
257+ updateClearButtonVisibility ( ) ;
258+ }
259+
70260 document . addEventListener ( "click" , async e => {
71261 const node = e . target . closest ( "[data-folder], .is-file[data-file], .expand-in-tree, [data-chevron]" ) ;
72262 if ( ! node ) return ;
142332 } ) ;
143333 } ) ;
144334
335+ initSearchInteractions ( ) ;
145336 } ) ;
146- } ) ( ) ;
337+ } ) ( ) ;
0 commit comments