feat(saved-connections): enhance drag-and-drop functionality with pointer events
- Introduced pointer drag support in ConnectionItem and GroupNodeItem components, allowing for improved user interactions during drag-and-drop operations. - Added new pointer event handlers for starting, moving, ending, and canceling pointer drags, enhancing the overall drag experience. - Updated SavedConnections component to manage pointer drag state and resolve drop targets based on pointer position, improving accuracy in item placement. - Refactored TabBar to support pointer-based dragging, ensuring a consistent experience across components.
This commit is contained in:
1 parent
e62561feb9
commit
d068313e5f
5 files changed
+349
-28
No files matched your search
@@ -162,6 +162,7 @@ function ConnectionDetailsTooltip({ conn, t }: ConnectionDetailsTooltipProps) {
|
||||
export default function ConnectionItem({ conn, indented, depth = 0 }: ConnectionItemProps) {
|
||||
const {
|
||||
isDragEnabled,
|
||||
isPointerDragEnabled,
|
||||
dragTarget,
|
||||
selectedConnectionIds,
|
||||
handleConnect,
|
||||
@@ -180,6 +181,10 @@ export default function ConnectionItem({ conn, indented, depth = 0 }: Connection
|
||||
handleDragOverItem,
|
||||
handleDragLeaveItem,
|
||||
handleDropItem,
|
||||
handlePointerDragStart,
|
||||
handlePointerDragMove,
|
||||
handlePointerDragEnd,
|
||||
handlePointerDragCancel,
|
||||
t,
|
||||
} = useSavedConnectionsContext();
|
||||
|
||||
@@ -242,9 +247,22 @@ export default function ConnectionItem({ conn, indented, depth = 0 }: Connection
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
data-saved-drop-type="connection"
|
||||
data-saved-drop-id={conn.id}
|
||||
className="relative min-w-full w-max"
|
||||
draggable={isDragEnabled}
|
||||
draggable={isDragEnabled && !isPointerDragEnabled}
|
||||
onWheel={closeAndSuppressDetails}
|
||||
onPointerDown={
|
||||
isPointerDragEnabled
|
||||
? (e) => {
|
||||
closeAndSuppressDetails();
|
||||
handlePointerDragStart(e, "connection", conn.id);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onPointerMove={isPointerDragEnabled ? handlePointerDragMove : undefined}
|
||||
onPointerUp={isPointerDragEnabled ? handlePointerDragEnd : undefined}
|
||||
onPointerCancel={isPointerDragEnabled ? handlePointerDragCancel : undefined}
|
||||
onDragStart={
|
||||
isDragEnabled
|
||||
? (e) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ interface GroupNodeItemProps {
|
||||
export default function GroupNodeItem({ node, depth }: GroupNodeItemProps) {
|
||||
const {
|
||||
isDragEnabled,
|
||||
isPointerDragEnabled,
|
||||
dragTarget,
|
||||
expandedGroups,
|
||||
toggleGroup,
|
||||
@@ -41,6 +42,10 @@ export default function GroupNodeItem({ node, depth }: GroupNodeItemProps) {
|
||||
handleDragOverItem,
|
||||
handleDragLeaveItem,
|
||||
handleDropItem,
|
||||
handlePointerDragStart,
|
||||
handlePointerDragMove,
|
||||
handlePointerDragEnd,
|
||||
handlePointerDragCancel,
|
||||
t,
|
||||
} = useSavedConnectionsContext();
|
||||
|
||||
@@ -56,7 +61,15 @@ export default function GroupNodeItem({ node, depth }: GroupNodeItemProps) {
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className="relative"
|
||||
draggable={isDragEnabled}
|
||||
draggable={isDragEnabled && !isPointerDragEnabled}
|
||||
onPointerDown={
|
||||
isPointerDragEnabled
|
||||
? (e) => handlePointerDragStart(e, "group", node.group.id)
|
||||
: undefined
|
||||
}
|
||||
onPointerMove={isPointerDragEnabled ? handlePointerDragMove : undefined}
|
||||
onPointerUp={isPointerDragEnabled ? handlePointerDragEnd : undefined}
|
||||
onPointerCancel={isPointerDragEnabled ? handlePointerDragCancel : undefined}
|
||||
onDragStart={
|
||||
isDragEnabled ? (e) => handleDragStart(e, "group", node.group.id) : undefined
|
||||
}
|
||||
@@ -70,6 +83,8 @@ export default function GroupNodeItem({ node, depth }: GroupNodeItemProps) {
|
||||
)}
|
||||
<div
|
||||
data-group-header
|
||||
data-saved-drop-type="group"
|
||||
data-saved-drop-id={node.group.id}
|
||||
className={`flex items-center gap-1.5 py-1.5 px-2 rounded cursor-pointer transition-colors select-none df-hover ${isInside ? "ring-1 ring-primary/60 bg-primary/10" : ""}`}
|
||||
style={{ paddingLeft: indentPx }}
|
||||
onClick={() => toggleGroup(node.group.id)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import type { MouseEvent as ReactMouseEvent } from "react";
|
||||
import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import type { NewSessionTarget } from "@/lib/windowManager";
|
||||
import type { Group, SavedConnection } from "@/types/global";
|
||||
@@ -29,6 +29,7 @@ export const naturalCompare = (a: string, b: string) =>
|
||||
export interface SavedConnectionsContextValue {
|
||||
// UI state
|
||||
isDragEnabled: boolean;
|
||||
isPointerDragEnabled: boolean;
|
||||
dragTarget: DragTarget | null;
|
||||
expandedGroups: Set<string>;
|
||||
selectedConnectionIds: Set<string>;
|
||||
@@ -68,6 +69,10 @@ export interface SavedConnectionsContextValue {
|
||||
id: string,
|
||||
tgtType: "connection" | "group",
|
||||
) => Promise<void>;
|
||||
handlePointerDragStart: (e: ReactPointerEvent, type: "connection" | "group", id: string) => void;
|
||||
handlePointerDragMove: (e: ReactPointerEvent) => void;
|
||||
handlePointerDragEnd: (e: ReactPointerEvent) => void;
|
||||
handlePointerDragCancel: (e: ReactPointerEvent) => void;
|
||||
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,21 @@ type HeaderActionButtonProps = ComponentProps<typeof Button> & {
|
||||
};
|
||||
|
||||
const SAVED_CONNECTIONS_DRAG_MIME = "application/x-nyaterm-saved-connections";
|
||||
const POINTER_SAVED_DRAG_THRESHOLD_PX = 4;
|
||||
|
||||
interface PointerSavedDragState {
|
||||
type: "connection" | "group";
|
||||
id: string;
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
function shouldUsePointerSavedConnectionsDrag() {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /Mac/.test(navigator.platform) && /AppleWebKit/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function HeaderActionButton({ tooltip, children, ...props }: HeaderActionButtonProps) {
|
||||
return (
|
||||
@@ -138,6 +153,7 @@ export default function SavedConnections({
|
||||
const [dragTarget, _setDragTarget] = useState<DragTarget | null>(null);
|
||||
const dragTargetRef = useRef<DragTarget | null>(null);
|
||||
const dragSourceRef = useRef<{ type: "connection" | "group"; id: string } | null>(null);
|
||||
const pointerDragRef = useRef<PointerSavedDragState | null>(null);
|
||||
const connectionsRef = useRef(savedConnections);
|
||||
const groupsRef = useRef(savedGroups);
|
||||
connectionsRef.current = savedConnections;
|
||||
@@ -145,6 +161,7 @@ export default function SavedConnections({
|
||||
|
||||
const keyword = filterText.trim().toLowerCase();
|
||||
const isDragEnabled = sortMode === "default";
|
||||
const isPointerDragEnabled = isDragEnabled && shouldUsePointerSavedConnectionsDrag();
|
||||
const connectionById = useMemo(
|
||||
() => new Map(savedConnections.map((connection) => [connection.id, connection])),
|
||||
[savedConnections],
|
||||
@@ -766,6 +783,64 @@ export default function SavedConnections({
|
||||
return y < rect.height * 0.5 ? "before" : "after";
|
||||
};
|
||||
|
||||
const computeDropPositionFromPoint = (
|
||||
element: HTMLElement,
|
||||
clientY: number,
|
||||
itemType: "connection" | "group",
|
||||
srcType: "connection" | "group",
|
||||
): DragTarget["position"] => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const y = clientY - rect.top;
|
||||
if (itemType === "group") {
|
||||
if (srcType === "connection") return "inside";
|
||||
if (y < rect.height * 0.25) return "before";
|
||||
if (y > rect.height * 0.75) return "after";
|
||||
return "inside";
|
||||
}
|
||||
return y < rect.height * 0.5 ? "before" : "after";
|
||||
};
|
||||
|
||||
const resolvePointerItemTarget = (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
source: { type: "connection" | "group"; id: string },
|
||||
): DragTarget | null => {
|
||||
const elements = document.elementsFromPoint(clientX, clientY);
|
||||
for (const element of elements) {
|
||||
const target = element.closest<HTMLElement>("[data-saved-drop-type][data-saved-drop-id]");
|
||||
if (!target) continue;
|
||||
|
||||
const type = target.dataset.savedDropType;
|
||||
const id = target.dataset.savedDropId;
|
||||
if ((type !== "connection" && type !== "group") || !id) continue;
|
||||
if (!canDropOnItem(source, id, type)) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: computeDropPositionFromPoint(target, clientY, type, source.type),
|
||||
};
|
||||
}
|
||||
|
||||
const background = panelRootRef.current?.querySelector<HTMLElement>("[data-saved-drop-bg]");
|
||||
if (!background) return null;
|
||||
const rect = background.getBoundingClientRect();
|
||||
if (
|
||||
clientX < rect.left ||
|
||||
clientX > rect.right ||
|
||||
clientY < rect.top ||
|
||||
clientY > rect.bottom
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAtRoot =
|
||||
source.type === "connection"
|
||||
? !connectionsRef.current.find((c) => c.id === source.id)?.group_id
|
||||
: !groupsRef.current.find((g) => g.id === source.id)?.parent_id;
|
||||
return isAtRoot ? null : { id: null, type: "background", position: "inside" };
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, type: "connection" | "group", id: string) => {
|
||||
e.stopPropagation();
|
||||
// WebKit-based runtimes may ignore HTML5 drops unless the drag carries real data.
|
||||
@@ -780,6 +855,89 @@ export default function SavedConnections({
|
||||
dragSourceRef.current = null;
|
||||
};
|
||||
|
||||
const handlePointerDragStart = (
|
||||
e: React.PointerEvent,
|
||||
type: "connection" | "group",
|
||||
id: string,
|
||||
) => {
|
||||
if (!isPointerDragEnabled) return;
|
||||
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
|
||||
|
||||
pointerDragRef.current = {
|
||||
type,
|
||||
id,
|
||||
pointerId: e.pointerId,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
dragging: false,
|
||||
};
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerDragMove = (e: React.PointerEvent) => {
|
||||
if (!isPointerDragEnabled) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== e.pointerId) return;
|
||||
|
||||
const moved =
|
||||
Math.abs(e.clientX - state.startX) >= POINTER_SAVED_DRAG_THRESHOLD_PX ||
|
||||
Math.abs(e.clientY - state.startY) >= POINTER_SAVED_DRAG_THRESHOLD_PX;
|
||||
if (!state.dragging) {
|
||||
if (!moved) return;
|
||||
state.dragging = true;
|
||||
dragSourceRef.current = { type: state.type, id: state.id };
|
||||
}
|
||||
|
||||
const source = { type: state.type, id: state.id } as const;
|
||||
const target = resolvePointerItemTarget(e.clientX, e.clientY, source);
|
||||
const prev = dragTargetRef.current;
|
||||
if (
|
||||
prev?.id !== target?.id ||
|
||||
prev?.type !== target?.type ||
|
||||
prev?.position !== target?.position
|
||||
) {
|
||||
setDragTarget(target);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handlePointerDragEnd = (e: React.PointerEvent) => {
|
||||
if (!isPointerDragEnabled) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== e.pointerId) return;
|
||||
pointerDragRef.current = null;
|
||||
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
const source = { type: state.type, id: state.id } as const;
|
||||
const target = state.dragging ? resolvePointerItemTarget(e.clientX, e.clientY, source) : null;
|
||||
setDragTarget(null);
|
||||
dragSourceRef.current = null;
|
||||
|
||||
if ((target?.type === "connection" || target?.type === "group") && target.id) {
|
||||
void submitItemDrop(source, target.id, target.type, target.position);
|
||||
} else if (target?.type === "background") {
|
||||
void dropSourceToRoot(source);
|
||||
}
|
||||
|
||||
if (state.dragging) e.preventDefault();
|
||||
};
|
||||
|
||||
const handlePointerDragCancel = (e: React.PointerEvent) => {
|
||||
if (!isPointerDragEnabled) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== e.pointerId) return;
|
||||
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
pointerDragRef.current = null;
|
||||
dragSourceRef.current = null;
|
||||
setDragTarget(null);
|
||||
};
|
||||
|
||||
const updateItemDragTarget = (e: React.DragEvent, id: string, type: "connection" | "group") => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -811,22 +969,17 @@ export default function SavedConnections({
|
||||
if (cur?.id === id && cur.type === type) setDragTarget(null);
|
||||
};
|
||||
|
||||
const handleDropItem = async (
|
||||
e: React.DragEvent,
|
||||
const submitItemDrop = async (
|
||||
source: { type: "connection" | "group"; id: string },
|
||||
id: string,
|
||||
tgtType: "connection" | "group",
|
||||
position: DragTarget["position"],
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const source = resolveDragSource(e.dataTransfer);
|
||||
setDragTarget(null);
|
||||
dragSourceRef.current = null;
|
||||
if (!source || !canDropOnItem(source, id, tgtType)) return;
|
||||
if (!canDropOnItem(source, id, tgtType)) return;
|
||||
|
||||
const connections = connectionsRef.current;
|
||||
const groups = groupsRef.current;
|
||||
const { id: srcId, type: srcType } = source;
|
||||
const position = computeDropPosition(e, tgtType, srcType);
|
||||
|
||||
try {
|
||||
if (position === "inside" && tgtType === "group") {
|
||||
@@ -933,6 +1086,21 @@ export default function SavedConnections({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropItem = async (
|
||||
e: React.DragEvent,
|
||||
id: string,
|
||||
tgtType: "connection" | "group",
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const source = resolveDragSource(e.dataTransfer);
|
||||
setDragTarget(null);
|
||||
dragSourceRef.current = null;
|
||||
if (!source || !canDropOnItem(source, id, tgtType)) return;
|
||||
|
||||
await submitItemDrop(source, id, tgtType, computeDropPosition(e, tgtType, source.type));
|
||||
};
|
||||
|
||||
const handleDragOverBg = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const source = resolveDragSource(e.dataTransfer);
|
||||
@@ -951,12 +1119,7 @@ export default function SavedConnections({
|
||||
setDragTarget({ id: null, type: "background", position: "inside" });
|
||||
};
|
||||
|
||||
const handleDropBg = async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const source = resolveDragSource(e.dataTransfer);
|
||||
setDragTarget(null);
|
||||
dragSourceRef.current = null;
|
||||
if (!source) return;
|
||||
async function dropSourceToRoot(source: { type: "connection" | "group"; id: string }) {
|
||||
try {
|
||||
if (source.type === "connection") {
|
||||
const conn = connectionsRef.current.find((c) => c.id === source.id);
|
||||
@@ -995,6 +1158,15 @@ export default function SavedConnections({
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleDropBg = async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const source = resolveDragSource(e.dataTransfer);
|
||||
setDragTarget(null);
|
||||
dragSourceRef.current = null;
|
||||
if (!source) return;
|
||||
await dropSourceToRoot(source);
|
||||
};
|
||||
|
||||
// ── Sort button helpers ───────────────────────────────────────────────────
|
||||
@@ -1015,6 +1187,7 @@ export default function SavedConnections({
|
||||
// ── Context value ─────────────────────────────────────────────────────────
|
||||
const ctxValue: SavedConnectionsContextValue = {
|
||||
isDragEnabled,
|
||||
isPointerDragEnabled,
|
||||
dragTarget,
|
||||
expandedGroups,
|
||||
selectedConnectionIds,
|
||||
@@ -1040,6 +1213,10 @@ export default function SavedConnections({
|
||||
handleDragOverItem,
|
||||
handleDragLeaveItem,
|
||||
handleDropItem,
|
||||
handlePointerDragStart,
|
||||
handlePointerDragMove,
|
||||
handlePointerDragEnd,
|
||||
handlePointerDragCancel,
|
||||
t,
|
||||
};
|
||||
|
||||
@@ -1169,6 +1346,7 @@ export default function SavedConnections({
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
data-saved-drop-bg
|
||||
className={`flex-1 overflow-x-auto overflow-y-auto p-1.5 text-xs space-y-0.5 terminal-scroll ${dragTarget?.type === "background" ? "ring-inset ring-2 ring-primary/20" : ""}`}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 0 || event.target !== event.currentTarget) return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type DragEvent,
|
||||
type MouseEvent,
|
||||
memo,
|
||||
type PointerEvent,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
@@ -92,6 +93,20 @@ interface ConnectionGroupNode {
|
||||
|
||||
const TAB_STRIP_SCROLL_DURATION_MS = 180;
|
||||
const TAB_STRIP_SCROLL_PADDING = 12;
|
||||
const POINTER_TAB_DRAG_THRESHOLD_PX = 4;
|
||||
|
||||
interface PointerTabDragState {
|
||||
tabId: string;
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
function shouldUsePointerTabDrag() {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /Mac/.test(navigator.platform) && /AppleWebKit/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function easeOutCubic(progress: number): number {
|
||||
return 1 - (1 - progress) ** 3;
|
||||
@@ -264,8 +279,12 @@ function TabBar({
|
||||
const tabButtonRefs = useRef(new Map<string, HTMLDivElement>());
|
||||
const draggedTabIdRef = useRef<string | null>(null);
|
||||
const droppedTabRef = useRef(false);
|
||||
const completedDropRef = useRef(false);
|
||||
const pointerDragRef = useRef<PointerTabDragState | null>(null);
|
||||
const suppressTabClickRef = useRef(false);
|
||||
const tabStripAnimatingRef = useRef(false);
|
||||
const tabStripScrollAnimationRef = useRef<(() => void) | null>(null);
|
||||
const usePointerTabDrag = shouldUsePointerTabDrag();
|
||||
const [tabStripScroll, setTabStripScroll] = useState({
|
||||
hasOverflow: false,
|
||||
});
|
||||
@@ -733,6 +752,7 @@ function TabBar({
|
||||
if (!effectiveTabId) return;
|
||||
|
||||
droppedTabRef.current = true;
|
||||
completedDropRef.current = true;
|
||||
const fromIndex = tabs.findIndex((tab) => tab.id === effectiveTabId);
|
||||
if (fromIndex === -1) {
|
||||
if (onMoveTabHere) {
|
||||
@@ -746,6 +766,11 @@ function TabBar({
|
||||
}
|
||||
|
||||
const nextIndex = insertionIndex > fromIndex ? insertionIndex - 1 : insertionIndex;
|
||||
if (nextIndex === fromIndex) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
onReorderTabs(effectiveTabId, nextIndex);
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("nyaterm:refresh-terminals"));
|
||||
@@ -761,16 +786,89 @@ function TabBar({
|
||||
event.dataTransfer.setData("application/nyaterm-tab", tabId);
|
||||
draggedTabIdRef.current = tabId;
|
||||
droppedTabRef.current = false;
|
||||
completedDropRef.current = false;
|
||||
setDraggedTabId(tabId);
|
||||
setDropIndex(tabs.findIndex((tab) => tab.id === tabId));
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEvent<HTMLDivElement>) => {
|
||||
if (droppedTabRef.current) {
|
||||
const handlePointerDown = (event: PointerEvent<HTMLDivElement>, tabId: string) => {
|
||||
if (!usePointerTabDrag) return;
|
||||
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
pointerDragRef.current = {
|
||||
tabId,
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
dragging: false,
|
||||
};
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!usePointerTabDrag) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== event.pointerId) return;
|
||||
|
||||
const moved =
|
||||
Math.abs(event.clientX - state.startX) >= POINTER_TAB_DRAG_THRESHOLD_PX ||
|
||||
Math.abs(event.clientY - state.startY) >= POINTER_TAB_DRAG_THRESHOLD_PX;
|
||||
if (!state.dragging) {
|
||||
if (!moved) return;
|
||||
state.dragging = true;
|
||||
suppressTabClickRef.current = true;
|
||||
draggedTabIdRef.current = state.tabId;
|
||||
droppedTabRef.current = false;
|
||||
setDraggedTabId(state.tabId);
|
||||
}
|
||||
|
||||
setDropIndex(getInsertionIndexFromClientX(event.clientX));
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handlePointerEnd = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!usePointerTabDrag) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== event.pointerId) return;
|
||||
pointerDragRef.current = null;
|
||||
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
if (state.dragging && isDragEndFallbackNearTabStrip(event.clientX, event.clientY)) {
|
||||
handleDropAtIndex(getInsertionIndexFromClientX(event.clientX));
|
||||
} else {
|
||||
resetDragState();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
window.setTimeout(() => {
|
||||
suppressTabClickRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handlePointerCancel = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!usePointerTabDrag) return;
|
||||
const state = pointerDragRef.current;
|
||||
if (!state || state.pointerId !== event.pointerId) return;
|
||||
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
pointerDragRef.current = null;
|
||||
resetDragState();
|
||||
window.setTimeout(() => {
|
||||
suppressTabClickRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEvent<HTMLDivElement>) => {
|
||||
if (droppedTabRef.current || completedDropRef.current) {
|
||||
completedDropRef.current = false;
|
||||
resetDragState();
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("nyaterm:refresh-terminals"));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -781,9 +879,6 @@ function TabBar({
|
||||
}
|
||||
|
||||
resetDragState();
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new CustomEvent("nyaterm:refresh-terminals"));
|
||||
});
|
||||
};
|
||||
|
||||
const renderTabIcon = (tab: Tab) => {
|
||||
@@ -944,7 +1039,7 @@ function TabBar({
|
||||
|
||||
const tabButton = (
|
||||
<div
|
||||
draggable
|
||||
draggable={!usePointerTabDrag}
|
||||
className={`group relative flex items-center gap-2 border-r pl-3 pr-2 text-xs transition-[color,background-color,opacity] duration-200 ${
|
||||
isActive ? "font-semibold" : "font-medium df-hover"
|
||||
} ${draggedTabId === tab.id ? "opacity-60" : ""}`}
|
||||
@@ -959,7 +1054,13 @@ function TabBar({
|
||||
: "transparent",
|
||||
color: isActive ? "var(--df-text)" : "var(--df-text-muted)",
|
||||
}}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
onClick={() => {
|
||||
if (suppressTabClickRef.current) {
|
||||
suppressTabClickRef.current = false;
|
||||
return;
|
||||
}
|
||||
onTabChange(tab.id);
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
handleConfiguredTabMouseAction(event, tab, doubleClickAction);
|
||||
}}
|
||||
@@ -982,6 +1083,10 @@ function TabBar({
|
||||
|
||||
handleConfiguredTabMouseAction(event, tab, rightClickAction);
|
||||
}}
|
||||
onPointerDown={(event) => handlePointerDown(event, tab.id)}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerEnd}
|
||||
onPointerCancel={handlePointerCancel}
|
||||
onDragStart={(event) => handleDragStart(event, tab.id)}
|
||||
onDragEnd={(event) => {
|
||||
handleDragEnd(event);
|
||||
|
||||
Reference in new issue
Block a user