11<script lang="ts" setup>
22import ViewFileTreeItem from ' ./ViewFileTreeItem.vue' ;
3- import {onMounted , onUnmounted , useTemplateRef , ref , computed } from ' vue' ;
3+ import {onMounted , onUnmounted , useTemplateRef , ref , computed , watch , nextTick } from ' vue' ;
44import {createViewFileTreeStore } from ' ./ViewFileTreeStore.ts' ;
55import {GET } from ' ../modules/fetch.ts' ;
66import {filterRepoFilesWeighted } from ' ../features/repo-findfile.ts' ;
77import {pathEscapeSegments } from ' ../utils/url.ts' ;
88
99const elRoot = useTemplateRef (' elRoot' );
10+ const searchResults = useTemplateRef (' searchResults' );
1011const searchQuery = ref (' ' );
1112const allFiles = ref <string []>([]);
1213const selectedIndex = ref (0 );
@@ -39,9 +40,11 @@ const handleKeyDown = (e: KeyboardEvent) => {
3940 if (e .key === ' ArrowDown' ) {
4041 e .preventDefault ();
4142 selectedIndex .value = Math .min (selectedIndex .value + 1 , filteredFiles .value .length - 1 );
43+ scrollSelectedIntoView ();
4244 } else if (e .key === ' ArrowUp' ) {
4345 e .preventDefault ();
4446 selectedIndex .value = Math .max (selectedIndex .value - 1 , 0 );
47+ scrollSelectedIntoView ();
4548 } else if (e .key === ' Enter' ) {
4649 e .preventDefault ();
4750 const selectedFile = filteredFiles .value [selectedIndex .value ];
@@ -54,6 +57,32 @@ const handleKeyDown = (e: KeyboardEvent) => {
5457 }
5558};
5659
60+ const scrollSelectedIntoView = () => {
61+ nextTick (() => {
62+ const resultsEl = searchResults .value ;
63+ if (! resultsEl ) return ;
64+
65+ const selectedEl = resultsEl .querySelector (' .file-tree-search-result-item.selected' );
66+ if (selectedEl ) {
67+ selectedEl .scrollIntoView ({block: ' nearest' , behavior: ' smooth' });
68+ }
69+ });
70+ };
71+
72+ const handleClickOutside = (e : MouseEvent ) => {
73+ if (! searchQuery .value ) return ;
74+
75+ const target = e .target as HTMLElement ;
76+ const resultsEl = searchResults .value ;
77+
78+ // Check if click is outside search input and results
79+ if (searchInputElement && ! searchInputElement .contains (target ) &&
80+ resultsEl && ! resultsEl .contains (target )) {
81+ searchQuery .value = ' ' ;
82+ if (searchInputElement ) searchInputElement .value = ' ' ;
83+ }
84+ };
85+
5786onMounted (async () => {
5887 store .rootFiles = await store .loadChildren (' ' , props .treePath );
5988 elRoot .value .closest (' .is-loading' )?.classList ?.remove (' is-loading' );
@@ -72,17 +101,34 @@ onMounted(async () => {
72101 searchInputElement .addEventListener (' keydown' , handleKeyDown );
73102 }
74103
104+ // Add click outside listener
105+ document .addEventListener (' click' , handleClickOutside );
106+
75107 window .addEventListener (' popstate' , (e ) => {
76108 store .selectedItem = e .state ?.treePath || ' ' ;
77109 if (e .state ?.url ) store .loadViewContent (e .state .url );
78110 });
79111});
80112
113+ // Position search results below the input
114+ watch (searchQuery , async () => {
115+ if (searchQuery .value && searchInputElement ) {
116+ await nextTick ();
117+ const resultsEl = searchResults .value ;
118+ if (resultsEl ) {
119+ const rect = searchInputElement .getBoundingClientRect ();
120+ resultsEl .style .top = ` ${rect .bottom + 4 }px ` ;
121+ resultsEl .style .left = ` ${rect .left }px ` ;
122+ }
123+ }
124+ });
125+
81126onUnmounted (() => {
82127 if (searchInputElement ) {
83128 searchInputElement .removeEventListener (' input' , handleSearchInput );
84129 searchInputElement .removeEventListener (' keydown' , handleKeyDown );
85130 }
131+ document .removeEventListener (' click' , handleClickOutside );
86132});
87133
88134function handleSearchResultClick(filePath : string ) {
@@ -93,35 +139,42 @@ function handleSearchResultClick(filePath: string) {
93139 </script >
94140
95141<template >
96- <div ref =" elRoot" >
97- <div v-if =" searchQuery && filteredFiles.length > 0" class =" file-tree-search-results" >
98- <div
99- v-for =" (result, idx) in filteredFiles"
100- :key =" result.matchResult.join('')"
101- :class =" ['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
102- @click =" handleSearchResultClick(result.matchResult.join(''))"
103- @mouseenter =" selectedIndex = idx"
104- >
105- <svg class =" svg octicon-file" width =" 16" height =" 16" aria-hidden =" true" ><use href =" #octicon-file" /></svg >
106- <span class =" file-tree-search-result-path" >
107- <span
108- v-for =" (part, index) in result.matchResult"
109- :key =" index"
110- :class =" {'search-match': index % 2 === 1}"
111- >{{ part }}</span >
112- </span >
142+ <div ref =" elRoot" class =" file-tree-root" >
143+ <Teleport to =" body" >
144+ <div v-if =" searchQuery && filteredFiles.length > 0" ref =" searchResults" class =" file-tree-search-results" >
145+ <div
146+ v-for =" (result, idx) in filteredFiles"
147+ :key =" result.matchResult.join('')"
148+ :class =" ['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
149+ @click =" handleSearchResultClick(result.matchResult.join(''))"
150+ @mouseenter =" selectedIndex = idx"
151+ :title =" result.matchResult.join('')"
152+ >
153+ <svg class =" svg octicon-file" width =" 16" height =" 16" aria-hidden =" true" ><use href =" #octicon-file" /></svg >
154+ <span class =" file-tree-search-result-path" >
155+ <span
156+ v-for =" (part, index) in result.matchResult"
157+ :key =" index"
158+ :class =" {'search-match': index % 2 === 1}"
159+ >{{ part }}</span >
160+ </span >
161+ </div >
113162 </div >
114- </ div >
115- < div v-else-if = " searchQuery && filteredFiles.length === 0 " class = " file-tree-search-no-results " >
116- No matching file found
117- </div >
118- <div v-else class =" view-file-tree-items" >
163+ < div v-if = " searchQuery && filteredFiles.length === 0 " class = " file-tree-search-no-results " >
164+ No matching file found
165+ </ div >
166+ </Teleport >
167+ <div class =" view-file-tree-items" >
119168 <ViewFileTreeItem v-for =" item in store.rootFiles" :key =" item.name" :item =" item" :store =" store" />
120169 </div >
121170 </div >
122171</template >
123172
124173<style scoped>
174+ .file-tree-root {
175+ position : relative ;
176+ }
177+
125178.view-file-tree-items {
126179 display : flex ;
127180 flex-direction : column ;
@@ -130,27 +183,36 @@ function handleSearchResultClick(filePath: string) {
130183}
131184
132185.file-tree-search-results {
186+ position : fixed ;
133187 display : flex ;
134188 flex-direction : column ;
135- margin : 0 0.5rem 0.5rem ;
136189 max-height : 400px ;
137190 overflow-y : auto ;
138191 background : var (--color-box-body );
139192 border : 1px solid var (--color-secondary );
140193 border-radius : 6px ;
141194 box-shadow : 0 8px 24px rgba (0 , 0 , 0 , 0.12 );
195+ min-width : 300px ;
196+ width : max-content ;
197+ max-width : 600px ;
198+ z-index : 99999 ;
142199}
143200
144201.file-tree-search-result-item {
145202 display : flex ;
146- align-items : center ;
203+ align-items : flex-start ;
147204 gap : 0.5rem ;
148205 padding : 0.5rem 0.75rem ;
149206 cursor : pointer ;
150207 transition : background-color 0.1s ;
151208 border-bottom : 1px solid var (--color-secondary );
152209}
153210
211+ .file-tree-search-result-item svg {
212+ flex-shrink : 0 ;
213+ margin-top : 0.125rem ;
214+ }
215+
154216.file-tree-search-result-item :last-child {
155217 border-bottom : none ;
156218}
@@ -162,10 +224,9 @@ function handleSearchResultClick(filePath: string) {
162224
163225.file-tree-search-result-path {
164226 flex : 1 ;
165- overflow : hidden ;
166- text-overflow : ellipsis ;
167- white-space : nowrap ;
168227 font-size : 14px ;
228+ word-break : break-all ;
229+ overflow-wrap : break-word ;
169230}
170231
171232.search-match {
0 commit comments