mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-07 19:28:44 +01:00
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
|
// Copyright 2024, Command Line Inc.
|
||
|
// SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||
|
import { atoms } from "@/store/global";
|
||
|
import * as services from "@/store/services";
|
||
|
import { PrimitiveAtom, atom, useAtom, useAtomValue } from "jotai";
|
||
|
import React, { createRef, useCallback, useEffect, useRef } from "react";
|
||
|
|
||
|
import { Tab } from "./tab";
|
||
|
|
||
|
import "./tabbar.less";
|
||
|
|
||
|
const DEFAULT_TAB_WIDTH = 130;
|
||
|
|
||
|
// Atoms
|
||
|
const tabIdsAtom = atom<string[]>([]);
|
||
|
const tabWidthAtom = atom<number>(DEFAULT_TAB_WIDTH);
|
||
|
const dragStartPositionsAtom = atom<number[]>([]);
|
||
|
const draggingTabAtom = atom<string | null>(null) as PrimitiveAtom<string | null>;
|
||
|
const loadingAtom = atom<boolean>(true);
|
||
|
|
||
|
interface TabBarProps {
|
||
|
workspace: Workspace;
|
||
|
}
|
||
|
|
||
|
const TabBar = ({ workspace }: TabBarProps) => {
|
||
|
const [tabIds, setTabIds] = useAtom(tabIdsAtom);
|
||
|
const [tabWidth, setTabWidth] = useAtom(tabWidthAtom);
|
||
|
const [dragStartPositions, setDragStartPositions] = useAtom(dragStartPositionsAtom);
|
||
|
const [draggingTab, setDraggingTab] = useAtom(draggingTabAtom);
|
||
|
const [loading, setLoading] = useAtom(loadingAtom);
|
||
|
|
||
|
const tabBarRef = useRef<HTMLDivElement>(null);
|
||
|
const tabRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
|
||
|
const addBtnRef = useRef<HTMLDivElement>(null);
|
||
|
const draggingTimeoutId = useRef<NodeJS.Timeout>(null);
|
||
|
const draggingRemovedRef = useRef(false);
|
||
|
const draggingTabDataRef = useRef({
|
||
|
tabId: "",
|
||
|
ref: { current: null },
|
||
|
tabStartX: 0,
|
||
|
tabIndex: 0,
|
||
|
dragged: false,
|
||
|
});
|
||
|
|
||
|
const windowData = useAtomValue(atoms.waveWindow);
|
||
|
const { activetabid } = windowData;
|
||
|
|
||
|
let prevDelta: number;
|
||
|
let prevDragDirection: string;
|
||
|
let shrunk: boolean;
|
||
|
|
||
|
// Update refs when tabIds change
|
||
|
useEffect(() => {
|
||
|
tabRefs.current = tabIds.map((_, index) => tabRefs.current[index] || createRef());
|
||
|
}, [tabIds]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (workspace) {
|
||
|
// Compare current tabIds with new workspace.tabids
|
||
|
const currentTabIds = new Set(tabIds);
|
||
|
const newTabIds = new Set(workspace.tabids);
|
||
|
|
||
|
const areEqual =
|
||
|
currentTabIds.size === newTabIds.size && [...currentTabIds].every((id) => newTabIds.has(id));
|
||
|
|
||
|
if (!areEqual) {
|
||
|
setTabIds(workspace.tabids);
|
||
|
}
|
||
|
setLoading(false);
|
||
|
}
|
||
|
}, [workspace, tabIds, setTabIds, setLoading]);
|
||
|
|
||
|
const updateTabPositions = useCallback(() => {
|
||
|
if (tabBarRef.current) {
|
||
|
const newStartPositions: number[] = [];
|
||
|
let cumulativeLeft = 0; // Start from the left edge
|
||
|
|
||
|
tabRefs.current.forEach((ref) => {
|
||
|
if (ref.current) {
|
||
|
newStartPositions.push(cumulativeLeft);
|
||
|
cumulativeLeft += ref.current.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
||
|
}
|
||
|
});
|
||
|
|
||
|
setDragStartPositions(newStartPositions);
|
||
|
}
|
||
|
}, [tabRefs.current, setDragStartPositions]);
|
||
|
|
||
|
const handleResizeTabs = useCallback(() => {
|
||
|
const tabBar = tabBarRef.current;
|
||
|
if (!tabBar) return;
|
||
|
|
||
|
const containerWidth = tabBar.getBoundingClientRect().width;
|
||
|
const numberOfTabs = tabIds.length;
|
||
|
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||
|
let newTabWidth = DEFAULT_TAB_WIDTH;
|
||
|
|
||
|
if (totalDefaultTabWidth > containerWidth) {
|
||
|
newTabWidth = containerWidth / numberOfTabs;
|
||
|
shrunk = true;
|
||
|
} else {
|
||
|
shrunk = false;
|
||
|
}
|
||
|
|
||
|
// Apply the calculated width and position to all tabs
|
||
|
tabRefs.current.forEach((ref, index) => {
|
||
|
if (ref.current) {
|
||
|
ref.current.style.width = `${newTabWidth}px`;
|
||
|
ref.current.style.transform = `translateX(${index * newTabWidth}px)`;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Update the state with the new tab width if it has changed
|
||
|
if (newTabWidth !== tabWidth) {
|
||
|
setTabWidth(newTabWidth);
|
||
|
}
|
||
|
|
||
|
// Update the position of the Add Tab button if needed
|
||
|
const addButton = addBtnRef.current;
|
||
|
const lastTabRef = tabRefs.current[tabRefs.current.length - 1];
|
||
|
if (addButton && lastTabRef && lastTabRef.current) {
|
||
|
const lastTabRect = lastTabRef.current.getBoundingClientRect();
|
||
|
addButton.style.position = "absolute";
|
||
|
addButton.style.transform = `translateX(${lastTabRect.right}px) translateY(-50%)`;
|
||
|
}
|
||
|
}, [tabIds, tabWidth, updateTabPositions, setTabWidth]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
window.addEventListener("resize", handleResizeTabs);
|
||
|
return () => {
|
||
|
window.removeEventListener("resize", handleResizeTabs);
|
||
|
};
|
||
|
}, [handleResizeTabs]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (!loading) {
|
||
|
handleResizeTabs();
|
||
|
updateTabPositions();
|
||
|
}
|
||
|
}, [loading, handleResizeTabs, updateTabPositions]);
|
||
|
|
||
|
// Make sure timeouts are cleared when component is unmounted
|
||
|
useEffect(() => {
|
||
|
return () => {
|
||
|
if (draggingTimeoutId.current) {
|
||
|
clearTimeout(draggingTimeoutId.current);
|
||
|
}
|
||
|
};
|
||
|
}, []);
|
||
|
|
||
|
const handleMouseMove = (event: MouseEvent) => {
|
||
|
const { tabId, ref, tabStartX } = draggingTabDataRef.current;
|
||
|
|
||
|
let tabIndex = draggingTabDataRef.current.tabIndex;
|
||
|
let currentX = event.clientX - ref.current.getBoundingClientRect().width / 2;
|
||
|
|
||
|
// Check if the tab has moved 5 pixels
|
||
|
if (Math.abs(currentX - tabStartX) >= 5) {
|
||
|
setDraggingTab(tabId);
|
||
|
draggingTabDataRef.current.dragged = true;
|
||
|
}
|
||
|
|
||
|
// Constrain movement within the container bounds
|
||
|
if (tabBarRef.current) {
|
||
|
const numberOfTabs = tabIds.length;
|
||
|
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||
|
const containerRect = tabBarRef.current.getBoundingClientRect();
|
||
|
let containerRectWidth = containerRect.width;
|
||
|
// Set to the total default tab width if there's vacant space
|
||
|
if (totalDefaultTabWidth < containerRectWidth) {
|
||
|
containerRectWidth = totalDefaultTabWidth;
|
||
|
}
|
||
|
|
||
|
const minLeft = 0;
|
||
|
const maxRight = containerRectWidth - tabWidth;
|
||
|
|
||
|
// Adjust currentX to stay within bounds
|
||
|
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
|
||
|
}
|
||
|
|
||
|
ref.current!.style.transform = `translateX(${currentX}px)`;
|
||
|
ref.current!.style.zIndex = "100";
|
||
|
|
||
|
let dragDirection;
|
||
|
if (currentX - prevDelta > 0) {
|
||
|
dragDirection = "+";
|
||
|
} else if (currentX - prevDelta === 0) {
|
||
|
dragDirection = prevDragDirection;
|
||
|
} else {
|
||
|
dragDirection = "-";
|
||
|
}
|
||
|
prevDelta = currentX;
|
||
|
prevDragDirection = dragDirection;
|
||
|
|
||
|
let newTabIndex = tabIndex;
|
||
|
|
||
|
if (dragDirection === "+") {
|
||
|
// Dragging to the right
|
||
|
for (let i = tabIndex + 1; i < tabIds.length; i++) {
|
||
|
const otherTabStart = dragStartPositions[i];
|
||
|
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
|
||
|
newTabIndex = i;
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// Dragging to the left
|
||
|
for (let i = tabIndex - 1; i >= 0; i--) {
|
||
|
const otherTabEnd = dragStartPositions[i] + tabWidth;
|
||
|
if (currentX < otherTabEnd - tabWidth / 2) {
|
||
|
newTabIndex = i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (newTabIndex !== tabIndex) {
|
||
|
// Remove the dragged tab if not already done
|
||
|
if (!draggingRemovedRef.current) {
|
||
|
tabIds.splice(tabIndex, 1);
|
||
|
draggingRemovedRef.current = true;
|
||
|
}
|
||
|
|
||
|
// Find current index of the dragged tab in tempTabs
|
||
|
const currentIndexOfDraggingTab = tabIds.indexOf(tabId);
|
||
|
|
||
|
// Move the dragged tab to its new position
|
||
|
if (currentIndexOfDraggingTab !== -1) {
|
||
|
tabIds.splice(currentIndexOfDraggingTab, 1);
|
||
|
}
|
||
|
tabIds.splice(newTabIndex, 0, tabId);
|
||
|
|
||
|
// Update visual positions of the tabs
|
||
|
tabIds.forEach((localTabId, index) => {
|
||
|
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === localTabId);
|
||
|
if (ref.current && localTabId !== tabId) {
|
||
|
ref.current.style.transform = `translateX(${index * tabWidth}px)`;
|
||
|
ref.current.classList.add("animate");
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tabIndex = newTabIndex;
|
||
|
draggingTabDataRef.current.tabIndex = newTabIndex;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const handleMouseUp = (event: MouseEvent) => {
|
||
|
const { tabIndex, dragged } = draggingTabDataRef.current;
|
||
|
|
||
|
// Update the final position of the dragged tab
|
||
|
const draggingTab = tabIds[tabIndex];
|
||
|
const finalLeftPosition = tabIndex * tabWidth;
|
||
|
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === draggingTab);
|
||
|
if (ref.current) {
|
||
|
ref.current.classList.add("animate");
|
||
|
ref.current.style.transform = `translateX(${finalLeftPosition}px)`;
|
||
|
}
|
||
|
|
||
|
if (dragged) {
|
||
|
draggingTimeoutId.current = setTimeout(() => {
|
||
|
// Reset styles
|
||
|
tabRefs.current.forEach((ref) => {
|
||
|
ref.current.style.zIndex = "0";
|
||
|
ref.current.classList.remove("animate");
|
||
|
});
|
||
|
// Reset dragging state
|
||
|
setDraggingTab(null);
|
||
|
// Update workspace tab ids
|
||
|
services.ObjectService.UpdateWorkspaceTabIds(workspace.oid, tabIds);
|
||
|
}, 300);
|
||
|
}
|
||
|
|
||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||
|
draggingRemovedRef.current = false;
|
||
|
};
|
||
|
|
||
|
const handleDragStart = useCallback(
|
||
|
(name: string, ref: React.RefObject<HTMLDivElement>) => {
|
||
|
const tabIndex = tabIds.indexOf(name);
|
||
|
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
||
|
|
||
|
if (ref.current) {
|
||
|
draggingTabDataRef.current = {
|
||
|
tabId: ref.current.dataset.tabId,
|
||
|
ref,
|
||
|
tabStartX,
|
||
|
tabIndex,
|
||
|
dragged: false,
|
||
|
};
|
||
|
|
||
|
document.addEventListener("mousemove", handleMouseMove);
|
||
|
document.addEventListener("mouseup", handleMouseUp);
|
||
|
|
||
|
if (draggingTimeoutId.current) {
|
||
|
clearTimeout(draggingTimeoutId.current);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
[tabIds, dragStartPositions, tabWidth]
|
||
|
);
|
||
|
|
||
|
const handleSelectTab = (tabId: string) => {
|
||
|
if (!draggingTabDataRef.current.dragged) {
|
||
|
services.ObjectService.SetActiveTab(tabId);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const handleAddTab = () => {
|
||
|
const newTabName = `T${tabIds.length + 1}`;
|
||
|
setTabIds([...tabIds, newTabName]);
|
||
|
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
||
|
};
|
||
|
|
||
|
const handleCloseTab = (tabId: string) => {
|
||
|
services.ObjectService.CloseTab(tabId);
|
||
|
deleteLayoutStateAtomForTab(tabId);
|
||
|
};
|
||
|
|
||
|
const isBeforeActive = (tabId: string) => {
|
||
|
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<div className="tab-bar-wrapper">
|
||
|
<div className="tab-bar" ref={tabBarRef}>
|
||
|
{tabIds.map((tabId, index) => (
|
||
|
<Tab
|
||
|
key={tabId}
|
||
|
ref={tabRefs.current[index]}
|
||
|
id={tabId}
|
||
|
onSelect={() => handleSelectTab(tabId)}
|
||
|
active={activetabid === tabId}
|
||
|
onDragStart={() => handleDragStart(tabId, tabRefs.current[index])}
|
||
|
onClose={() => handleCloseTab(tabId)}
|
||
|
isBeforeActive={isBeforeActive(tabId)}
|
||
|
isDragging={draggingTab === tabId}
|
||
|
/>
|
||
|
))}
|
||
|
</div>
|
||
|
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
|
||
|
<i className="fa fa-solid fa-plus fa-fw" />
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export { TabBar };
|