waveterm/frontend/app/tab/tab.tsx

382 lines
16 KiB
TypeScript
Raw Normal View History

2024-06-23 21:03:09 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-06-18 06:50:33 +02:00
import { Button } from "@/element/button";
2024-06-25 02:50:06 +02:00
import { ContextMenuModel } from "@/store/contextmenu";
2024-06-18 06:50:33 +02:00
import { clsx } from "clsx";
2024-12-09 14:33:28 +01:00
import { atom, useAtom, useAtomValue } from "jotai";
2024-12-04 01:45:15 +01:00
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
2024-05-14 00:10:31 +02:00
2024-10-25 08:16:44 +02:00
import { atoms, globalStore, refocusNode } from "@/app/store/global";
2024-11-16 01:09:26 +01:00
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { ObjectService } from "../store/services";
import { makeORef, useWaveObjectValue } from "../store/wos";
2024-12-09 14:33:28 +01:00
import "./tab.scss";
2024-05-14 00:10:31 +02:00
2024-12-09 14:33:28 +01:00
const adjacentTabsAtom = atom<Set<string>>(new Set<string>());
2024-06-18 06:50:33 +02:00
interface TabProps {
id: string;
2024-12-04 07:15:36 +01:00
isActive: boolean;
2024-06-23 21:03:09 +02:00
isFirst: boolean;
2024-06-18 06:50:33 +02:00
isBeforeActive: boolean;
2024-12-10 03:52:38 +01:00
draggingId: string;
tabWidth: number;
isNew: boolean;
isPinned: boolean;
tabIds: string[];
tabRefs: React.MutableRefObject<React.RefObject<HTMLDivElement>[]>;
onClick: () => void;
2024-06-25 02:50:06 +02:00
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
onMouseDown: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onLoaded: () => void;
onPinChange: () => void;
2024-12-09 14:33:28 +01:00
// onMouseEnter: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
// onMouseLeave: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
2024-06-18 06:50:33 +02:00
}
2024-12-04 01:45:15 +01:00
const Tab = memo(
2024-06-26 18:31:43 +02:00
forwardRef<HTMLDivElement, TabProps>(
(
{
id,
2024-12-04 07:15:36 +01:00
isActive,
isFirst,
isPinned,
isBeforeActive,
2024-12-10 03:52:38 +01:00
draggingId,
tabWidth,
isNew,
tabIds,
tabRefs,
onLoaded,
onClick,
onClose,
onMouseDown,
2024-12-09 14:33:28 +01:00
// onMouseEnter,
// onMouseLeave,
onPinChange,
},
ref
) => {
const [tabData, _] = useWaveObjectValue<Tab>(makeORef("tab", id));
2024-06-26 18:31:43 +02:00
const [originalName, setOriginalName] = useState("");
const [isEditable, setIsEditable] = useState(false);
2024-06-26 18:31:43 +02:00
const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>();
const loadedRef = useRef(false);
const tabRef = useRef<HTMLDivElement>(null);
2024-12-09 14:33:28 +01:00
const adjacentTabsRef = useRef<Set<string>>(new Set());
2024-12-10 03:52:38 +01:00
const tabsSwapped = useAtomValue<boolean>(atoms.tabsSwapped);
2024-12-09 14:33:28 +01:00
const tabs = document.querySelectorAll(".tab");
const [adjacentTabs, setAdjacentTabs] = useAtom(adjacentTabsAtom);
2024-12-03 13:56:48 +01:00
useImperativeHandle(ref, () => tabRef.current as HTMLDivElement);
2024-06-26 18:31:43 +02:00
useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
2024-06-21 19:23:04 +02:00
}
2024-06-26 18:31:43 +02:00
}, [tabData]);
useEffect(() => {
return () => {
if (editableTimeoutRef.current) {
clearTimeout(editableTimeoutRef.current);
}
};
}, []);
2024-10-25 08:16:44 +02:00
const handleRenameTab = (event) => {
event?.stopPropagation();
2024-06-26 18:31:43 +02:00
setIsEditable(true);
editableTimeoutRef.current = setTimeout(() => {
if (editableRef.current) {
editableRef.current.focus();
document.execCommand("selectAll", false);
}
}, 0);
2024-06-21 19:23:04 +02:00
};
2024-06-26 18:31:43 +02:00
const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
newText = newText || originalName;
editableRef.current.innerText = newText;
setIsEditable(false);
ObjectService.UpdateTabName(id, newText);
2024-10-25 08:16:44 +02:00
setTimeout(() => refocusNode(null), 10);
2024-06-26 18:31:43 +02:00
};
const handleKeyDown = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
event.preventDefault();
if (editableRef.current) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editableRef.current);
selection.removeAllRanges();
selection.addRange(range);
}
return;
2024-06-21 19:23:04 +02:00
}
// this counts glyphs, not characters
const curLen = Array.from(editableRef.current.innerText).length;
2024-06-26 18:31:43 +02:00
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
2024-06-26 18:31:43 +02:00
if (editableRef.current.innerText.trim() === "") {
editableRef.current.innerText = originalName;
}
editableRef.current.blur();
} else if (event.key === "Escape") {
2024-06-21 19:23:04 +02:00
editableRef.current.innerText = originalName;
2024-06-26 18:31:43 +02:00
editableRef.current.blur();
event.preventDefault();
event.stopPropagation();
2024-10-25 08:16:44 +02:00
} else if (curLen >= 14 && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)) {
2024-06-26 18:31:43 +02:00
event.preventDefault();
event.stopPropagation();
2024-06-21 19:23:04 +02:00
}
2024-06-26 18:31:43 +02:00
};
useEffect(() => {
if (!loadedRef.current) {
onLoaded();
loadedRef.current = true;
}
}, [onLoaded]);
2024-06-21 19:23:04 +02:00
useEffect(() => {
if (tabRef.current && isNew) {
const initialWidth = `${(tabWidth / 3) * 2}px`;
tabRef.current.style.setProperty("--initial-tab-width", initialWidth);
tabRef.current.style.setProperty("--final-tab-width", `${tabWidth}px`);
}
}, [isNew, tabWidth]);
2024-06-26 18:31:43 +02:00
// Prevent drag from being triggered on mousedown
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
};
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.preventDefault();
let menu: ContextMenuItem[] = [
{ label: isPinned ? "Unpin Tab" : "Pin Tab", click: () => onPinChange() },
{ label: "Rename Tab", click: () => handleRenameTab(null) },
{ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) },
{ type: "separator" },
];
const fullConfig = globalStore.get(atoms.fullConfigAtom);
const bgPresets: string[] = [];
for (const key in fullConfig?.presets ?? {}) {
if (key.startsWith("bg@")) {
bgPresets.push(key);
}
2024-07-31 08:22:41 +02:00
}
bgPresets.sort((a, b) => {
const aOrder = fullConfig.presets[a]["display:order"] ?? 0;
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
return aOrder - bOrder;
});
if (bgPresets.length > 0) {
const submenu: ContextMenuItem[] = [];
const oref = makeORef("tab", id);
for (const presetName of bgPresets) {
const preset = fullConfig.presets[presetName];
if (preset == null) {
continue;
}
submenu.push({
label: preset["display:name"] ?? presetName,
click: () => {
ObjectService.UpdateObjectMeta(oref, preset);
RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 });
},
});
2024-07-31 08:22:41 +02:00
}
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
2024-07-31 08:22:41 +02:00
}
menu.push({ label: "Close Tab", click: () => onClose(null) });
ContextMenuModel.showContextMenu(menu, e);
},
[onPinChange, handleRenameTab, id, onClose, isPinned]
);
2024-06-25 02:50:06 +02:00
useEffect(() => {
2024-12-10 03:52:38 +01:00
console.log("triggered!!!!", tabsSwapped);
2024-12-09 14:33:28 +01:00
2024-12-10 03:52:38 +01:00
// Get the index of the current tab ID
const currentIndex = tabIds.indexOf(id);
// Get the right adjacent ID
const rightAdjacentId = tabIds[currentIndex + 1];
// Get the left adjacent ID
const leftAdjacentId = tabIds[currentIndex - 1];
2024-12-09 14:33:28 +01:00
2024-12-10 03:52:38 +01:00
const reset = () => {
if (!isActive) {
const currentTabElement = document.querySelector(`[data-tab-id="${id}"]`) as HTMLElement;
// To check if leftAdjacentElement is the active tab then do not reset opacity
const leftAdjacentElement = document.querySelector(
`[data-tab-id="${leftAdjacentId}"]`
) as HTMLElement;
if (!currentTabElement || !leftAdjacentElement) return;
const separator = currentTabElement.querySelector(".separator") as HTMLElement;
2024-12-09 14:33:28 +01:00
2024-12-10 03:52:38 +01:00
if (!leftAdjacentElement.classList.contains("active")) {
console.log("here!!!!!", currentTabElement, draggingId);
2024-12-10 03:52:38 +01:00
separator.style.opacity = "1"; // Reset opacity for the current tab only if not active
}
// If dragging tab is the first tab set opacity to 1
if (draggingId === tabIds[0]) {
const draggingTabElement = document.querySelector(
`[data-tab-id="${draggingId}"]`
) as HTMLElement;
if (!draggingTabElement) return;
const separator = draggingTabElement.querySelector(".separator") as HTMLElement;
separator.style.opacity = "1";
}
2024-12-10 03:52:38 +01:00
}
2024-12-10 03:52:38 +01:00
if (rightAdjacentId) {
// To check if rightAdjacentElement is the active tab then do not reset opacity
const rightAdjacentElement = document.querySelector(
`[data-tab-id="${rightAdjacentId}"]`
) as HTMLElement;
if (!rightAdjacentElement) return;
const separator = rightAdjacentElement.querySelector(".separator") as HTMLElement;
if (!rightAdjacentElement.classList.contains("active")) {
separator.style.opacity = "1"; // Reset opacity for the right adjacent tab
2024-12-09 14:33:28 +01:00
}
}
2024-12-10 03:52:38 +01:00
};
if (tabsSwapped || isActive) {
// Find the index of the current tab ID
console.log("tabIds", tabIds);
console.log("id", id);
const currentTabElement = document.querySelector(`[data-tab-id="${id}"]`) as HTMLElement;
if (!currentTabElement) return;
const separator = currentTabElement.querySelector(".separator") as HTMLElement;
if (isActive || draggingId === id) {
separator.style.opacity = "0";
}
2024-12-09 14:33:28 +01:00
// Set the opacity of the separator for the right adjacent tab
if (rightAdjacentId) {
const rightAdjacentTabElement = document.querySelector(
`[data-tab-id="${rightAdjacentId}"]`
) as HTMLElement;
2024-12-10 03:52:38 +01:00
if (!rightAdjacentTabElement) return;
const separator = rightAdjacentTabElement.querySelector(".separator") as HTMLElement;
if (isActive || draggingId === id) {
separator.style.opacity = "0";
2024-12-09 14:33:28 +01:00
}
}
2024-12-09 14:33:28 +01:00
return () => {
2024-12-10 03:52:38 +01:00
console.log("entered return +++++++++++++++");
reset();
2024-12-09 14:33:28 +01:00
};
2024-12-10 03:52:38 +01:00
} else {
console.log("entered else ?????????????????????????", tabsSwapped);
2024-12-10 03:52:38 +01:00
reset();
2024-12-09 14:33:28 +01:00
}
2024-12-10 03:52:38 +01:00
}, [id, tabIds, isFirst, isActive, draggingId, tabsSwapped]);
2024-12-09 16:48:28 +01:00
const handleMouseEnter = useCallback(() => {
if (isActive) return;
const currentIndex = tabIds.indexOf(id);
2024-12-10 03:52:38 +01:00
// console.log("tabIds", tabIds);
// console.log("id", id);
2024-12-09 16:48:28 +01:00
const currentTabElement = document.querySelector(`[data-tab-id="${id}"]`) as HTMLElement;
if (currentTabElement) {
// Ensure the element exists
2024-12-10 03:52:38 +01:00
if (!tabsSwapped) {
2024-12-09 16:48:28 +01:00
currentTabElement.classList.add("hover");
}
}
2024-12-10 03:52:38 +01:00
}, [id, isActive, tabsSwapped]);
2024-12-09 16:48:28 +01:00
const handleMouseLeave = useCallback(() => {
if (isActive) return;
2024-12-10 03:52:38 +01:00
// console.log("tabIds", tabIds);
// console.log("id", id);
2024-12-09 16:48:28 +01:00
const currentTabElement = document.querySelector(`[data-tab-id="${id}"]`) as HTMLElement;
if (currentTabElement) {
// Ensure the element exists
currentTabElement.classList.remove("hover");
}
2024-12-10 03:52:38 +01:00
}, [id, isActive, tabsSwapped]);
2024-12-09 16:48:28 +01:00
2024-06-26 18:31:43 +02:00
return (
2024-06-21 19:23:04 +02:00
<div
ref={tabRef}
className={clsx("tab", {
2024-12-04 07:15:36 +01:00
active: isActive,
2024-12-10 03:52:38 +01:00
isDragging: draggingId === id,
"before-active": isBeforeActive,
"new-tab": isNew,
})}
onMouseDown={onMouseDown}
onClick={onClick}
2024-06-26 18:31:43 +02:00
onContextMenu={handleContextMenu}
2024-12-09 16:48:28 +01:00
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
2024-06-26 18:31:43 +02:00
data-tab-id={id}
2024-06-21 19:23:04 +02:00
>
2024-12-09 14:33:28 +01:00
<div className="separator"></div>
2024-07-06 04:54:28 +02:00
<div className="tab-inner">
<div
ref={editableRef}
className={clsx("name", { focused: isEditable })}
contentEditable={isEditable}
2024-10-25 08:16:44 +02:00
onDoubleClick={handleRenameTab}
2024-07-06 04:54:28 +02:00
onBlur={handleBlur}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
>
2024-12-04 15:11:53 +01:00
{tabData?.name}
{id.substring(id.length - 3)}
2024-07-06 04:54:28 +02:00
</div>
{isPinned ? (
<Button
className="ghost grey pin"
onClick={(e) => {
e.stopPropagation();
onPinChange();
}}
>
<i className="fa fa-solid fa-thumbtack" />
</Button>
) : (
<Button className="ghost grey close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
<i className="fa fa-solid fa-xmark" />
</Button>
)}
2024-07-06 04:54:28 +02:00
</div>
2024-06-21 19:23:04 +02:00
</div>
2024-06-26 18:31:43 +02:00
);
}
)
2024-06-18 06:50:33 +02:00
);
2024-06-18 06:50:33 +02:00
export { Tab };