From 50bf7087fa64c01b78384364c05819f1a8700918 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Thu, 29 May 2025 01:59:43 +0200
Subject: [PATCH 01/17] Open new tab when clicking file tree with mouse wheel

---
 web_src/js/components/ViewFileTree.vue     |  6 +++++-
 web_src/js/components/ViewFileTreeItem.vue | 21 ++++++++++++---------
 2 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue
index c692142792754..bdbf71a04e975 100644
--- a/web_src/js/components/ViewFileTree.vue
+++ b/web_src/js/components/ViewFileTree.vue
@@ -37,8 +37,12 @@ async function loadViewContent(url: string) {
   document.querySelector('.repo-view-content').innerHTML = await response.text();
 }
 
-async function navigateTreeView(treePath: string) {
+async function navigateTreeView(treePath: string, newTab: boolean) {
   const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
+  if (newTab) {
+    window.open(url, '_blank');
+    return;
+  }
   window.history.pushState({treePath, url}, null, url);
   selectedItem.value = treePath;
   await loadViewContent(url);
diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index c39fa1f4aef16..bb3ab8fbeb40a 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -14,7 +14,7 @@ type Item = {
 
 const props = defineProps<{
   item: Item,
-  navigateViewContent:(treePath: string) => void,
+  navigateViewContent:(treePath: string, newTab: boolean) => void,
   loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
   selectedItem?: string,
 }>();
@@ -35,13 +35,13 @@ const doLoadChildren = async () => {
   }
 };
 
-const doLoadDirContent = () => {
-  doLoadChildren();
-  props.navigateViewContent(props.item.fullPath);
+const doLoadDirContent = (newTab: boolean) => {
+  if (!newTab) doLoadChildren();
+  props.navigateViewContent(props.item.fullPath, newTab);
 };
 
-const doLoadFileContent = () => {
-  props.navigateViewContent(props.item.fullPath);
+const doLoadFileContent = (newTab: boolean) => {
+  props.navigateViewContent(props.item.fullPath, newTab);
 };
 
 const doGotoSubModule = () => {
@@ -67,7 +67,8 @@ const doGotoSubModule = () => {
     v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadFileContent"
+    @click.stop="doLoadFileContent(false)"
+    @auxclick.stop="doLoadFileContent(true)"
   >
     <!-- symlink -->
     <div class="item-content">
@@ -80,7 +81,8 @@ const doGotoSubModule = () => {
     v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadFileContent"
+    @click.stop="doLoadFileContent(false)"
+    @auxclick.stop="doLoadFileContent(true)"
   >
     <!-- file -->
     <div class="item-content">
@@ -93,7 +95,8 @@ const doGotoSubModule = () => {
     v-else class="tree-item type-directory"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadDirContent"
+    @click.stop="doLoadDirContent(false)"
+    @auxclick.stop="doLoadDirContent(true)"
   >
     <!-- directory -->
     <div class="item-toggle">

From 83e925a6051c860f57623a81fbd2028d7f1547fb Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Thu, 29 May 2025 02:20:57 +0200
Subject: [PATCH 02/17] Use own function instead

---
 web_src/js/components/ViewFileTree.vue     |  2 +-
 web_src/js/components/ViewFileTreeItem.vue | 28 ++++++++++++----------
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue
index bdbf71a04e975..0e1ea7f59d3b0 100644
--- a/web_src/js/components/ViewFileTree.vue
+++ b/web_src/js/components/ViewFileTree.vue
@@ -37,7 +37,7 @@ async function loadViewContent(url: string) {
   document.querySelector('.repo-view-content').innerHTML = await response.text();
 }
 
-async function navigateTreeView(treePath: string, newTab: boolean) {
+async function navigateTreeView(treePath: string, newTab?: boolean) {
   const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
   if (newTab) {
     window.open(url, '_blank');
diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index bb3ab8fbeb40a..5b2a4e577bfa9 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -14,7 +14,7 @@ type Item = {
 
 const props = defineProps<{
   item: Item,
-  navigateViewContent:(treePath: string, newTab: boolean) => void,
+  navigateViewContent:(treePath: string, newTab?: boolean) => void,
   loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
   selectedItem?: string,
 }>();
@@ -35,13 +35,17 @@ const doLoadChildren = async () => {
   }
 };
 
-const doLoadDirContent = (newTab: boolean) => {
-  if (!newTab) doLoadChildren();
-  props.navigateViewContent(props.item.fullPath, newTab);
+const doLoadDirContent = () => {
+  doLoadChildren();
+  props.navigateViewContent(props.item.fullPath);
 };
 
-const doLoadFileContent = (newTab: boolean) => {
-  props.navigateViewContent(props.item.fullPath, newTab);
+const doLoadFileContent = () => {
+  props.navigateViewContent(props.item.fullPath);
+};
+
+const doOpenContentInNewTab = () => {
+  props.navigateViewContent(props.item.fullPath, true);
 };
 
 const doGotoSubModule = () => {
@@ -67,8 +71,8 @@ const doGotoSubModule = () => {
     v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadFileContent(false)"
-    @auxclick.stop="doLoadFileContent(true)"
+    @click.stop="doLoadFileContent"
+    @auxclick.stop="doOpenContentInNewTab"
   >
     <!-- symlink -->
     <div class="item-content">
@@ -81,8 +85,8 @@ const doGotoSubModule = () => {
     v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadFileContent(false)"
-    @auxclick.stop="doLoadFileContent(true)"
+    @click.stop="doLoadFileContent"
+    @auxclick.stop="doOpenContentInNewTab"
   >
     <!-- file -->
     <div class="item-content">
@@ -95,8 +99,8 @@ const doGotoSubModule = () => {
     v-else class="tree-item type-directory"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
-    @click.stop="doLoadDirContent(false)"
-    @auxclick.stop="doLoadDirContent(true)"
+    @click.stop="doLoadDirContent"
+    @auxclick.stop="doOpenContentInNewTab"
   >
     <!-- directory -->
     <div class="item-toggle">

From 91069cf5382365362f94a5608e282f6993514afa Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Sun, 15 Jun 2025 16:21:58 +0200
Subject: [PATCH 03/17] use click.middle instead of auxclick

---
 web_src/js/components/ViewFileTreeItem.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 5b2a4e577bfa9..cd1969e58c653 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -72,7 +72,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadFileContent"
-    @auxclick.stop="doOpenContentInNewTab"
+    @click.middle.stop="doOpenContentInNewTab"
   >
     <!-- symlink -->
     <div class="item-content">
@@ -86,7 +86,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadFileContent"
-    @auxclick.stop="doOpenContentInNewTab"
+    @click.middle.stop="doOpenContentInNewTab"
   >
     <!-- file -->
     <div class="item-content">
@@ -100,7 +100,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadDirContent"
-    @auxclick.stop="doOpenContentInNewTab"
+    @click.middle.stop="doOpenContentInNewTab"
   >
     <!-- directory -->
     <div class="item-toggle">

From 2b5e11981656ed4c0db17232dc786ba71838143d Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Mon, 16 Jun 2025 12:36:24 +0200
Subject: [PATCH 04/17] also check if ctrl key was pressed

---
 web_src/js/components/ViewFileTreeItem.vue | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index cd1969e58c653..8ffe3e464f68a 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -40,12 +40,12 @@ const doLoadDirContent = () => {
   props.navigateViewContent(props.item.fullPath);
 };
 
-const doLoadFileContent = () => {
-  props.navigateViewContent(props.item.fullPath);
-};
-
-const doOpenContentInNewTab = () => {
-  props.navigateViewContent(props.item.fullPath, true);
+const doLoadFileContent = (event: MouseEvent) => {
+  // open the file in a new tab if either
+  // - the auxiliary button (mouse wheel button) is the origin of the click
+  // - the ctrl key was pressed while clicking
+  const openNewTab = event.button === 1 || event.ctrlKey;
+  props.navigateViewContent(props.item.fullPath, openNewTab);
 };
 
 const doGotoSubModule = () => {
@@ -72,7 +72,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadFileContent"
-    @click.middle.stop="doOpenContentInNewTab"
+    @click.middle.stop="doLoadFileContent"
   >
     <!-- symlink -->
     <div class="item-content">
@@ -86,7 +86,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadFileContent"
-    @click.middle.stop="doOpenContentInNewTab"
+    @click.middle.stop="doLoadFileContent"
   >
     <!-- file -->
     <div class="item-content">
@@ -100,7 +100,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadDirContent"
-    @click.middle.stop="doOpenContentInNewTab"
+    @click.middle.stop="doLoadFileContent"
   >
     <!-- directory -->
     <div class="item-toggle">

From 06ac1c9f1371c321b4850cc967cdbf750b9d640d Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Mon, 16 Jun 2025 12:38:14 +0200
Subject: [PATCH 05/17] update comment

---
 web_src/js/components/ViewFileTreeItem.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 8ffe3e464f68a..696d234e10f18 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -42,7 +42,7 @@ const doLoadDirContent = () => {
 
 const doLoadFileContent = (event: MouseEvent) => {
   // open the file in a new tab if either
-  // - the auxiliary button (mouse wheel button) is the origin of the click
+  // - the auxiliary button (usually the mouse wheel button) is the origin of the click
   // - the ctrl key was pressed while clicking
   const openNewTab = event.button === 1 || event.ctrlKey;
   props.navigateViewContent(props.item.fullPath, openNewTab);

From 10d947114617c395b787c874d65f0d029982a2a3 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Mon, 16 Jun 2025 13:53:48 +0200
Subject: [PATCH 06/17] check meta key in addition to ctrl

---
 web_src/js/components/ViewFileTreeItem.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 696d234e10f18..8cd9d36b57295 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -43,8 +43,8 @@ const doLoadDirContent = () => {
 const doLoadFileContent = (event: MouseEvent) => {
   // open the file in a new tab if either
   // - the auxiliary button (usually the mouse wheel button) is the origin of the click
-  // - the ctrl key was pressed while clicking
-  const openNewTab = event.button === 1 || event.ctrlKey;
+  // - the ctrl key or meta key (for mac support) was pressed while clicking
+  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
   props.navigateViewContent(props.item.fullPath, openNewTab);
 };
 

From 9c4a8e2da7cd73cd605c700c8953b7a18c85c6aa Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Mon, 16 Jun 2025 14:02:46 +0200
Subject: [PATCH 07/17] add aux click check to doLoadDirContent

---
 web_src/js/components/ViewFileTreeItem.vue | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 8cd9d36b57295..42c1978a185cd 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -35,15 +35,16 @@ const doLoadChildren = async () => {
   }
 };
 
-const doLoadDirContent = () => {
-  doLoadChildren();
-  props.navigateViewContent(props.item.fullPath);
+const doLoadDirContent = (event: MouseEvent) => {
+  // open the directory in a new tab if either
+  // - the auxiliary button (usually the mouse wheel button) is the origin of the click
+  // - the ctrl key or meta key (for mac support) was pressed while clicking
+  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
+  if (!openNewTab) doLoadChildren();
+  props.navigateViewContent(props.item.fullPath, openNewTab);
 };
 
 const doLoadFileContent = (event: MouseEvent) => {
-  // open the file in a new tab if either
-  // - the auxiliary button (usually the mouse wheel button) is the origin of the click
-  // - the ctrl key or meta key (for mac support) was pressed while clicking
   const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
   props.navigateViewContent(props.item.fullPath, openNewTab);
 };
@@ -100,7 +101,7 @@ const doGotoSubModule = () => {
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     @click.stop="doLoadDirContent"
-    @click.middle.stop="doLoadFileContent"
+    @click.middle.stop="doLoadDirContent"
   >
     <!-- directory -->
     <div class="item-toggle">

From 9915581f4ce8111ddef27bb4765fdae6bed2a018 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Mon, 16 Jun 2025 14:46:54 +0200
Subject: [PATCH 08/17] add sub module aux click support

---
 web_src/js/components/ViewFileTreeItem.vue | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 42c1978a185cd..769888b651b07 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -49,7 +49,12 @@ const doLoadFileContent = (event: MouseEvent) => {
   props.navigateViewContent(props.item.fullPath, openNewTab);
 };
 
-const doGotoSubModule = () => {
+const doGotoSubModule = (event: MouseEvent) => {
+  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
+  if (openNewTab) {
+    window.open(props.item.submoduleUrl, '_blank');
+    return;
+  }
   location.href = props.item.submoduleUrl;
 };
 </script>
@@ -60,6 +65,7 @@ const doGotoSubModule = () => {
     v-if="item.entryMode === 'commit'" class="tree-item type-submodule"
     :title="item.entryName"
     @click.stop="doGotoSubModule"
+    @click.middle.stop="doGotoSubModule"
   >
     <!-- submodule -->
     <div class="item-content">

From ee2f52a1c3bf463bda3cfb061c5c44b257626925 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Wed, 18 Jun 2025 18:04:57 +0200
Subject: [PATCH 09/17] use `a` for file tree files

---
 web_src/js/components/ViewFileTree.vue     | 14 ++---
 web_src/js/components/ViewFileTreeItem.vue | 67 +++++++++++-----------
 2 files changed, 40 insertions(+), 41 deletions(-)

diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue
index 0e1ea7f59d3b0..b1e57752acbd5 100644
--- a/web_src/js/components/ViewFileTree.vue
+++ b/web_src/js/components/ViewFileTree.vue
@@ -37,17 +37,17 @@ async function loadViewContent(url: string) {
   document.querySelector('.repo-view-content').innerHTML = await response.text();
 }
 
-async function navigateTreeView(treePath: string, newTab?: boolean) {
-  const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
-  if (newTab) {
-    window.open(url, '_blank');
-    return;
-  }
+async function navigateTreeView(treePath: string) {
+  const url = getWebUrl(treePath);
   window.history.pushState({treePath, url}, null, url);
   selectedItem.value = treePath;
   await loadViewContent(url);
 }
 
+function getWebUrl(treePath: string) {
+  return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
+}
+
 onMounted(async () => {
   selectedItem.value = props.treePath;
   files.value = await loadChildren('', props.treePath);
@@ -62,7 +62,7 @@ onMounted(async () => {
 <template>
   <div class="view-file-tree-items" ref="elRoot">
     <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
-    <ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/>
+    <ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :get-web-url="getWebUrl" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/>
   </div>
 </template>
 
diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 769888b651b07..9474141be5759 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -14,8 +14,9 @@ type Item = {
 
 const props = defineProps<{
   item: Item,
-  navigateViewContent:(treePath: string, newTab?: boolean) => void,
+  navigateViewContent:(treePath: string) => void,
   loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
+  getWebUrl:(treePath: string) => string,
   selectedItem?: string,
 }>();
 
@@ -23,7 +24,11 @@ const isLoading = ref(false);
 const children = ref(props.item.children);
 const collapsed = ref(!props.item.children);
 
-const doLoadChildren = async () => {
+const doLoadChildren = async (e?: MouseEvent) => {
+  // the event is only not undefined if the user explicitly clicked on the directory item toggle. the preventDefault
+  // stops the event from bubbling up and causing a directory content load
+  e?.preventDefault();
+
   collapsed.value = !collapsed.value;
   if (!collapsed.value && props.loadChildren) {
     isLoading.value = true;
@@ -35,37 +40,29 @@ const doLoadChildren = async () => {
   }
 };
 
-const doLoadDirContent = (event: MouseEvent) => {
-  // open the directory in a new tab if either
-  // - the auxiliary button (usually the mouse wheel button) is the origin of the click
-  // - the ctrl key or meta key (for mac support) was pressed while clicking
-  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
-  if (!openNewTab) doLoadChildren();
-  props.navigateViewContent(props.item.fullPath, openNewTab);
-};
+const doLoadDirContent = (e: MouseEvent) => {
+  // only load the directory content without a window refresh if the user didn't press any special key
+  if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
+  e.preventDefault();
 
-const doLoadFileContent = (event: MouseEvent) => {
-  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
-  props.navigateViewContent(props.item.fullPath, openNewTab);
+  doLoadChildren();
+  props.navigateViewContent(props.item.fullPath);
 };
 
-const doGotoSubModule = (event: MouseEvent) => {
-  const openNewTab = event.button === 1 || event.ctrlKey || event.metaKey;
-  if (openNewTab) {
-    window.open(props.item.submoduleUrl, '_blank');
-    return;
-  }
-  location.href = props.item.submoduleUrl;
+const doLoadFileContent = (e: MouseEvent) => {
+  if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
+  e.preventDefault();
+
+  props.navigateViewContent(props.item.fullPath);
 };
 </script>
 
 <!--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"-->
 <template>
-  <div
+  <a
     v-if="item.entryMode === 'commit'" class="tree-item type-submodule"
     :title="item.entryName"
-    @click.stop="doGotoSubModule"
-    @click.middle.stop="doGotoSubModule"
+    :href="getWebUrl(item.fullPath)"
   >
     <!-- submodule -->
     <div class="item-content">
@@ -73,13 +70,13 @@ const doGotoSubModule = (event: MouseEvent) => {
       <span class="tw-contents" v-html="item.entryIcon"/>
       <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
     </div>
-  </div>
-  <div
+  </a>
+  <a
     v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
+    :href="getWebUrl(item.fullPath)"
     @click.stop="doLoadFileContent"
-    @click.middle.stop="doLoadFileContent"
   >
     <!-- symlink -->
     <div class="item-content">
@@ -87,13 +84,13 @@ const doGotoSubModule = (event: MouseEvent) => {
       <span class="tw-contents" v-html="item.entryIcon"/>
       <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
     </div>
-  </div>
-  <div
+  </a>
+  <a
     v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
+    :href="getWebUrl(item.fullPath)"
     @click.stop="doLoadFileContent"
-    @click.middle.stop="doLoadFileContent"
   >
     <!-- file -->
     <div class="item-content">
@@ -101,13 +98,13 @@ const doGotoSubModule = (event: MouseEvent) => {
       <span class="tw-contents" v-html="item.entryIcon"/>
       <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
     </div>
-  </div>
-  <div
+  </a>
+  <a
     v-else class="tree-item type-directory"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
+    :href="getWebUrl(item.fullPath)"
     @click.stop="doLoadDirContent"
-    @click.middle.stop="doLoadDirContent"
   >
     <!-- directory -->
     <div class="item-toggle">
@@ -119,10 +116,10 @@ const doGotoSubModule = (event: MouseEvent) => {
       <span class="tw-contents" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/>
       <span class="gt-ellipsis">{{ item.entryName }}</span>
     </div>
-  </div>
+  </a>
 
   <div v-if="children?.length" v-show="!collapsed" class="sub-items">
-    <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/>
+    <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :get-web-url="getWebUrl" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/>
   </div>
 </template>
 <style scoped>
@@ -145,6 +142,8 @@ const doGotoSubModule = (event: MouseEvent) => {
 }
 
 .tree-item {
+  color: inherit;
+  text-decoration: inherit;
   display: grid;
   grid-template-columns: 16px 1fr;
   grid-template-areas: "toggle content";

From 0c8c45fdb6d9ac2df466a5c334f6f761323c6daf Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Wed, 18 Jun 2025 18:51:32 +0200
Subject: [PATCH 10/17] use class to disable default link styling

---
 web_src/js/components/ViewFileTreeItem.vue | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 9474141be5759..b9d9d15f0d697 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -60,7 +60,7 @@ const doLoadFileContent = (e: MouseEvent) => {
 <!--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"-->
 <template>
   <a
-    v-if="item.entryMode === 'commit'" class="tree-item type-submodule"
+    v-if="item.entryMode === 'commit'" class="tree-item type-submodule silenced"
     :title="item.entryName"
     :href="getWebUrl(item.fullPath)"
   >
@@ -72,7 +72,7 @@ const doLoadFileContent = (e: MouseEvent) => {
     </div>
   </a>
   <a
-    v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
+    v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink silenced"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     :href="getWebUrl(item.fullPath)"
@@ -86,7 +86,7 @@ const doLoadFileContent = (e: MouseEvent) => {
     </div>
   </a>
   <a
-    v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
+    v-else-if="item.entryMode !== 'tree'" class="tree-item type-file silenced"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     :href="getWebUrl(item.fullPath)"
@@ -100,7 +100,7 @@ const doLoadFileContent = (e: MouseEvent) => {
     </div>
   </a>
   <a
-    v-else class="tree-item type-directory"
+    v-else class="tree-item type-directory silenced"
     :class="{'selected': selectedItem === item.fullPath}"
     :title="item.entryName"
     :href="getWebUrl(item.fullPath)"
@@ -142,8 +142,6 @@ const doLoadFileContent = (e: MouseEvent) => {
 }
 
 .tree-item {
-  color: inherit;
-  text-decoration: inherit;
   display: grid;
   grid-template-columns: 16px 1fr;
   grid-template-areas: "toggle content";

From e2655f3df71a7ffe9914b546b2287516b47fd00d Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Wed, 18 Jun 2025 18:53:09 +0200
Subject: [PATCH 11/17] move plain click check to utils

---
 web_src/js/components/ViewFileTreeItem.vue | 5 +++--
 web_src/js/utils/dom.ts                    | 5 +++++
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index b9d9d15f0d697..6c4481748b75a 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import {SvgIcon} from '../svg.ts';
+import {isPlainClick} from '../utils/dom.ts';
 import {ref} from 'vue';
 
 type Item = {
@@ -42,7 +43,7 @@ const doLoadChildren = async (e?: MouseEvent) => {
 
 const doLoadDirContent = (e: MouseEvent) => {
   // only load the directory content without a window refresh if the user didn't press any special key
-  if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
+  if (!isPlainClick(e)) return;
   e.preventDefault();
 
   doLoadChildren();
@@ -50,7 +51,7 @@ const doLoadDirContent = (e: MouseEvent) => {
 };
 
 const doLoadFileContent = (e: MouseEvent) => {
-  if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
+  if (!isPlainClick(e)) return;
   e.preventDefault();
 
   props.navigateViewContent(props.item.fullPath);
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 8f758bf9accff..6a5baa188809b 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -369,3 +369,8 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
     listener(elem as T, e as E);
   }, options);
 }
+
+// Check if a click is done without any special mouse or key inputs that would trigger special browser behavior.
+export function isPlainClick(e: MouseEvent) {
+  return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
+}

From 35ab11f333be0bd021f676a06c23b7e6d7380165 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Wed, 18 Jun 2025 18:54:19 +0200
Subject: [PATCH 12/17] mark mouse event explicitly null

---
 web_src/js/components/ViewFileTreeItem.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 6c4481748b75a..32d637c4afd88 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -25,8 +25,8 @@ const isLoading = ref(false);
 const children = ref(props.item.children);
 const collapsed = ref(!props.item.children);
 
-const doLoadChildren = async (e?: MouseEvent) => {
-  // the event is only not undefined if the user explicitly clicked on the directory item toggle. the preventDefault
+const doLoadChildren = async (e: MouseEvent | null) => {
+  // the event is only not null if the user explicitly clicked on the directory item toggle. the preventDefault
   // stops the event from bubbling up and causing a directory content load
   e?.preventDefault();
 
@@ -46,7 +46,7 @@ const doLoadDirContent = (e: MouseEvent) => {
   if (!isPlainClick(e)) return;
   e.preventDefault();
 
-  doLoadChildren();
+  doLoadChildren(null);
   props.navigateViewContent(props.item.fullPath);
 };
 

From ed23d70b621893df5fd6c35d3fbd7e985358020b Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Wed, 18 Jun 2025 18:55:53 +0200
Subject: [PATCH 13/17] update docs

---
 web_src/js/utils/dom.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 6a5baa188809b..7ed0d73406e31 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -370,7 +370,7 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
   }, options);
 }
 
-// Check if a click is done without any special mouse or key inputs that would trigger special browser behavior.
+/** Returns whether a click event is a left-click without any modifiers held */
 export function isPlainClick(e: MouseEvent) {
   return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
 }

From 156de3091d27a61a98a051491af93f7580801f65 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 19 Jun 2025 13:08:35 +0800
Subject: [PATCH 14/17] refactor

---
 web_src/js/components/DiffFileTree.vue     |  2 +-
 web_src/js/components/ViewFileTree.vue     | 50 +++-------------------
 web_src/js/components/ViewFileTreeItem.vue | 31 +++++++-------
 web_src/js/components/ViewFileTreeStore.ts | 44 +++++++++++++++++++
 4 files changed, 66 insertions(+), 61 deletions(-)
 create mode 100644 web_src/js/components/ViewFileTreeStore.ts

diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index 5426a672cbe1e..981d10c1c16fc 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -60,8 +60,8 @@ function updateState(visible: boolean) {
 </script>
 
 <template>
+  <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
   <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
-    <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
     <DiffFileTreeItem v-for="item in store.diffFileTree.TreeRoot.Children" :key="item.FullName" :item="item"/>
   </div>
 </template>
diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue
index b1e57752acbd5..d56082415980a 100644
--- a/web_src/js/components/ViewFileTree.vue
+++ b/web_src/js/components/ViewFileTree.vue
@@ -1,9 +1,7 @@
 <script lang="ts" setup>
 import ViewFileTreeItem from './ViewFileTreeItem.vue';
 import {onMounted, ref} from 'vue';
-import {pathEscapeSegments} from '../utils/url.ts';
-import {GET} from '../modules/fetch.ts';
-import {createElementFromHTML} from '../utils/dom.ts';
+import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
 
 const elRoot = ref<HTMLElement | null>(null);
 
@@ -13,56 +11,20 @@ const props = defineProps({
   currentRefNameSubURL: {type: String, required: true},
 });
 
-const files = ref([]);
-const selectedItem = ref('');
-
-async function loadChildren(treePath: string, subPath: string = '') {
-  const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
-  const json = await response.json();
-  const poolSvgs = [];
-  for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
-    if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
-  }
-  if (poolSvgs.length) {
-    const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
-    svgContainer.innerHTML = poolSvgs.join('');
-    document.body.append(svgContainer);
-  }
-  return json.fileTreeNodes ?? null;
-}
-
-async function loadViewContent(url: string) {
-  url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
-  const response = await GET(url);
-  document.querySelector('.repo-view-content').innerHTML = await response.text();
-}
-
-async function navigateTreeView(treePath: string) {
-  const url = getWebUrl(treePath);
-  window.history.pushState({treePath, url}, null, url);
-  selectedItem.value = treePath;
-  await loadViewContent(url);
-}
-
-function getWebUrl(treePath: string) {
-  return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
-}
-
+const store = createViewFileTreeStore(props);
 onMounted(async () => {
-  selectedItem.value = props.treePath;
-  files.value = await loadChildren('', props.treePath);
+  store.rootFiles = await store.loadChildren('', props.treePath);
   elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
   window.addEventListener('popstate', (e) => {
-    selectedItem.value = e.state?.treePath || '';
-    if (e.state?.url) loadViewContent(e.state.url);
+    store.selectedItem = e.state?.treePath || '';
+    if (e.state?.url) store.loadViewContent(e.state.url);
   });
 });
 </script>
 
 <template>
   <div class="view-file-tree-items" ref="elRoot">
-    <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
-    <ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :get-web-url="getWebUrl" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/>
+    <ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
   </div>
 </template>
 
diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 32d637c4afd88..931fc8d881cb9 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -2,6 +2,7 @@
 import {SvgIcon} from '../svg.ts';
 import {isPlainClick} from '../utils/dom.ts';
 import {ref} from 'vue';
+import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
 
 type Item = {
   entryName: string;
@@ -15,12 +16,10 @@ type Item = {
 
 const props = defineProps<{
   item: Item,
-  navigateViewContent:(treePath: string) => void,
-  loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
-  getWebUrl:(treePath: string) => string,
-  selectedItem?: string,
+  store: ReturnType<typeof createViewFileTreeStore>
 }>();
 
+const store = props.store;
 const isLoading = ref(false);
 const children = ref(props.item.children);
 const collapsed = ref(!props.item.children);
@@ -31,10 +30,10 @@ const doLoadChildren = async (e: MouseEvent | null) => {
   e?.preventDefault();
 
   collapsed.value = !collapsed.value;
-  if (!collapsed.value && props.loadChildren) {
+  if (!collapsed.value) {
     isLoading.value = true;
     try {
-      children.value = await props.loadChildren(props.item.fullPath);
+      children.value = await store.loadChildren(props.item.fullPath);
     } finally {
       isLoading.value = false;
     }
@@ -47,14 +46,14 @@ const doLoadDirContent = (e: MouseEvent) => {
   e.preventDefault();
 
   doLoadChildren(null);
-  props.navigateViewContent(props.item.fullPath);
+  store.navigateTreeView(props.item.fullPath);
 };
 
 const doLoadFileContent = (e: MouseEvent) => {
   if (!isPlainClick(e)) return;
   e.preventDefault();
 
-  props.navigateViewContent(props.item.fullPath);
+  store.navigateTreeView(props.item.fullPath);
 };
 </script>
 
@@ -63,7 +62,7 @@ const doLoadFileContent = (e: MouseEvent) => {
   <a
     v-if="item.entryMode === 'commit'" class="tree-item type-submodule silenced"
     :title="item.entryName"
-    :href="getWebUrl(item.fullPath)"
+    :href="store.getWebUrl(item.fullPath)"
   >
     <!-- submodule -->
     <div class="item-content">
@@ -74,9 +73,9 @@ const doLoadFileContent = (e: MouseEvent) => {
   </a>
   <a
     v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink silenced"
-    :class="{'selected': selectedItem === item.fullPath}"
+    :class="{'selected': store.selectedItem === item.fullPath}"
     :title="item.entryName"
-    :href="getWebUrl(item.fullPath)"
+    :href="store.getWebUrl(item.fullPath)"
     @click.stop="doLoadFileContent"
   >
     <!-- symlink -->
@@ -88,9 +87,9 @@ const doLoadFileContent = (e: MouseEvent) => {
   </a>
   <a
     v-else-if="item.entryMode !== 'tree'" class="tree-item type-file silenced"
-    :class="{'selected': selectedItem === item.fullPath}"
+    :class="{'selected': store.selectedItem === item.fullPath}"
     :title="item.entryName"
-    :href="getWebUrl(item.fullPath)"
+    :href="store.getWebUrl(item.fullPath)"
     @click.stop="doLoadFileContent"
   >
     <!-- file -->
@@ -102,9 +101,9 @@ const doLoadFileContent = (e: MouseEvent) => {
   </a>
   <a
     v-else class="tree-item type-directory silenced"
-    :class="{'selected': selectedItem === item.fullPath}"
+    :class="{'selected': store.selectedItem === item.fullPath}"
     :title="item.entryName"
-    :href="getWebUrl(item.fullPath)"
+    :href="store.getWebUrl(item.fullPath)"
     @click.stop="doLoadDirContent"
   >
     <!-- directory -->
@@ -120,7 +119,7 @@ const doLoadFileContent = (e: MouseEvent) => {
   </a>
 
   <div v-if="children?.length" v-show="!collapsed" class="sub-items">
-    <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :get-web-url="getWebUrl" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/>
+    <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/>
   </div>
 </template>
 <style scoped>
diff --git a/web_src/js/components/ViewFileTreeStore.ts b/web_src/js/components/ViewFileTreeStore.ts
new file mode 100644
index 0000000000000..6f249ee19d0ed
--- /dev/null
+++ b/web_src/js/components/ViewFileTreeStore.ts
@@ -0,0 +1,44 @@
+import {reactive} from 'vue';
+import {GET} from '../modules/fetch.ts';
+import {pathEscapeSegments} from '../utils/url.ts';
+import {createElementFromHTML} from '../utils/dom.ts';
+
+export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) {
+  const store = reactive({
+    rootFiles: [],
+    selectedItem: props.treePath,
+
+    async loadChildren(treePath: string, subPath: string = '') {
+      const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
+      const json = await response.json();
+      const poolSvgs = [];
+      for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
+        if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
+      }
+      if (poolSvgs.length) {
+        const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
+        svgContainer.innerHTML = poolSvgs.join('');
+        document.body.append(svgContainer);
+      }
+      return json.fileTreeNodes ?? null;
+    },
+
+    async loadViewContent(url: string) {
+      url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
+      const response = await GET(url);
+      document.querySelector('.repo-view-content').innerHTML = await response.text();
+    },
+
+    async navigateTreeView(treePath: string) {
+      const url = store.getWebUrl(treePath);
+      window.history.pushState({treePath, url}, null, url);
+      store.selectedItem = treePath;
+      await store.loadViewContent(url);
+    },
+
+    getWebUrl(treePath: string) {
+      return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
+    },
+  });
+  return store;
+}

From 55195776d4b555123cf781687c2d532f0ca04f38 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 19 Jun 2025 13:29:14 +0800
Subject: [PATCH 15/17] refactor

---
 web_src/js/components/ViewFileTreeItem.vue | 77 +++++-----------------
 1 file changed, 16 insertions(+), 61 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 931fc8d881cb9..60c5fc6d6e049 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -6,7 +6,7 @@ import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
 
 type Item = {
   entryName: string;
-  entryMode: string;
+  entryMode: 'blob' | 'exec' | 'tree' | 'commit' | 'symlink' | 'unknown';
   entryIcon: string;
   entryIconOpen: string;
   fullPath: string;
@@ -24,11 +24,7 @@ const isLoading = ref(false);
 const children = ref(props.item.children);
 const collapsed = ref(!props.item.children);
 
-const doLoadChildren = async (e: MouseEvent | null) => {
-  // the event is only not null if the user explicitly clicked on the directory item toggle. the preventDefault
-  // stops the event from bubbling up and causing a directory content load
-  e?.preventDefault();
-
+const doLoadChildren = async () => {
   collapsed.value = !collapsed.value;
   if (!collapsed.value) {
     isLoading.value = true;
@@ -40,74 +36,33 @@ const doLoadChildren = async (e: MouseEvent | null) => {
   }
 };
 
-const doLoadDirContent = (e: MouseEvent) => {
-  // only load the directory content without a window refresh if the user didn't press any special key
+const onItemClick = (e: MouseEvent) => {
+  // only handle the click event with page partial reloading if the user didn't press any special key
+  // let browsers handle special keys like "Ctrl+Click"
   if (!isPlainClick(e)) return;
   e.preventDefault();
-
-  doLoadChildren(null);
+  if (props.item.entryMode === 'tree') doLoadChildren();
   store.navigateTreeView(props.item.fullPath);
 };
 
-const doLoadFileContent = (e: MouseEvent) => {
-  if (!isPlainClick(e)) return;
-  e.preventDefault();
-
-  store.navigateTreeView(props.item.fullPath);
-};
 </script>
 
-<!--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"-->
 <template>
   <a
-    v-if="item.entryMode === 'commit'" class="tree-item type-submodule silenced"
-    :title="item.entryName"
-    :href="store.getWebUrl(item.fullPath)"
-  >
-    <!-- submodule -->
-    <div class="item-content">
-      <!-- eslint-disable-next-line vue/no-v-html -->
-      <span class="tw-contents" v-html="item.entryIcon"/>
-      <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
-    </div>
-  </a>
-  <a
-    v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink silenced"
-    :class="{'selected': store.selectedItem === item.fullPath}"
-    :title="item.entryName"
-    :href="store.getWebUrl(item.fullPath)"
-    @click.stop="doLoadFileContent"
-  >
-    <!-- symlink -->
-    <div class="item-content">
-      <!-- eslint-disable-next-line vue/no-v-html -->
-      <span class="tw-contents" v-html="item.entryIcon"/>
-      <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
-    </div>
-  </a>
-  <a
-    v-else-if="item.entryMode !== 'tree'" class="tree-item type-file silenced"
-    :class="{'selected': store.selectedItem === item.fullPath}"
-    :title="item.entryName"
-    :href="store.getWebUrl(item.fullPath)"
-    @click.stop="doLoadFileContent"
-  >
-    <!-- file -->
-    <div class="item-content">
-      <!-- eslint-disable-next-line vue/no-v-html -->
-      <span class="tw-contents" v-html="item.entryIcon"/>
-      <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
-    </div>
-  </a>
-  <a
-    v-else class="tree-item type-directory silenced"
-    :class="{'selected': store.selectedItem === item.fullPath}"
+    class="tree-item silenced"
+    :class="{
+      'selected': store.selectedItem === item.fullPath,
+      'type-submodule': item.entryMode === 'commit',
+      'type-directory': item.entryMode === 'tree',
+      'type-symlink': item.entryMode === 'symlink',
+      'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
+    }"
     :title="item.entryName"
     :href="store.getWebUrl(item.fullPath)"
-    @click.stop="doLoadDirContent"
+    @click.stop="onItemClick"
   >
     <!-- directory -->
-    <div class="item-toggle">
+    <div v-if="item.entryMode === 'tree'" class="item-toggle">
       <SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/>
       <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
     </div>

From ef3873c4a7035f62eff4f53c077fb75a051cd563 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 19 Jun 2025 13:33:22 +0800
Subject: [PATCH 16/17] refactor

---
 web_src/js/components/ViewFileTreeItem.vue | 4 ++--
 web_src/js/components/ViewFileTreeStore.ts | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 60c5fc6d6e049..8ba2421907e80 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -58,10 +58,9 @@ const onItemClick = (e: MouseEvent) => {
       'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
     }"
     :title="item.entryName"
-    :href="store.getWebUrl(item.fullPath)"
+    :href="store.buildTreePathWebUrl(item.fullPath)"
     @click.stop="onItemClick"
   >
-    <!-- directory -->
     <div v-if="item.entryMode === 'tree'" class="item-toggle">
       <SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/>
       <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
@@ -77,6 +76,7 @@ const onItemClick = (e: MouseEvent) => {
     <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/>
   </div>
 </template>
+
 <style scoped>
 .sub-items {
   display: flex;
diff --git a/web_src/js/components/ViewFileTreeStore.ts b/web_src/js/components/ViewFileTreeStore.ts
index 6f249ee19d0ed..13e2753c94018 100644
--- a/web_src/js/components/ViewFileTreeStore.ts
+++ b/web_src/js/components/ViewFileTreeStore.ts
@@ -30,13 +30,13 @@ export function createViewFileTreeStore(props: { repoLink: string, treePath: str
     },
 
     async navigateTreeView(treePath: string) {
-      const url = store.getWebUrl(treePath);
+      const url = store.buildTreePathWebUrl(treePath);
       window.history.pushState({treePath, url}, null, url);
       store.selectedItem = treePath;
       await store.loadViewContent(url);
     },
 
-    getWebUrl(treePath: string) {
+    buildTreePathWebUrl(treePath: string) {
       return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
     },
   });

From f01e51fd366995967d0385724ed128b2cde9ed03 Mon Sep 17 00:00:00 2001
From: bytedream <me@bytedream.dev>
Date: Thu, 19 Jun 2025 19:49:44 +0200
Subject: [PATCH 17/17] prevent page reload on subtree load

---
 web_src/js/components/ViewFileTreeItem.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 8ba2421907e80..4a7569e92139a 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -63,7 +63,7 @@ const onItemClick = (e: MouseEvent) => {
   >
     <div v-if="item.entryMode === 'tree'" class="item-toggle">
       <SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/>
-      <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
+      <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop.prevent="doLoadChildren"/>
     </div>
     <div class="item-content">
       <!-- eslint-disable-next-line vue/no-v-html -->