mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
Scrollable tabs using OverlayScrollbars (#60)
This commit is contained in:
@@ -60,3 +60,10 @@ body {
|
|||||||
.error-boundary {
|
.error-boundary {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* OverlayScrollbars styling */
|
||||||
|
.os-scrollbar {
|
||||||
|
--os-handle-bg: var(--scrollbar-thumb-color);
|
||||||
|
--os-handle-bg-hover: var(--scrollbar-thumb-hover-color);
|
||||||
|
--os-handle-bg-active: var(--scrollbar-thumb-active-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { Provider } from "jotai";
|
|||||||
|
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import "./app.less";
|
|
||||||
import { CenteredDiv } from "./element/quickelems";
|
import { CenteredDiv } from "./element/quickelems";
|
||||||
|
|
||||||
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
|
import "./app.less";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<Provider store={globalStore}>
|
<Provider store={globalStore}>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { Button } from "@/element/button";
|
import { Button } from "@/element/button";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import "./tab.less";
|
import "./tab.less";
|
||||||
|
|
||||||
@@ -16,13 +16,23 @@ interface TabProps {
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDragStart: () => void;
|
onDragStart: () => void;
|
||||||
|
onLoaded: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
||||||
({ id, active, isBeforeActive, isDragging, onSelect, onClose, onDragStart }, ref) => {
|
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
||||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
||||||
const name = tabData?.name ?? "...";
|
const name = tabData?.name ?? "...";
|
||||||
|
|
||||||
|
const loadedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRef.current) {
|
||||||
|
onLoaded();
|
||||||
|
loadedRef.current = true;
|
||||||
|
}
|
||||||
|
}, [onLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
position: relative; // Needed for absolute positioning of child tabs
|
position: relative; // Needed for absolute positioning of child tabs
|
||||||
min-height: 34px; // Adjust as necessary to fit the height of tabs
|
min-height: 34px; // Adjust as necessary to fit the height of tabs
|
||||||
width: calc(100vw - 36px); // 36 is the width of add tab button
|
width: calc(100vw - 36px); // 36 is the width of add tab button
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-tab-btn {
|
.add-tab-btn {
|
||||||
@@ -20,13 +19,24 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%); // overridden in js
|
transform: translateY(-50%); // overridden in js
|
||||||
border-radius: 100%;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 32px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize scrollbar styles
|
||||||
|
.os-theme-dark,
|
||||||
|
.os-theme-light {
|
||||||
|
box-sizing: border-box;
|
||||||
|
--os-size: 3px;
|
||||||
|
--os-padding-perpendicular: 0px;
|
||||||
|
--os-padding-axis: 0px;
|
||||||
|
--os-track-border-radius: 3px;
|
||||||
|
--os-handle-interactive-area-offset: 0px;
|
||||||
|
--os-handle-border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+201
-102
@@ -2,39 +2,54 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||||
|
import { debounce } from "@/faraday/lib/utils";
|
||||||
import { atoms } from "@/store/global";
|
import { atoms } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import { PrimitiveAtom, atom, useAtom, useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, { createRef, useCallback, useEffect, useRef } from "react";
|
import { OverlayScrollbars } from "overlayscrollbars";
|
||||||
|
import React, { createRef, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Tab } from "./tab";
|
import { Tab } from "./tab";
|
||||||
|
|
||||||
import "./tabbar.less";
|
import "./tabbar.less";
|
||||||
|
|
||||||
const DEFAULT_TAB_WIDTH = 130;
|
const TAB_DEFAULT_WIDTH = 130;
|
||||||
|
const TAB_MIN_WIDTH = 100;
|
||||||
// Atoms
|
const OS_OPTIONS = {
|
||||||
const tabIdsAtom = atom<string[]>([]);
|
overflow: {
|
||||||
const tabWidthAtom = atom<number>(DEFAULT_TAB_WIDTH);
|
x: "scroll",
|
||||||
const dragStartPositionsAtom = atom<number[]>([]);
|
y: "hidden",
|
||||||
const draggingTabAtom = atom<string | null>(null) as PrimitiveAtom<string | null>;
|
},
|
||||||
const loadingAtom = atom<boolean>(true);
|
scrollbars: {
|
||||||
|
theme: "os-theme-dark",
|
||||||
|
visibility: "auto",
|
||||||
|
autoHide: "leave",
|
||||||
|
autoHideDelay: 1300,
|
||||||
|
autoHideSuspend: false,
|
||||||
|
dragScroll: true,
|
||||||
|
clickScroll: false,
|
||||||
|
pointers: ["mouse", "touch", "pen"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface TabBarProps {
|
interface TabBarProps {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabBar = ({ workspace }: TabBarProps) => {
|
const TabBar = ({ workspace }: TabBarProps) => {
|
||||||
const [tabIds, setTabIds] = useAtom(tabIdsAtom);
|
const [tabIds, setTabIds] = useState<string[]>([]);
|
||||||
const [tabWidth, setTabWidth] = useAtom(tabWidthAtom);
|
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||||
const [dragStartPositions, setDragStartPositions] = useAtom(dragStartPositionsAtom);
|
const [draggingTab, setDraggingTab] = useState<string>();
|
||||||
const [draggingTab, setDraggingTab] = useAtom(draggingTabAtom);
|
const [tabsLoaded, setTabsLoaded] = useState({});
|
||||||
const [loading, setLoading] = useAtom(loadingAtom);
|
const [scrollable, setScrollable] = useState(false);
|
||||||
|
const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH);
|
||||||
|
|
||||||
const tabBarRef = useRef<HTMLDivElement>(null);
|
const tabBarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tabsWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const tabRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
|
const tabRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
|
||||||
const addBtnRef = useRef<HTMLDivElement>(null);
|
const addBtnRef = useRef<HTMLDivElement>(null);
|
||||||
const draggingTimeoutId = useRef<NodeJS.Timeout>(null);
|
const draggingTimeoutIdRef = useRef<NodeJS.Timeout>(null);
|
||||||
|
const scrollToNewTabTimeoutIdRef = useRef<NodeJS.Timeout>(null);
|
||||||
const draggingRemovedRef = useRef(false);
|
const draggingRemovedRef = useRef(false);
|
||||||
const draggingTabDataRef = useRef({
|
const draggingTabDataRef = useRef({
|
||||||
tabId: "",
|
tabId: "",
|
||||||
@@ -43,13 +58,13 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
dragged: false,
|
dragged: false,
|
||||||
});
|
});
|
||||||
|
const osInstanceRef = useRef<OverlayScrollbars>(null);
|
||||||
|
|
||||||
const windowData = useAtomValue(atoms.waveWindow);
|
const windowData = useAtomValue(atoms.waveWindow);
|
||||||
const { activetabid } = windowData;
|
const { activetabid } = windowData;
|
||||||
|
|
||||||
let prevDelta: number;
|
let prevDelta: number;
|
||||||
let prevDragDirection: string;
|
let prevDragDirection: string;
|
||||||
let shrunk: boolean;
|
|
||||||
|
|
||||||
// Update refs when tabIds change
|
// Update refs when tabIds change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,40 +83,53 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
if (!areEqual) {
|
if (!areEqual) {
|
||||||
setTabIds(workspace.tabids);
|
setTabIds(workspace.tabids);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [workspace, tabIds, setTabIds, setLoading]);
|
}, [workspace, tabIds]);
|
||||||
|
|
||||||
const updateTabPositions = useCallback(() => {
|
const updateTabPositions = useCallback(() => {
|
||||||
if (tabBarRef.current) {
|
const tabs = tabRefs.current;
|
||||||
const newStartPositions: number[] = [];
|
if (tabs === null) return;
|
||||||
let cumulativeLeft = 0; // Start from the left edge
|
|
||||||
|
|
||||||
tabRefs.current.forEach((ref) => {
|
const newStartPositions: number[] = [];
|
||||||
if (ref.current) {
|
let cumulativeLeft = 0; // Start from the left edge
|
||||||
newStartPositions.push(cumulativeLeft);
|
|
||||||
cumulativeLeft += ref.current.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setDragStartPositions(newStartPositions);
|
tabRefs.current.forEach((ref) => {
|
||||||
}
|
if (ref.current) {
|
||||||
}, [tabRefs.current, setDragStartPositions]);
|
newStartPositions.push(cumulativeLeft);
|
||||||
|
cumulativeLeft += ref.current.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setDragStartPositions(newStartPositions);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100);
|
||||||
|
const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100);
|
||||||
|
const debouncedUpdateTabPositions = debounce(() => updateTabPositions(), 100);
|
||||||
|
|
||||||
const handleResizeTabs = useCallback(() => {
|
const handleResizeTabs = useCallback(() => {
|
||||||
const tabBar = tabBarRef.current;
|
const tabBar = tabBarRef.current;
|
||||||
if (!tabBar) return;
|
if (tabBar === null) return;
|
||||||
|
|
||||||
const containerWidth = tabBar.getBoundingClientRect().width;
|
const tabBarWidth = tabBar.getBoundingClientRect().width;
|
||||||
const numberOfTabs = tabIds.length;
|
const numberOfTabs = tabIds.length;
|
||||||
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
||||||
let newTabWidth = DEFAULT_TAB_WIDTH;
|
const minTotalTabWidth = numberOfTabs * TAB_MIN_WIDTH;
|
||||||
|
let newTabWidth = tabWidth;
|
||||||
|
let newScrollable = scrollable;
|
||||||
|
|
||||||
if (totalDefaultTabWidth > containerWidth) {
|
if (minTotalTabWidth > tabBarWidth) {
|
||||||
newTabWidth = containerWidth / numberOfTabs;
|
// Case where tabs cannot shrink further, make the tab bar scrollable
|
||||||
shrunk = true;
|
newTabWidth = TAB_MIN_WIDTH;
|
||||||
|
newScrollable = true;
|
||||||
|
} else if (totalDefaultTabWidth > tabBarWidth) {
|
||||||
|
// Case where resizing is needed due to limited container width
|
||||||
|
newTabWidth = tabBarWidth / numberOfTabs;
|
||||||
|
newScrollable = false;
|
||||||
} else {
|
} else {
|
||||||
shrunk = false;
|
// Case where tabs were previously shrunk or there is enough space for default width tabs
|
||||||
|
newTabWidth = TAB_DEFAULT_WIDTH;
|
||||||
|
newScrollable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the calculated width and position to all tabs
|
// Apply the calculated width and position to all tabs
|
||||||
@@ -114,7 +142,19 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
|
|
||||||
// Update the state with the new tab width if it has changed
|
// Update the state with the new tab width if it has changed
|
||||||
if (newTabWidth !== tabWidth) {
|
if (newTabWidth !== tabWidth) {
|
||||||
setTabWidth(newTabWidth);
|
debouncedSetTabWidth(newTabWidth);
|
||||||
|
}
|
||||||
|
// Update the state with the new scrollable state if it has changed
|
||||||
|
if (newScrollable !== scrollable) {
|
||||||
|
debouncedSetScrollable(newScrollable);
|
||||||
|
}
|
||||||
|
// Initialize/destroy overlay scrollbars
|
||||||
|
if (newScrollable) {
|
||||||
|
osInstanceRef.current = OverlayScrollbars(tabBarRef.current, { ...(OS_OPTIONS as any) });
|
||||||
|
} else {
|
||||||
|
if (osInstanceRef.current) {
|
||||||
|
osInstanceRef.current.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the position of the Add Tab button if needed
|
// Update the position of the Add Tab button if needed
|
||||||
@@ -123,66 +163,45 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
if (addButton && lastTabRef && lastTabRef.current) {
|
if (addButton && lastTabRef && lastTabRef.current) {
|
||||||
const lastTabRect = lastTabRef.current.getBoundingClientRect();
|
const lastTabRect = lastTabRef.current.getBoundingClientRect();
|
||||||
addButton.style.position = "absolute";
|
addButton.style.position = "absolute";
|
||||||
addButton.style.transform = `translateX(${lastTabRect.right}px) translateY(-50%)`;
|
if (newScrollable) {
|
||||||
|
addButton.style.transform = `translateX(${document.documentElement.clientWidth - addButton.offsetWidth}px) translateY(-50%)`;
|
||||||
|
} else {
|
||||||
|
addButton.style.transform = `translateX(${lastTabRect.right + 1}px) translateY(-50%)`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [tabIds, tabWidth, updateTabPositions, setTabWidth]);
|
|
||||||
|
debouncedUpdateTabPositions();
|
||||||
|
}, [tabIds, tabWidth, scrollable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("resize", handleResizeTabs);
|
window.addEventListener("resize", () => handleResizeTabs());
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", handleResizeTabs);
|
window.removeEventListener("resize", () => handleResizeTabs());
|
||||||
};
|
};
|
||||||
}, [handleResizeTabs]);
|
}, [handleResizeTabs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
// Check if all tabs are loaded
|
||||||
handleResizeTabs();
|
const allLoaded = tabIds.length > 0 && tabIds.every((id) => tabsLoaded[id]);
|
||||||
|
if (allLoaded) {
|
||||||
updateTabPositions();
|
updateTabPositions();
|
||||||
|
handleResizeTabs();
|
||||||
}
|
}
|
||||||
}, [loading, handleResizeTabs, updateTabPositions]);
|
}, [tabIds, tabsLoaded, handleResizeTabs, updateTabPositions]);
|
||||||
|
|
||||||
// Make sure timeouts are cleared when component is unmounted
|
// Make sure timeouts are cleared when component is unmounted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (draggingTimeoutId.current) {
|
if (draggingTimeoutIdRef.current) {
|
||||||
clearTimeout(draggingTimeoutId.current);
|
clearTimeout(draggingTimeoutIdRef.current);
|
||||||
|
}
|
||||||
|
if (scrollToNewTabTimeoutIdRef.current) {
|
||||||
|
clearTimeout(scrollToNewTabTimeoutIdRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const getDragDirection = (currentX: number) => {
|
||||||
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;
|
let dragDirection;
|
||||||
if (currentX - prevDelta > 0) {
|
if (currentX - prevDelta > 0) {
|
||||||
dragDirection = "+";
|
dragDirection = "+";
|
||||||
@@ -193,9 +212,11 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
prevDelta = currentX;
|
prevDelta = currentX;
|
||||||
prevDragDirection = dragDirection;
|
prevDragDirection = dragDirection;
|
||||||
|
return dragDirection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewTabIndex = (currentX: number, tabIndex: number, dragDirection: string) => {
|
||||||
let newTabIndex = tabIndex;
|
let newTabIndex = tabIndex;
|
||||||
|
|
||||||
if (dragDirection === "+") {
|
if (dragDirection === "+") {
|
||||||
// Dragging to the right
|
// Dragging to the right
|
||||||
for (let i = tabIndex + 1; i < tabIds.length; i++) {
|
for (let i = tabIndex + 1; i < tabIds.length; i++) {
|
||||||
@@ -213,6 +234,63 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return newTabIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const { tabId, ref, tabStartX } = draggingTabDataRef.current;
|
||||||
|
|
||||||
|
let currentX = event.clientX - ref.current.getBoundingClientRect().width / 2;
|
||||||
|
let tabBarRectWidth = tabBarRef.current.getBoundingClientRect().width;
|
||||||
|
const dragDirection = getDragDirection(currentX);
|
||||||
|
|
||||||
|
// Scroll the tab bar if the dragged tab overflows the container bounds
|
||||||
|
if (scrollable) {
|
||||||
|
const { viewport } = osInstanceRef.current.elements();
|
||||||
|
const { overflowAmount } = osInstanceRef.current.state();
|
||||||
|
const { scrollOffsetElement } = osInstanceRef.current.elements();
|
||||||
|
|
||||||
|
if (event.clientX <= 0) {
|
||||||
|
viewport.scrollLeft = 0;
|
||||||
|
} else if (event.clientX >= tabBarRectWidth) {
|
||||||
|
viewport.scrollLeft = tabBarRectWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollOffsetElement.scrollLeft > 0) {
|
||||||
|
currentX += overflowAmount.x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tab has moved 5 pixels
|
||||||
|
if (Math.abs(currentX - tabStartX) >= 5) {
|
||||||
|
setDraggingTab((prev) => (prev !== tabId ? tabId : prev));
|
||||||
|
draggingTabDataRef.current.dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constrain movement within the container bounds
|
||||||
|
if (tabBarRef.current) {
|
||||||
|
const numberOfTabs = tabIds.length;
|
||||||
|
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
||||||
|
if (totalDefaultTabWidth < tabBarRectWidth) {
|
||||||
|
// Set to the total default tab width if there's vacant space
|
||||||
|
tabBarRectWidth = totalDefaultTabWidth;
|
||||||
|
} else if (scrollable) {
|
||||||
|
// Set to the scrollable width if the tab bar is scrollable
|
||||||
|
tabBarRectWidth = tabsWrapperRef.current.scrollWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minLeft = 0;
|
||||||
|
const maxRight = tabBarRectWidth - 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";
|
||||||
|
|
||||||
|
const tabIndex = draggingTabDataRef.current.tabIndex;
|
||||||
|
const newTabIndex = getNewTabIndex(currentX, tabIndex, dragDirection);
|
||||||
|
|
||||||
if (newTabIndex !== tabIndex) {
|
if (newTabIndex !== tabIndex) {
|
||||||
// Remove the dragged tab if not already done
|
// Remove the dragged tab if not already done
|
||||||
@@ -239,7 +317,6 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tabIndex = newTabIndex;
|
|
||||||
draggingTabDataRef.current.tabIndex = newTabIndex;
|
draggingTabDataRef.current.tabIndex = newTabIndex;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -257,7 +334,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dragged) {
|
if (dragged) {
|
||||||
draggingTimeoutId.current = setTimeout(() => {
|
draggingTimeoutIdRef.current = setTimeout(() => {
|
||||||
// Reset styles
|
// Reset styles
|
||||||
tabRefs.current.forEach((ref) => {
|
tabRefs.current.forEach((ref) => {
|
||||||
ref.current.style.zIndex = "0";
|
ref.current.style.zIndex = "0";
|
||||||
@@ -292,12 +369,12 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
if (draggingTimeoutId.current) {
|
if (draggingTimeoutIdRef.current) {
|
||||||
clearTimeout(draggingTimeoutId.current);
|
clearTimeout(draggingTimeoutIdRef.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tabIds, dragStartPositions, tabWidth]
|
[tabIds, dragStartPositions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectTab = (tabId: string) => {
|
const handleSelectTab = (tabId: string) => {
|
||||||
@@ -310,6 +387,13 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
const newTabName = `T${tabIds.length + 1}`;
|
const newTabName = `T${tabIds.length + 1}`;
|
||||||
setTabIds([...tabIds, newTabName]);
|
setTabIds([...tabIds, newTabName]);
|
||||||
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
||||||
|
|
||||||
|
scrollToNewTabTimeoutIdRef.current = setTimeout(() => {
|
||||||
|
if (scrollable) {
|
||||||
|
const { viewport } = osInstanceRef.current.elements();
|
||||||
|
viewport.scrollLeft = tabIds.length * tabWidth;
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (tabId: string) => {
|
const handleCloseTab = (tabId: string) => {
|
||||||
@@ -317,26 +401,41 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
deleteLayoutStateAtomForTab(tabId);
|
deleteLayoutStateAtomForTab(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTabLoaded = useCallback((tabId) => {
|
||||||
|
setTabsLoaded((prev) => {
|
||||||
|
if (!prev[tabId]) {
|
||||||
|
// Only update if the tab isn't already marked as loaded
|
||||||
|
return { ...prev, [tabId]: true };
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isBeforeActive = (tabId: string) => {
|
const isBeforeActive = (tabId: string) => {
|
||||||
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tabsWrapperWidth = tabIds.length * tabWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-bar-wrapper">
|
<div className="tab-bar-wrapper">
|
||||||
<div className="tab-bar" ref={tabBarRef}>
|
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
||||||
{tabIds.map((tabId, index) => (
|
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: tabsWrapperWidth }}>
|
||||||
<Tab
|
{tabIds.map((tabId, index) => (
|
||||||
key={tabId}
|
<Tab
|
||||||
ref={tabRefs.current[index]}
|
key={tabId}
|
||||||
id={tabId}
|
ref={tabRefs.current[index]}
|
||||||
onSelect={() => handleSelectTab(tabId)}
|
id={tabId}
|
||||||
active={activetabid === tabId}
|
onSelect={() => handleSelectTab(tabId)}
|
||||||
onDragStart={() => handleDragStart(tabId, tabRefs.current[index])}
|
active={activetabid === tabId}
|
||||||
onClose={() => handleCloseTab(tabId)}
|
onDragStart={() => handleDragStart(tabId, tabRefs.current[index])}
|
||||||
isBeforeActive={isBeforeActive(tabId)}
|
onClose={() => handleCloseTab(tabId)}
|
||||||
isDragging={draggingTab === tabId}
|
onLoaded={() => handleTabLoaded(tabId)}
|
||||||
/>
|
isBeforeActive={isBeforeActive(tabId)}
|
||||||
))}
|
isDragging={draggingTab === tabId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
|
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
|
||||||
<i className="fa fa-solid fa-plus fa-fw" />
|
<i className="fa fa-solid fa-plus fa-fw" />
|
||||||
|
|||||||
@@ -29,4 +29,10 @@
|
|||||||
/* z-index values */
|
/* z-index values */
|
||||||
--zindex-header-hover: 100;
|
--zindex-header-hover: 100;
|
||||||
--zindex-termstickers: 20;
|
--zindex-termstickers: 20;
|
||||||
|
|
||||||
|
/* scrollbar colors */
|
||||||
|
--scrollbar-background-color: var(--app-bg-color);
|
||||||
|
--scrollbar-thumb-color: rgba(255, 255, 255, 0.3);
|
||||||
|
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
|
||||||
|
--scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -31,7 +31,7 @@ declare global {
|
|||||||
|
|
||||||
type BlockCommand = {
|
type BlockCommand = {
|
||||||
command: string;
|
command: string;
|
||||||
} & ( ResolveIdsCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockInputCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | CreateBlockCommand );
|
} & ( ResolveIdsCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockInputCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | CreateBlockCommand );
|
||||||
|
|
||||||
// wstore.BlockDef
|
// wstore.BlockDef
|
||||||
type BlockDef = {
|
type BlockDef = {
|
||||||
@@ -325,4 +325,4 @@ declare global {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jotai": "^2.8.0",
|
"jotai": "^2.8.0",
|
||||||
"monaco-editor": "^0.49.0",
|
"monaco-editor": "^0.49.0",
|
||||||
|
"overlayscrollbars": "^2.8.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
|
|||||||
@@ -10322,6 +10322,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"overlayscrollbars@npm:^2.8.3":
|
||||||
|
version: 2.8.3
|
||||||
|
resolution: "overlayscrollbars@npm:2.8.3"
|
||||||
|
checksum: 10c0/1f0713e981ce27e0cd9261a6a0acfe080bd44c21c9f6cbb9c3df582c155d11a0c949553d7ea4e01a107d784d527c5cf3465ab6979affca9e430d450968b763f6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"p-cancelable@npm:^2.0.0":
|
"p-cancelable@npm:^2.0.0":
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
resolution: "p-cancelable@npm:2.1.1"
|
resolution: "p-cancelable@npm:2.1.1"
|
||||||
@@ -12303,6 +12310,7 @@ __metadata:
|
|||||||
jotai: "npm:^2.8.0"
|
jotai: "npm:^2.8.0"
|
||||||
less: "npm:^4.2.0"
|
less: "npm:^4.2.0"
|
||||||
monaco-editor: "npm:^0.49.0"
|
monaco-editor: "npm:^0.49.0"
|
||||||
|
overlayscrollbars: "npm:^2.8.3"
|
||||||
prettier: "npm:^3.2.5"
|
prettier: "npm:^3.2.5"
|
||||||
prettier-plugin-jsdoc: "npm:^1.3.0"
|
prettier-plugin-jsdoc: "npm:^1.3.0"
|
||||||
prettier-plugin-organize-imports: "npm:^3.2.4"
|
prettier-plugin-organize-imports: "npm:^3.2.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user