/
uzer_007
/
Rixea
Обзор
Документация
Войти
/
uzer_007
/
Rixea
Код
Запросы
0
Задачи
Вики
Пакеты
5
Релизы
2
CI/CD
Аналитика
Безопасность
master
web_src/js/components/ViewFileTreeItem.vue
160 строк
5 KB
uzer_007
feat: первая версия Rixea
02 авг 2026, 22:16
02 авг 2026, 22:16
d4f7504
Код
Авторство
О чём код?
<script lang="ts" setup> import {SvgIcon} from '../svg.ts'; import {isPlainClick} from '../utils/dom.ts'; import {shouldTriggerAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {nextTick, shallowRef, useTemplateRef, type ShallowRef} from 'vue'; import type {createViewFileTreeStore, FileTreeItem} from './ViewFileTreeStore.ts'; const props = defineProps<{ item: FileTreeItem, store: ReturnType<typeof createViewFileTreeStore> }>(); const store = props.store; const isLoading = shallowRef(false); const hasLoadError = shallowRef(false); const children = shallowRef(props.item.children); const collapsed = shallowRef(!props.item.children); const itemLink = useTemplateRef('itemLink') as Readonly<ShallowRef<HTMLAnchorElement>>; const retryButton = useTemplateRef('retryButton') as Readonly<ShallowRef<HTMLButtonElement | null>>; const loadChildren = async () => { isLoading.value = true; hasLoadError.value = false; try { children.value = await store.loadChildren(props.item.fullPath); } catch { hasLoadError.value = true; } finally { isLoading.value = false; } }; const doLoadChildren = async () => { if (isLoading.value) return; collapsed.value = !collapsed.value; if (!collapsed.value && (children.value === undefined || hasLoadError.value)) await loadChildren(); }; const retryLoadChildren = async () => { if (isLoading.value) return; await loadChildren(); await nextTick(); if (hasLoadError.value) { retryButton.value?.focus(); } else { itemLink.value.focus(); } }; const onItemClick = (e: MouseEvent) => { // only handle the click event with partial page reloading if both // - the user didn't press any special key like "Ctrl+Click" (which may have custom browser behavior) // - the editor/commit form isn't dirty (a full page reload shows a confirmation dialog if the form contains unsaved changes) if (!isPlainClick(e) || shouldTriggerAreYouSure()) return; e.preventDefault(); if (props.item.entryMode === 'tree') void doLoadChildren(); void store.navigateTreeView(props.item.fullPath); }; </script> <template> <a ref="itemLink" 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.buildTreePathWebUrl(item.fullPath)" :aria-expanded="item.entryMode === 'tree' ? !collapsed : undefined" :aria-busy="item.entryMode === 'tree' && isLoading ? true : undefined" @click.stop="onItemClick" > <div v-if="item.entryMode === 'tree'" class="item-toggle"> <SvgIcon v-if="isLoading" name="gitea-running" class="rotate-clockwise"/> <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 --> <span class="tw-contents" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/> <span class="gt-ellipsis">{{ item.entryName }}</span> </div> </a> <div v-if="!collapsed && hasLoadError" class="file-tree-child-error" role="alert"> <span>{{ store.loadErrorText }}</span> <button ref="retryButton" type="button" class="ui mini compact basic button" @click="retryLoadChildren"> {{ store.retryText }} </button> </div> <div v-if="children?.length" v-show="!collapsed" class="sub-items"> <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/> </div> </template> <style scoped> .sub-items { display: flex; flex-direction: column; gap: 1px; margin-left: 14px; border-left: 1px solid var(--color-secondary); } .file-tree-child-error { display: flex; margin-left: 14px; padding: 0.5rem; border-left: 1px solid var(--color-secondary); flex-direction: column; align-items: flex-start; gap: 0.5rem; } .tree-item.selected { color: var(--color-text); background: var(--color-active); border-radius: 4px; } .tree-item.type-directory { user-select: none; } .tree-item { display: grid; grid-template-columns: 16px 1fr; grid-template-areas: "toggle content"; gap: 0.25em; padding: 6px; } .tree-item:hover { color: var(--color-text); background: var(--color-hover); border-radius: 4px; cursor: pointer; } .item-toggle { grid-area: toggle; display: flex; align-items: center; } .item-content { grid-area: content; display: flex; align-items: center; gap: 0.5em; text-overflow: ellipsis; min-width: 0; } </style>