Skip to content

Commit 7d4a08c

Browse files
committed
enhance: debounced input and search result scroll positioning.
1 parent 6e4312f commit 7d4a08c

File tree

7 files changed

+82
-103
lines changed

7 files changed

+82
-103
lines changed

templates/repo/editor/edit.tmpl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
{{.CsrfTokenHtml}}
1616
{{template "repo/editor/common_top" .}}
1717
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
18-
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
19-
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
20-
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
18+
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
2119
{{svg "octicon-sidebar-collapse"}}
2220
</button>
2321
{{template "repo/editor/common_breadcrumb" .}}

templates/repo/editor/patch.tmpl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
{{.CsrfTokenHtml}}
1313
{{template "repo/editor/common_top" .}}
1414
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
15-
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
16-
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
17-
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
15+
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
1816
{{svg "octicon-sidebar-collapse"}}
1917
</button>
2018
<div class="breadcrumb">
@@ -30,9 +28,7 @@
3028
<a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a>
3129
</div>
3230
<div class="ui active tab segment tw-rounded tw-p-0" data-tab="write">
33-
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch"
34-
data-context="{{.RepoLink}}"
35-
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
31+
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch" data-context="{{.RepoLink}}" data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
3632
<div class="editor-loading is-loading"></div>
3733
</div>
3834
</div>

templates/repo/editor/upload.tmpl

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,18 @@
99
</div>
1010
<div class="repo-view-content">
1111
<form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
12-
{{.CsrfTokenHtml}}
13-
{{template "repo/editor/common_top" .}}
14-
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
15-
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
16-
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
17-
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
18-
{{svg "octicon-sidebar-collapse"}}
19-
</button>
20-
{{template "repo/editor/common_breadcrumb" .}}
21-
</div>
22-
<div class="field">
23-
{{template "repo/upload" .}}
24-
</div>
25-
{{template "repo/editor/commit_form" .}}
12+
{{.CsrfTokenHtml}}
13+
{{template "repo/editor/common_top" .}}
14+
<div class="repo-editor-header tw-flex tw-items-center tw-gap-2">
15+
<button type="button" class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
16+
{{svg "octicon-sidebar-collapse"}}
17+
</button>
18+
{{template "repo/editor/common_breadcrumb" .}}
19+
</div>
20+
<div class="field">
21+
{{template "repo/upload" .}}
22+
</div>
23+
{{template "repo/editor/commit_form" .}}
2624
</form>
2725
</div>
2826
</div>

templates/repo/view_content.tmpl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
<div class="repo-button-row">
66
<div class="repo-button-row-left">
77
{{if not $isTreePathRoot}}
8-
<button class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}"
9-
data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show"
10-
data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
8+
<button class="repo-view-file-tree-toggle-show ui compact basic button icon not-mobile {{if .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" data-global-click="onRepoViewFileTreeToggle" data-toggle-action="show" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}">
119
{{svg "octicon-sidebar-collapse"}}
1210
</button>
1311
{{end}}

web_src/js/components/RepoFileSearch.vue

Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script lang="ts" setup>
2-
import {ref, computed, watch, nextTick, useTemplateRef, onMounted, onUnmounted} from 'vue';
3-
import {GET} from '../modules/fetch.ts';
4-
import {filterRepoFilesWeighted} from '../features/repo-findfile.ts';
5-
import {pathEscapeSegments} from '../utils/url.ts';
6-
import {svg} from '../svg.ts';
2+
import { ref, computed, watch, nextTick, useTemplateRef, onMounted, onUnmounted } from 'vue';
3+
import { onInputDebounce } from '../utils/dom.ts';
4+
import { GET } from '../modules/fetch.ts';
5+
import { filterRepoFilesWeighted } from '../features/repo-findfile.ts';
6+
import { pathEscapeSegments } from '../utils/url.ts';
7+
import { svg } from '../svg.ts';
78
89
const searchInput = useTemplateRef('searchInput');
910
const searchResults = useTemplateRef('searchResults');
@@ -14,11 +15,11 @@ const isLoadingFileList = ref(false);
1415
const hasLoadedFileList = ref(false);
1516
1617
const props = defineProps({
17-
repoLink: {type: String, required: true},
18-
currentRefNameSubURL: {type: String, required: true},
19-
treeListUrl: {type: String, required: true},
20-
noResultsText: {type: String, required: true},
21-
placeholder: {type: String, required: true},
18+
repoLink: { type: String, required: true },
19+
currentRefNameSubURL: { type: String, required: true },
20+
treeListUrl: { type: String, required: true },
21+
noResultsText: { type: String, required: true },
22+
placeholder: { type: String, required: true },
2223
});
2324
2425
const filteredFiles = computed(() => {
@@ -28,10 +29,12 @@ const filteredFiles = computed(() => {
2829
2930
const treeLink = computed(() => `${props.repoLink}/src/${pathEscapeSegments(props.currentRefNameSubURL)}`);
3031
31-
const handleSearchInput = (e: Event) => {
32-
searchQuery.value = (e.target as HTMLInputElement).value;
33-
selectedIndex.value = 0;
34-
};
32+
const handleSearchInput = onInputDebounce(() => {
33+
if (searchInput.value) {
34+
searchQuery.value = searchInput.value.value;
35+
selectedIndex.value = 0;
36+
}
37+
});
3538
3639
const handleKeyDown = (e: KeyboardEvent) => {
3740
if (e.key === 'Escape' && searchQuery.value) {
@@ -68,30 +71,30 @@ const scrollSelectedIntoView = () => {
6871
nextTick(() => {
6972
const resultsEl = searchResults.value;
7073
if (!resultsEl) return;
71-
74+
7275
const selectedEl = resultsEl.querySelector('.file-tree-search-result-item.selected');
7376
if (selectedEl) {
74-
selectedEl.scrollIntoView({block: 'nearest', behavior: 'smooth'});
77+
selectedEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
7578
}
7679
});
7780
};
7881
7982
const handleClickOutside = (e: MouseEvent) => {
8083
if (!searchQuery.value) return;
81-
84+
8285
const target = e.target as HTMLElement;
8386
const resultsEl = searchResults.value;
8487
const inputEl = searchInput.value;
85-
86-
if (inputEl && !inputEl.contains(target) &&
87-
resultsEl && !resultsEl.contains(target)) {
88+
89+
if (inputEl && !inputEl.contains(target) &&
90+
resultsEl && !resultsEl.contains(target)) {
8891
clearSearch();
8992
}
9093
};
9194
9295
const loadFileListForSearch = async () => {
9396
if (hasLoadedFileList.value || isLoadingFileList.value) return;
94-
97+
9598
isLoadingFileList.value = true;
9699
try {
97100
const response = await GET(props.treeListUrl);
@@ -111,64 +114,70 @@ function handleSearchResultClick(filePath: string) {
111114
window.location.href = `${treeLink.value}/${pathEscapeSegments(filePath)}`;
112115
}
113116
117+
const updatePosition = () => {
118+
if (searchInput.value && searchResults.value) {
119+
const rect = searchInput.value.getBoundingClientRect();
120+
searchResults.value.style.top = `${rect.bottom + 4}px`;
121+
searchResults.value.style.left = `${rect.left}px`;
122+
searchResults.value.style.visibility = 'visible';
123+
}
124+
};
125+
114126
onMounted(() => {
115127
document.addEventListener('click', handleClickOutside);
128+
window.addEventListener('scroll', updatePosition, { passive: true });
116129
});
117130
118131
onUnmounted(() => {
119132
document.removeEventListener('click', handleClickOutside);
133+
window.removeEventListener('scroll', updatePosition);
120134
});
121135
122136
// Position search results below the input
123-
watch(searchQuery, async () => {
124-
if (searchQuery.value && searchInput.value) {
137+
watch([searchQuery, filteredFiles], async () => {
138+
if (searchQuery.value) {
125139
await nextTick();
126-
const resultsEl = searchResults.value;
127-
if (resultsEl) {
128-
const rect = searchInput.value.getBoundingClientRect();
129-
resultsEl.style.top = `${rect.bottom + 4}px`;
130-
resultsEl.style.left = `${rect.left}px`;
131-
}
140+
updatePosition();
132141
}
133142
});
134143
</script>
135144

136145
<template>
137146
<div class="repo-file-search">
138-
<div class="ui small input tw-w-full tw-px-2 tw-pb-2">
139-
<input
140-
ref="searchInput"
141-
type="text"
142-
:placeholder="placeholder"
143-
autocomplete="off"
144-
@input="handleSearchInput"
145-
@keydown="handleKeyDown"
146-
@focus="handleSearchFocus"
147+
<div class="ui small input tw-w-full tw-px-2 tw-pb-2 tw-items-center">
148+
<input
149+
ref="searchInput" type="text" :placeholder="placeholder" autocomplete="off" role="combobox"
150+
aria-autocomplete="list" :aria-expanded="searchQuery ? 'true' : 'false'" aria-controls="file-search-results"
151+
@input="handleSearchInput" @keydown="handleKeyDown" @focus="handleSearchFocus"
147152
>
148153
</div>
149-
154+
150155
<Teleport to="body">
151-
<div v-if="searchQuery && filteredFiles.length > 0" ref="searchResults" class="file-tree-search-results">
152-
<div
153-
v-for="(result, idx) in filteredFiles"
154-
:key="result.matchResult.join('')"
155-
:class="['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
156-
@click="handleSearchResultClick(result.matchResult.join(''))"
157-
@mouseenter="selectedIndex = idx"
158-
:title="result.matchResult.join('')"
156+
<div
157+
v-if="searchQuery && filteredFiles.length > 0" id="file-search-results" ref="searchResults"
158+
class="file-tree-search-results" role="listbox" style="visibility: hidden"
159+
>
160+
<div
161+
v-for="(result, idx) in filteredFiles" :key="result.matchResult.join('')"
162+
:class="['file-tree-search-result-item', { 'selected': idx === selectedIndex }]" role="option"
163+
:aria-selected="idx === selectedIndex" @click="handleSearchResultClick(result.matchResult.join(''))"
164+
@mouseenter="selectedIndex = idx" :title="result.matchResult.join('')"
159165
>
160166
<!-- eslint-disable-next-line vue/no-v-html -->
161167
<span v-html="svg('octicon-file', 16)"/>
162168
<span class="file-tree-search-result-path">
163-
<span
164-
v-for="(part, index) in result.matchResult"
165-
:key="index"
166-
:class="{'search-match': index % 2 === 1}"
167-
>{{ part }}</span>
169+
<span
170+
v-for="(part, index) in result.matchResult" :key="index"
171+
:class="{ 'search-match': index % 2 === 1 }"
172+
>{{
173+
part }}</span>
168174
</span>
169175
</div>
170176
</div>
171-
<div v-if="searchQuery && filteredFiles.length === 0" ref="searchResults" class="file-tree-search-results file-tree-search-no-results">
177+
<div
178+
v-if="searchQuery && filteredFiles.length === 0" ref="searchResults"
179+
class="file-tree-search-results file-tree-search-no-results" style="visibility: hidden"
180+
>
172181
<div class="file-tree-no-results-content">
173182
<!-- eslint-disable-next-line vue/no-v-html -->
174183
<span v-html="svg('octicon-search', 24)"/>
@@ -193,11 +202,9 @@ watch(searchQuery, async () => {
193202
background: var(--color-box-body);
194203
border: 1px solid var(--color-secondary);
195204
border-radius: 6px;
196-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
197205
min-width: 300px;
198206
width: max-content;
199207
max-width: 600px;
200-
z-index: 99999;
201208
}
202209
203210
.file-tree-search-result-item {
@@ -210,7 +217,7 @@ watch(searchQuery, async () => {
210217
border-bottom: 1px solid var(--color-secondary);
211218
}
212219
213-
.file-tree-search-result-item > span:first-child {
220+
.file-tree-search-result-item>span:first-child {
214221
flex-shrink: 0;
215222
margin-top: 0.125rem;
216223
}
@@ -236,10 +243,6 @@ watch(searchQuery, async () => {
236243
font-weight: var(--font-weight-semibold);
237244
}
238245
239-
.file-tree-search-no-results {
240-
padding: 0;
241-
}
242-
243246
.file-tree-no-results-content {
244247
display: flex;
245248
flex-direction: column;
@@ -249,8 +252,4 @@ watch(searchQuery, async () => {
249252
color: var(--color-text-light-2);
250253
font-size: 14px;
251254
}
252-
253-
.file-tree-no-results-content > span:first-child {
254-
opacity: 0.5;
255-
}
256255
</style>

web_src/js/components/ViewFileTree.vue

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ const elRoot = useTemplateRef('elRoot');
88
const props = defineProps({
99
repoLink: {type: String, required: true},
1010
treePath: {type: String, required: true},
11-
currentRefNameSubURL: {type: String, required: true},
11+
currentRefNameSubURL: { type: String, required: true },
1212
});
1313
1414
const store = createViewFileTreeStore(props);
15-
1615
onMounted(async () => {
1716
store.rootFiles = await store.loadChildren('', props.treePath);
1817
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
19-
2018
window.addEventListener('popstate', (e) => {
2119
store.selectedItem = e.state?.treePath || '';
2220
if (e.state?.url) store.loadViewContent(e.state.url);
@@ -25,18 +23,12 @@ onMounted(async () => {
2523
</script>
2624

2725
<template>
28-
<div ref="elRoot" class="file-tree-root">
29-
<div class="view-file-tree-items">
30-
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
31-
</div>
26+
<div class="view-file-tree-items" ref="elRoot">
27+
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
3228
</div>
3329
</template>
3430

3531
<style scoped>
36-
.file-tree-root {
37-
position: relative;
38-
}
39-
4032
.view-file-tree-items {
4133
display: flex;
4234
flex-direction: column;

web_src/js/features/repo-view-file-tree.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export async function initRepoViewFileTree() {
2929

3030
registerGlobalEventFunc('click', 'onRepoViewFileTreeToggle', toggleSidebar);
3131

32-
// Mount file search component
3332
const fileSearchContainer = sidebar.querySelector('#file-tree-search-container');
3433
if (fileSearchContainer) {
3534
createApp(RepoFileSearch, {
@@ -41,7 +40,6 @@ export async function initRepoViewFileTree() {
4140
}).mount(fileSearchContainer);
4241
}
4342

44-
// Mount file tree component
4543
const fileTree = sidebar.querySelector('#view-file-tree');
4644
if (fileTree) {
4745
createApp(ViewFileTree, {

0 commit comments

Comments
 (0)