// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { atoms, globalStore, refocusNode } from "@/app/store/global"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { Button } from "@/element/button"; import { ContextMenuModel } from "@/store/contextmenu"; import { fireAndForget } from "@/util/util"; import { clsx } from "clsx"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { ObjectService } from "../store/services"; import { makeORef, useWaveObjectValue } from "../store/wos"; import "./tab.scss"; interface TabProps { id: string; active: boolean; isFirst: boolean; isBeforeActive: boolean; isDragging: boolean; tabWidth: number; isNew: boolean; isPinned: boolean; onSelect: () => void; onClose: (event: React.MouseEvent | null) => void; onDragStart: (event: React.MouseEvent) => void; onLoaded: () => void; onPinChange: () => void; } const Tab = memo( forwardRef( ( { id, active, isPinned, isBeforeActive, isDragging, tabWidth, isNew, onLoaded, onSelect, onClose, onDragStart, onPinChange, }, ref ) => { const [tabData, _] = useWaveObjectValue(makeORef("tab", id)); const [originalName, setOriginalName] = useState(""); const [isEditable, setIsEditable] = useState(false); const editableRef = useRef(null); const editableTimeoutRef = useRef(); const loadedRef = useRef(false); const tabRef = useRef(null); useImperativeHandle(ref, () => tabRef.current as HTMLDivElement); useEffect(() => { if (tabData?.name) { setOriginalName(tabData.name); } }, [tabData]); useEffect(() => { return () => { if (editableTimeoutRef.current) { clearTimeout(editableTimeoutRef.current); } }; }, []); const selectEditableText = useCallback(() => { if (editableRef.current) { const range = document.createRange(); const selection = window.getSelection(); range.selectNodeContents(editableRef.current); selection.removeAllRanges(); selection.addRange(range); } }, []); const handleRenameTab: React.MouseEventHandler = (event) => { event?.stopPropagation(); setIsEditable(true); editableTimeoutRef.current = setTimeout(() => { selectEditableText(); }, 0); }; const handleBlur = () => { let newText = editableRef.current.innerText.trim(); newText = newText || originalName; editableRef.current.innerText = newText; setIsEditable(false); fireAndForget(() => ObjectService.UpdateTabName(id, newText)); setTimeout(() => refocusNode(null), 10); }; const handleKeyDown: React.KeyboardEventHandler = (event) => { if ((event.metaKey || event.ctrlKey) && event.key === "a") { event.preventDefault(); selectEditableText(); return; } // this counts glyphs, not characters const curLen = Array.from(editableRef.current.innerText).length; if (event.key === "Enter") { event.preventDefault(); event.stopPropagation(); if (editableRef.current.innerText.trim() === "") { editableRef.current.innerText = originalName; } editableRef.current.blur(); } else if (event.key === "Escape") { editableRef.current.innerText = originalName; editableRef.current.blur(); event.preventDefault(); event.stopPropagation(); } else if (curLen >= 14 && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)) { event.preventDefault(); event.stopPropagation(); } }; useEffect(() => { if (!loadedRef.current) { onLoaded(); loadedRef.current = true; } }, [onLoaded]); 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]); // Prevent drag from being triggered on mousedown const handleMouseDownOnClose = (event: React.MouseEvent) => { event.stopPropagation(); }; const handleContextMenu = useCallback( (e: React.MouseEvent) => { e.preventDefault(); let menu: ContextMenuItem[] = [ { label: isPinned ? "Unpin Tab" : "Pin Tab", click: () => onPinChange() }, { label: "Rename Tab", click: () => handleRenameTab(null) }, { label: "Copy TabId", click: () => fireAndForget(() => 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); } } 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: () => fireAndForget(async () => { await ObjectService.UpdateObjectMeta(oref, preset); await RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 }); }), }); } menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); } menu.push({ label: "Close Tab", click: () => onClose(null) }); ContextMenuModel.showContextMenu(menu, e); }, [onPinChange, handleRenameTab, id, onClose, isPinned] ); return (
{tabData?.name}
{isPinned ? ( ) : ( )}
); } ) ); export { Tab };