Files
20260512-skg-tk/web/components/nodes/resize-handle.tsx
2026-05-14 02:20:00 +08:00

100 lines
3.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { type PointerEvent } from "react"
import { useReactFlow } from "@xyflow/react"
interface Bounds {
minWidth?: number
maxWidth?: number
minHeight?: number
maxHeight?: number
}
/** 拖动期间持续更新 ReactFlow node 的 width/height同时写到 style.width/height 让 wrapper 真实渲染。
* 自适应 viewport zoom。不走 xyflow NodeResizeControlglass-node overflow:hidden + reserved-type CSS 干扰下不响应)。 */
function startResize(
e: PointerEvent<HTMLDivElement>,
setNodes: ReturnType<typeof useReactFlow>["setNodes"],
getZoom: ReturnType<typeof useReactFlow>["getZoom"],
axis: "x" | "y" | "xy",
bounds: Required<Bounds>,
) {
e.preventDefault()
e.stopPropagation()
const target = e.currentTarget
const nodeEl = target.closest(".react-flow__node") as HTMLElement | null
const nodeId = nodeEl?.getAttribute("data-id")
if (!nodeId || !nodeEl) return
target.setPointerCapture(e.pointerId)
const startX = e.clientX
const startY = e.clientY
const zoom = getZoom() || 1
// computedStyle width/height are already in ReactFlow node coordinates.
// Only pointer movement needs zoom normalization because clientX/Y are viewport pixels.
const startWidth = parseFloat(getComputedStyle(nodeEl).width)
const startHeight = parseFloat(getComputedStyle(nodeEl).height)
const onMove = (ev: globalThis.PointerEvent) => {
const rawDx = ev.clientX - startX
const rawDy = ev.clientY - startY
if (Math.abs(rawDx) < 2 && Math.abs(rawDy) < 2) return
const dx = rawDx / zoom
const dy = rawDy / zoom
const wantW = axis === "y" ? null : Math.max(bounds.minWidth, Math.min(bounds.maxWidth, startWidth + dx))
const wantH = axis === "x" ? null : Math.max(bounds.minHeight, Math.min(bounds.maxHeight, startHeight + dy))
setNodes((nodes) =>
nodes.map((n) => {
if (n.id !== nodeId) return n
const nextStyle = { ...n.style }
const patch: Record<string, unknown> = {}
if (wantW !== null) { patch.width = wantW; nextStyle.width = wantW }
if (wantH !== null) { patch.height = wantH; nextStyle.height = wantH }
return { ...n, ...patch, style: nextStyle }
}),
)
}
const onUp = () => {
window.removeEventListener("pointermove", onMove)
window.removeEventListener("pointerup", onUp)
try { target.releasePointerCapture(e.pointerId) } catch {}
}
window.addEventListener("pointermove", onMove)
window.addEventListener("pointerup", onUp)
}
/** 右边缘把手 — 只改宽度 */
export function ResizeRight({ minWidth = 240, maxWidth = 1400 }: Bounds) {
const { setNodes, getZoom } = useReactFlow()
return (
<div
onPointerDown={(e) => startResize(e, setNodes, getZoom, "x", { minWidth, maxWidth, minHeight: 0, maxHeight: 0 })}
title="拖动调整宽度"
className="nodrag absolute z-20 hover:bg-violet-400/60 active:bg-violet-400/80 transition rounded-r"
style={{ right: 0, top: 12, bottom: 12, width: 6, cursor: "ew-resize", touchAction: "none" }}
/>
)
}
/** 右下角把手 — 同时改宽和高 */
export function ResizeBR({
minWidth = 240, maxWidth = 1400, minHeight = 120, maxHeight = 1400,
}: Bounds) {
const { setNodes, getZoom } = useReactFlow()
return (
<div
onPointerDown={(e) => startResize(e, setNodes, getZoom, "xy", { minWidth, maxWidth, minHeight, maxHeight })}
title="拖动调整大小(宽 × 高)"
className="nodrag absolute z-30 hover:bg-violet-400/80 active:bg-violet-400 transition"
style={{
right: 0,
bottom: 0,
width: 14,
height: 14,
cursor: "nwse-resize",
touchAction: "none",
background: "linear-gradient(135deg, transparent 50%, rgba(167,139,250,0.55) 50%)",
}}
/>
)
}