diff --git a/src/App.svelte b/src/App.svelte index 067eef3..a7fd05f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2799,6 +2799,19 @@ return repoTabs.findIndex((tab) => sameRepoPath(tab.path, path)); } + function reorderRepoTab(path: string, targetPath: string, after: boolean) { + if (isBusy || sameRepoPath(path, targetPath)) return; + const sourceIndex = repoTabIndex(path); + if (sourceIndex < 0 || repoTabIndex(targetPath) < 0) return; + const reordered = [...repoTabs]; + const [moved] = reordered.splice(sourceIndex, 1); + const targetIndex = reordered.findIndex(tab => sameRepoPath(tab.path, targetPath)); + reordered.splice(targetIndex + (after ? 1 : 0), 0, moved); + repoTabs = reordered; + closeRepoTabContextMenu(); + persistRepoLists(); + } + function closeRepoTabContextMenu() { repoTabContextMenu = null; } @@ -5405,6 +5418,7 @@ onOpenIssues={() => { activeView = "issues"; }} isActive={(path) => activeView === "repository" && sameRepoPath(activeRepoPath, path)} onSelect={selectRepoTab} + onReorder={reorderRepoTab} onClose={closeRepoTab} onContextMenu={openRepoTabContextMenu} onAdd={chooseRepositoryFolder} diff --git a/src/lib/RepoTabs.svelte b/src/lib/RepoTabs.svelte index 2f1351b..3674d20 100644 --- a/src/lib/RepoTabs.svelte +++ b/src/lib/RepoTabs.svelte @@ -20,8 +20,67 @@ export let onClose: (path: string, event: MouseEvent) => void | Promise = () => {}; export let onContextMenu: (path: string, event: MouseEvent) => void = () => {}; export let onAdd: () => void | Promise = () => {}; + export let onReorder: (path: string, targetPath: string, after: boolean) => void = () => {}; + + let navigation: HTMLElement; + let drag: { path: string; pointerId: number; startX: number; startScroll: number; source: number; target: number; width: number; centers: number[]; element: HTMLElement } | null = null; + let dragging = false; + let offset = 0; + let suppressClick = false; + + function startDrag(event: PointerEvent, path: string) { + if (isBusy || event.button !== 0 || !event.isPrimary) return; + const element = event.currentTarget as HTMLElement; + const tabs = Array.from(navigation.querySelectorAll(".repository-tab")); + const source = repoTabs.findIndex(tab => tab.path === path); + drag = { path, pointerId: event.pointerId, startX: event.clientX, + startScroll: navigation.scrollLeft, source, target: source, + width: tabs[source].getBoundingClientRect().width + 4, + centers: tabs.map(tab => { const rect = tab.getBoundingClientRect(); return rect.left + rect.width / 2; }), element }; + suppressClick = false; + element.setPointerCapture(event.pointerId); + } + + function moveDrag(event: PointerEvent) { + if (!drag || event.pointerId !== drag.pointerId) return; + if (isBusy) { finishDrag(false); return; } + if (!dragging && Math.abs(event.clientX - drag.startX) < 5) return; + dragging = true; + suppressClick = true; + const bounds = navigation.getBoundingClientRect(); + if (event.clientX < bounds.left + 35) navigation.scrollLeft -= 15; + if (event.clientX > bounds.right - 35) navigation.scrollLeft += 15; + offset = event.clientX - drag.startX + navigation.scrollLeft - drag.startScroll; + const center = drag.centers[drag.source] + offset; + let target = drag.source; + while (target < drag.centers.length - 1 && center > drag.centers[target + 1]) target++; + while (target > 0 && center < drag.centers[target - 1]) target--; + drag = { ...drag, target }; + } + + function finishDrag(commit: boolean) { + const current = drag; + if (!current) return; + drag = null; + if (current.element.hasPointerCapture(current.pointerId)) current.element.releasePointerCapture(current.pointerId); + if (commit && dragging && !isBusy && current.target !== current.source) { + onReorder(current.path, repoTabs[current.target].path, current.target > current.source); + } + dragging = false; + offset = 0; + } + + function tabOffset(index: number, current: typeof drag, moving: boolean, displacement: number) { + if (!current || !moving) return 0; + if (index === current.source) return displacement; + if (current.source < index && index <= current.target) return -current.width; + if (current.target <= index && index < current.source) return current.width; + return 0; + } + { if (event.pointerId === drag?.pointerId) finishDrag(true); }} onpointercancel={() => finishDrag(false)} onblur={() => finishDrag(false)} onkeydown={(event) => { if (event.key === "Escape") finishDrag(false); }}/> +
{#if activeView === "repository"} -