waveterm/frontend/app/tab/tab.tsx

151 lines
5.5 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-21 19:23:04 +02:00
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
2024-06-18 06:50:33 +02:00
import { clsx } from "clsx";
2024-06-21 19:23:04 +02:00
import { forwardRef, useEffect, useRef, useState } from "react";
2024-05-14 00:10:31 +02:00
2024-05-28 21:12:28 +02:00
import "./tab.less";
2024-05-14 00:10:31 +02:00
2024-06-18 06:50:33 +02:00
interface TabProps {
id: string;
active: boolean;
2024-06-23 21:03:09 +02:00
isFirst: boolean;
2024-06-18 06:50:33 +02:00
isBeforeActive: boolean;
isDragging: boolean;
onSelect: () => void;
2024-06-25 02:50:06 +02:00
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
2024-06-21 19:23:04 +02:00
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onLoaded: () => void;
2024-06-18 06:50:33 +02:00
}
2024-06-21 19:23:04 +02:00
const Tab = forwardRef<HTMLDivElement, TabProps>(
2024-06-23 21:03:09 +02:00
({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
2024-06-18 06:50:33 +02:00
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
2024-06-21 19:23:04 +02:00
const [originalName, setOriginalName] = useState("");
const [isEditable, setIsEditable] = useState(false);
2024-06-21 19:23:04 +02:00
const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>();
const loadedRef = useRef(false);
2024-06-21 19:23:04 +02:00
useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
}
}, [tabData]);
useEffect(() => {
return () => {
if (editableTimeoutRef.current) {
clearTimeout(editableTimeoutRef.current);
}
};
}, []);
2024-06-23 21:03:09 +02:00
const handleDoubleClick = (event) => {
event.stopPropagation();
2024-06-21 19:23:04 +02:00
setIsEditable(true);
editableTimeoutRef.current = setTimeout(() => {
if (editableRef.current) {
editableRef.current.focus();
document.execCommand("selectAll", false);
}
}, 0);
};
const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
newText = newText || originalName;
editableRef.current.innerText = newText;
setIsEditable(false);
services.ObjectService.UpdateTabName(id, newText);
};
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;
}
if (event.key === "Enter") {
event.preventDefault();
if (editableRef.current.innerText.trim() === "") {
editableRef.current.innerText = originalName;
}
editableRef.current.blur();
} else if (event.key === "Escape") {
editableRef.current.innerText = originalName;
editableRef.current.blur();
} else if (
editableRef.current.innerText.length >= 8 &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)
) {
event.preventDefault();
}
};
useEffect(() => {
if (!loadedRef.current) {
onLoaded();
loadedRef.current = true;
}
}, [onLoaded]);
2024-06-21 19:23:04 +02:00
// Prevent drag from being triggered on mousedown
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
};
2024-06-25 02:50:06 +02:00
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
let menu: ContextMenuItem[] = [];
menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) });
menu.push({ type: "separator" });
2024-06-25 02:50:06 +02:00
menu.push({ label: "Close Tab", click: () => onClose(null) });
ContextMenuModel.showContextMenu(menu, e);
}
2024-05-28 01:33:31 +02:00
return (
2024-06-18 06:50:33 +02:00
<div
ref={ref}
className={clsx("tab", { active, isDragging, "before-active": isBeforeActive })}
onMouseDown={onDragStart}
onClick={onSelect}
2024-06-25 02:50:06 +02:00
onContextMenu={handleContextMenu}
2024-06-18 06:50:33 +02:00
data-tab-id={id}
>
2024-06-23 21:03:09 +02:00
{isFirst && <div className="vertical-line first" />}
2024-06-21 19:23:04 +02:00
<div
ref={editableRef}
className={clsx("name", { focused: isEditable })}
contentEditable={isEditable}
onDoubleClick={handleDoubleClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
suppressContentEditableWarning={true}
>
{tabData?.name}
</div>
2024-06-18 06:50:33 +02:00
{!isDragging && <div className="vertical-line" />}
{active && <div className="mask" />}
2024-06-21 19:23:04 +02:00
<Button className="secondary ghost close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
2024-06-18 06:50:33 +02:00
<i className="fa fa-solid fa-xmark" />
</Button>
2024-05-28 01:33:31 +02:00
</div>
);
}
2024-06-18 06:50:33 +02:00
);
2024-06-18 06:50:33 +02:00
export { Tab };