Skip to content

Commit 7346ae7

Browse files
Add repo file tree item link behavior (#34730)
Converts the repo file tree items into `<a>` elements to have default link behavior. Dynamic content load is still done when no special key is pressed while clicking on an item. --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 0ea958d commit 7346ae7

File tree

5 files changed

+86
-105
lines changed

5 files changed

+86
-105
lines changed

web_src/js/components/DiffFileTree.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ function updateState(visible: boolean) {
6060
</script>
6161

6262
<template>
63+
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
6364
<div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
64-
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
6565
<DiffFileTreeItem v-for="item in store.diffFileTree.TreeRoot.Children" :key="item.FullName" :item="item"/>
6666
</div>
6767
</template>

web_src/js/components/ViewFileTree.vue

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
<script lang="ts" setup>
22
import ViewFileTreeItem from './ViewFileTreeItem.vue';
33
import {onMounted, ref} from 'vue';
4-
import {pathEscapeSegments} from '../utils/url.ts';
5-
import {GET} from '../modules/fetch.ts';
6-
import {createElementFromHTML} from '../utils/dom.ts';
4+
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
75
86
const elRoot = ref<HTMLElement | null>(null);
97
@@ -13,52 +11,20 @@ const props = defineProps({
1311
currentRefNameSubURL: {type: String, required: true},
1412
});
1513
16-
const files = ref([]);
17-
const selectedItem = ref('');
18-
19-
async function loadChildren(treePath: string, subPath: string = '') {
20-
const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
21-
const json = await response.json();
22-
const poolSvgs = [];
23-
for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
24-
if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
25-
}
26-
if (poolSvgs.length) {
27-
const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
28-
svgContainer.innerHTML = poolSvgs.join('');
29-
document.body.append(svgContainer);
30-
}
31-
return json.fileTreeNodes ?? null;
32-
}
33-
34-
async function loadViewContent(url: string) {
35-
url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
36-
const response = await GET(url);
37-
document.querySelector('.repo-view-content').innerHTML = await response.text();
38-
}
39-
40-
async function navigateTreeView(treePath: string) {
41-
const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
42-
window.history.pushState({treePath, url}, null, url);
43-
selectedItem.value = treePath;
44-
await loadViewContent(url);
45-
}
46-
14+
const store = createViewFileTreeStore(props);
4715
onMounted(async () => {
48-
selectedItem.value = props.treePath;
49-
files.value = await loadChildren('', props.treePath);
16+
store.rootFiles = await store.loadChildren('', props.treePath);
5017
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
5118
window.addEventListener('popstate', (e) => {
52-
selectedItem.value = e.state?.treePath || '';
53-
if (e.state?.url) loadViewContent(e.state.url);
19+
store.selectedItem = e.state?.treePath || '';
20+
if (e.state?.url) store.loadViewContent(e.state.url);
5421
});
5522
});
5623
</script>
5724

5825
<template>
5926
<div class="view-file-tree-items" ref="elRoot">
60-
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
61-
<ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/>
27+
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
6228
</div>
6329
</template>
6430

web_src/js/components/ViewFileTreeItem.vue

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script lang="ts" setup>
22
import {SvgIcon} from '../svg.ts';
3+
import {isPlainClick} from '../utils/dom.ts';
34
import {ref} from 'vue';
5+
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
46
57
type Item = {
68
entryName: string;
7-
entryMode: string;
9+
entryMode: 'blob' | 'exec' | 'tree' | 'commit' | 'symlink' | 'unknown';
810
entryIcon: string;
911
entryIconOpen: string;
1012
fullPath: string;
@@ -14,103 +16,67 @@ type Item = {
1416
1517
const props = defineProps<{
1618
item: Item,
17-
navigateViewContent:(treePath: string) => void,
18-
loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
19-
selectedItem?: string,
19+
store: ReturnType<typeof createViewFileTreeStore>
2020
}>();
2121
22+
const store = props.store;
2223
const isLoading = ref(false);
2324
const children = ref(props.item.children);
2425
const collapsed = ref(!props.item.children);
2526
2627
const doLoadChildren = async () => {
2728
collapsed.value = !collapsed.value;
28-
if (!collapsed.value && props.loadChildren) {
29+
if (!collapsed.value) {
2930
isLoading.value = true;
3031
try {
31-
children.value = await props.loadChildren(props.item.fullPath);
32+
children.value = await store.loadChildren(props.item.fullPath);
3233
} finally {
3334
isLoading.value = false;
3435
}
3536
}
3637
};
3738
38-
const doLoadDirContent = () => {
39-
doLoadChildren();
40-
props.navigateViewContent(props.item.fullPath);
39+
const onItemClick = (e: MouseEvent) => {
40+
// only handle the click event with page partial reloading if the user didn't press any special key
41+
// let browsers handle special keys like "Ctrl+Click"
42+
if (!isPlainClick(e)) return;
43+
e.preventDefault();
44+
if (props.item.entryMode === 'tree') doLoadChildren();
45+
store.navigateTreeView(props.item.fullPath);
4146
};
4247
43-
const doLoadFileContent = () => {
44-
props.navigateViewContent(props.item.fullPath);
45-
};
46-
47-
const doGotoSubModule = () => {
48-
location.href = props.item.submoduleUrl;
49-
};
5048
</script>
5149

52-
<!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"-->
5350
<template>
54-
<div
55-
v-if="item.entryMode === 'commit'" class="tree-item type-submodule"
56-
:title="item.entryName"
57-
@click.stop="doGotoSubModule"
58-
>
59-
<!-- submodule -->
60-
<div class="item-content">
61-
<!-- eslint-disable-next-line vue/no-v-html -->
62-
<span class="tw-contents" v-html="item.entryIcon"/>
63-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
64-
</div>
65-
</div>
66-
<div
67-
v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
68-
:class="{'selected': selectedItem === item.fullPath}"
51+
<a
52+
class="tree-item silenced"
53+
:class="{
54+
'selected': store.selectedItem === item.fullPath,
55+
'type-submodule': item.entryMode === 'commit',
56+
'type-directory': item.entryMode === 'tree',
57+
'type-symlink': item.entryMode === 'symlink',
58+
'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
59+
}"
6960
:title="item.entryName"
70-
@click.stop="doLoadFileContent"
61+
:href="store.buildTreePathWebUrl(item.fullPath)"
62+
@click.stop="onItemClick"
7163
>
72-
<!-- symlink -->
73-
<div class="item-content">
74-
<!-- eslint-disable-next-line vue/no-v-html -->
75-
<span class="tw-contents" v-html="item.entryIcon"/>
76-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
77-
</div>
78-
</div>
79-
<div
80-
v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
81-
:class="{'selected': selectedItem === item.fullPath}"
82-
:title="item.entryName"
83-
@click.stop="doLoadFileContent"
84-
>
85-
<!-- file -->
86-
<div class="item-content">
87-
<!-- eslint-disable-next-line vue/no-v-html -->
88-
<span class="tw-contents" v-html="item.entryIcon"/>
89-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
90-
</div>
91-
</div>
92-
<div
93-
v-else class="tree-item type-directory"
94-
:class="{'selected': selectedItem === item.fullPath}"
95-
:title="item.entryName"
96-
@click.stop="doLoadDirContent"
97-
>
98-
<!-- directory -->
99-
<div class="item-toggle">
64+
<div v-if="item.entryMode === 'tree'" class="item-toggle">
10065
<SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/>
101-
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
66+
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop.prevent="doLoadChildren"/>
10267
</div>
10368
<div class="item-content">
10469
<!-- eslint-disable-next-line vue/no-v-html -->
10570
<span class="tw-contents" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/>
10671
<span class="gt-ellipsis">{{ item.entryName }}</span>
10772
</div>
108-
</div>
73+
</a>
10974

11075
<div v-if="children?.length" v-show="!collapsed" class="sub-items">
111-
<ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/>
76+
<ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/>
11277
</div>
11378
</template>
79+
11480
<style scoped>
11581
.sub-items {
11682
display: flex;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {reactive} from 'vue';
2+
import {GET} from '../modules/fetch.ts';
3+
import {pathEscapeSegments} from '../utils/url.ts';
4+
import {createElementFromHTML} from '../utils/dom.ts';
5+
6+
export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) {
7+
const store = reactive({
8+
rootFiles: [],
9+
selectedItem: props.treePath,
10+
11+
async loadChildren(treePath: string, subPath: string = '') {
12+
const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
13+
const json = await response.json();
14+
const poolSvgs = [];
15+
for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
16+
if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
17+
}
18+
if (poolSvgs.length) {
19+
const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
20+
svgContainer.innerHTML = poolSvgs.join('');
21+
document.body.append(svgContainer);
22+
}
23+
return json.fileTreeNodes ?? null;
24+
},
25+
26+
async loadViewContent(url: string) {
27+
url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
28+
const response = await GET(url);
29+
document.querySelector('.repo-view-content').innerHTML = await response.text();
30+
},
31+
32+
async navigateTreeView(treePath: string) {
33+
const url = store.buildTreePathWebUrl(treePath);
34+
window.history.pushState({treePath, url}, null, url);
35+
store.selectedItem = treePath;
36+
await store.loadViewContent(url);
37+
},
38+
39+
buildTreePathWebUrl(treePath: string) {
40+
return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
41+
},
42+
});
43+
return store;
44+
}

web_src/js/utils/dom.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,8 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
369369
listener(elem as T, e as E);
370370
}, options);
371371
}
372+
373+
/** Returns whether a click event is a left-click without any modifiers held */
374+
export function isPlainClick(e: MouseEvent) {
375+
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
376+
}

0 commit comments

Comments
 (0)