mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-12 21:58:57 +02:00
114 lines
5.8 KiB
JavaScript
114 lines
5.8 KiB
JavaScript
import { h, reactive, ref, computed, watch, nextTick } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
|
|
export function fileHierarchy(files) {
|
|
const root = { children: [] };
|
|
const directories = new Map([["", root]]);
|
|
function directory(path) {
|
|
if (directories.has(path)) return directories.get(path);
|
|
const index = path.lastIndexOf("/");
|
|
const parent = directory(index < 0 ? "" : path.slice(0, index));
|
|
const node = { name: path.slice(index + 1), children: [] };
|
|
directories.set(path, node);
|
|
parent.children.push(node);
|
|
return node;
|
|
}
|
|
for (const file of files || []) {
|
|
const path = file.path ? `${file.path}/${file.name}` : file.name;
|
|
if (file.size == null) directory(path);
|
|
else directory(file.path || "").children.push({ ...file });
|
|
}
|
|
return root.children;
|
|
}
|
|
|
|
export default {
|
|
name: "FileTree",
|
|
props: ["file", "parent", "searchQuery", "searchResults", "page"],
|
|
setup(props) {
|
|
const route = useRoute();
|
|
const host = ref(null);
|
|
const opens = reactive(Object.create(null));
|
|
let focusedPath = null;
|
|
const hierarchy = computed(() => fileHierarchy(props.file));
|
|
const selectedPath = () => "/" + (Array.isArray(route.params.path) ? route.params.path.join("/") : route.params.path || "");
|
|
watch(() => [route.params.repoId, route.params.path], () => {
|
|
let path = "";
|
|
selectedPath().split("/").filter(Boolean).forEach(part => { path += "/" + part; opens[path] = true; });
|
|
}, { immediate: true });
|
|
const matching = computed(() => {
|
|
if (!props.searchQuery || !props.searchResults) return null;
|
|
const files = new Set(), folders = new Set();
|
|
props.searchResults.forEach(file => {
|
|
files.add(file.path ? `${file.path}/${file.name}` : file.name);
|
|
let path = "";
|
|
(file.path || "").split("/").filter(Boolean).forEach(part => { path = path ? path + "/" + part : part; folders.add(path); });
|
|
});
|
|
return { files, folders };
|
|
});
|
|
const visibleLinks = () => [...host.value.querySelectorAll("li > a")];
|
|
const focus = link => {
|
|
host.value.querySelector(".tree-focused")?.classList.remove("tree-focused");
|
|
if (!link) return;
|
|
focusedPath = link.dataset.path;
|
|
link.classList.add("tree-focused");
|
|
link.scrollIntoView?.({ block: "nearest" });
|
|
host.value.focus();
|
|
};
|
|
function keyboard(event) {
|
|
const links = visibleLinks();
|
|
const current = host.value.querySelector(".tree-focused") || links[0];
|
|
const index = links.indexOf(current);
|
|
if (!["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Enter"].includes(event.key)) return;
|
|
event.preventDefault();
|
|
if (event.key === "ArrowDown") focus(links[(index + 1) % links.length]);
|
|
if (event.key === "ArrowUp") focus(links[(index - 1 + links.length) % links.length]);
|
|
if (!current) return;
|
|
const li = current.closest("li");
|
|
if (event.key === "ArrowRight" && li.classList.contains("folder")) {
|
|
if (!li.classList.contains("open")) current.click();
|
|
else focus(li.querySelector(":scope > ul > li > a"));
|
|
}
|
|
if (event.key === "ArrowLeft") {
|
|
if (li.classList.contains("folder") && li.classList.contains("open")) current.click();
|
|
else focus(li.parentElement.closest("li.folder")?.querySelector(":scope > a"));
|
|
}
|
|
if (event.key === "Enter") current.click();
|
|
}
|
|
function renderNodes(nodes, parent = "") {
|
|
const sorted = [...nodes].sort((a, b) => Number(!!b.children) - Number(!!a.children) || a.name.localeCompare(b.name));
|
|
return h("ul", sorted.flatMap(original => {
|
|
let node = original, name = node.name;
|
|
const originalPath = (parent + "/" + name).slice(1);
|
|
if (matching.value && !matching.value.files.has(originalPath) && !matching.value.folders.has(originalPath)) return [];
|
|
while (node.children?.length === 1) { node = node.children[0]; name += "/" + node.name; }
|
|
const path = parent + "/" + name;
|
|
const folder = !!node.children;
|
|
const open = matching.value ? opens[path] !== false : !!opens[path];
|
|
const truncated = folder && props.page?.options?.truncatedFolders?.includes(path.slice(1));
|
|
const count = props.page?.fileCounts?.[path.slice(1)] || 0;
|
|
const icon = h("span", { class: folder ? "tree-icon-folder" : "tree-icon-file" });
|
|
const label = h("span", { class: "tree-name" }, name);
|
|
const onClick = event => {
|
|
focus(event.currentTarget);
|
|
if (!folder) return;
|
|
event.preventDefault();
|
|
opens[path] = !open;
|
|
if (opens[path] && !node.children.length) props.page.getFiles(path.slice(1));
|
|
nextTick(() => focus(visibleLinks().find(link => link.dataset.path === focusedPath)));
|
|
};
|
|
const link = h("a", {
|
|
"data-path": path,
|
|
href: folder ? undefined : `/r/${encodeURIComponent(route.params.repoId)}${encodePathForUrl(path)}`,
|
|
class: { "tree-focused": focusedPath === path },
|
|
onClick,
|
|
}, [folder ? h("span", { class: "tree-toggle" }) : parent ? h("span", { class: "tree-spacer" }) : null, icon, label,
|
|
truncated ? h("span", { class: "truncated-warning", title: props.page.fmt.translate("WARNINGS.folder_truncated") }, h("i", { class: "fas fa-exclamation-triangle" })) : null,
|
|
folder && count ? h("span", { class: "tree-count" }, count) : null]);
|
|
return h("li", { key: path, class: { file: true, folder, open, active: selectedPath() === path, truncated }, title: folder ? "" : `Size: ${humanFileSize(node.size || 0)}` }, [link, folder && open ? renderNodes(node.children, path) : null]);
|
|
}));
|
|
}
|
|
return () => h("tree", { ref: host, tabindex: 0, onKeydown: keyboard },
|
|
!props.file?.length ? "Empty repository" : matching.value?.files.size === 0 ? h("div", { class: "tree-search-empty" }, "No files found") : renderNodes(hierarchy.value));
|
|
},
|
|
};
|