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' ;
8+ import {svg } from ' ../svg.ts' ;
89
910const elRoot = useTemplateRef (' elRoot' );
11+ const searchResults = useTemplateRef (' searchResults' );
1012const searchQuery = ref (' ' );
1113const allFiles = ref <string []>([]);
1214const selectedIndex = ref (0 );
@@ -39,9 +41,11 @@ const handleKeyDown = (e: KeyboardEvent) => {
3941 if (e .key === ' ArrowDown' ) {
4042 e .preventDefault ();
4143 selectedIndex .value = Math .min (selectedIndex .value + 1 , filteredFiles .value .length - 1 );
44+ scrollSelectedIntoView ();
4245 } else if (e .key === ' ArrowUp' ) {
4346 e .preventDefault ();
4447 selectedIndex .value = Math .max (selectedIndex .value - 1 , 0 );
48+ scrollSelectedIntoView ();
4549 } else if (e .key === ' Enter' ) {
4650 e .preventDefault ();
4751 const selectedFile = filteredFiles .value [selectedIndex .value ];
@@ -54,6 +58,32 @@ const handleKeyDown = (e: KeyboardEvent) => {
5458 }
5559};
5660
61+ const scrollSelectedIntoView = () => {
62+ nextTick (() => {
63+ const resultsEl = searchResults .value ;
64+ if (! resultsEl ) return ;
65+
66+ const selectedEl = resultsEl .querySelector (' .file-tree-search-result-item.selected' );
67+ if (selectedEl ) {
68+ selectedEl .scrollIntoView ({block: ' nearest' , behavior: ' smooth' });
69+ }
70+ });
71+ };
72+
73+ const handleClickOutside = (e : MouseEvent ) => {
74+ if (! searchQuery .value ) return ;
75+
76+ const target = e .target as HTMLElement ;
77+ const resultsEl = searchResults .value ;
78+
79+ // Check if click is outside search input and results
80+ if (searchInputElement && ! searchInputElement .contains (target ) &&
81+ resultsEl && ! resultsEl .contains (target )) {
82+ searchQuery .value = ' ' ;
83+ if (searchInputElement ) searchInputElement .value = ' ' ;
84+ }
85+ };
86+
5787onMounted (async () => {
5888 store .rootFiles = await store .loadChildren (' ' , props .treePath );
5989 elRoot .value .closest (' .is-loading' )?.classList ?.remove (' is-loading' );
@@ -72,17 +102,34 @@ onMounted(async () => {
72102 searchInputElement .addEventListener (' keydown' , handleKeyDown );
73103 }
74104
105+ // Add click outside listener
106+ document .addEventListener (' click' , handleClickOutside );
107+
75108 window .addEventListener (' popstate' , (e ) => {
76109 store .selectedItem = e .state ?.treePath || ' ' ;
77110 if (e .state ?.url ) store .loadViewContent (e .state .url );
78111 });
79112});
80113
114+ // Position search results below the input
115+ watch (searchQuery , async () => {
116+ if (searchQuery .value && searchInputElement ) {
117+ await nextTick ();
118+ const resultsEl = searchResults .value ;
119+ if (resultsEl ) {
120+ const rect = searchInputElement .getBoundingClientRect ();
121+ resultsEl .style .top = ` ${rect .bottom + 4 }px ` ;
122+ resultsEl .style .left = ` ${rect .left }px ` ;
123+ }
124+ }
125+ });
126+
81127onUnmounted (() => {
82128 if (searchInputElement ) {
83129 searchInputElement .removeEventListener (' input' , handleSearchInput );
84130 searchInputElement .removeEventListener (' keydown' , handleKeyDown );
85131 }
132+ document .removeEventListener (' click' , handleClickOutside );
86133});
87134
88135function handleSearchResultClick(filePath : string ) {
@@ -93,35 +140,42 @@ function handleSearchResultClick(filePath: string) {
93140 </script >
94141
95142<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 >
143+ <div ref =" elRoot" class =" file-tree-root" >
144+ <Teleport to =" body" >
145+ <div v-if =" searchQuery && filteredFiles.length > 0" ref =" searchResults" class =" file-tree-search-results" >
146+ <div
147+ v-for =" (result, idx) in filteredFiles"
148+ :key =" result.matchResult.join('')"
149+ :class =" ['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
150+ @click =" handleSearchResultClick(result.matchResult.join(''))"
151+ @mouseenter =" selectedIndex = idx"
152+ :title =" result.matchResult.join('')"
153+ >
154+ <span v-html =" svg('octicon-file', 16)" ></span >
155+ <span class =" file-tree-search-result-path" >
156+ <span
157+ v-for =" (part, index) in result.matchResult"
158+ :key =" index"
159+ :class =" {'search-match': index % 2 === 1}"
160+ >{{ part }}</span >
161+ </span >
162+ </div >
113163 </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" >
164+ < div v-if = " searchQuery && filteredFiles.length === 0 " class = " file-tree-search-no-results " >
165+ No matching file found
166+ </ div >
167+ </Teleport >
168+ <div class =" view-file-tree-items" >
119169 <ViewFileTreeItem v-for =" item in store.rootFiles" :key =" item.name" :item =" item" :store =" store" />
120170 </div >
121171 </div >
122172</template >
123173
124174<style scoped>
175+ .file-tree-root {
176+ position : relative ;
177+ }
178+
125179.view-file-tree-items {
126180 display : flex ;
127181 flex-direction : column ;
@@ -130,27 +184,36 @@ function handleSearchResultClick(filePath: string) {
130184}
131185
132186.file-tree-search-results {
187+ position : fixed ;
133188 display : flex ;
134189 flex-direction : column ;
135- margin : 0 0.5rem 0.5rem ;
136190 max-height : 400px ;
137191 overflow-y : auto ;
138192 background : var (--color-box-body );
139193 border : 1px solid var (--color-secondary );
140194 border-radius : 6px ;
141195 box-shadow : 0 8px 24px rgba (0 , 0 , 0 , 0.12 );
196+ min-width : 300px ;
197+ width : max-content ;
198+ max-width : 600px ;
199+ z-index : 99999 ;
142200}
143201
144202.file-tree-search-result-item {
145203 display : flex ;
146- align-items : center ;
204+ align-items : flex-start ;
147205 gap : 0.5rem ;
148206 padding : 0.5rem 0.75rem ;
149207 cursor : pointer ;
150208 transition : background-color 0.1s ;
151209 border-bottom : 1px solid var (--color-secondary );
152210}
153211
212+ .file-tree-search-result-item svg {
213+ flex-shrink : 0 ;
214+ margin-top : 0.125rem ;
215+ }
216+
154217.file-tree-search-result-item :last-child {
155218 border-bottom : none ;
156219}
@@ -162,10 +225,9 @@ function handleSearchResultClick(filePath: string) {
162225
163226.file-tree-search-result-path {
164227 flex : 1 ;
165- overflow : hidden ;
166- text-overflow : ellipsis ;
167- white-space : nowrap ;
168228 font-size : 14px ;
229+ word-break : break-all ;
230+ overflow-wrap : break-word ;
169231}
170232
171233.search-match {
0 commit comments