95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
"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 NodeResizeControl(glass-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
|
||
const startWidth = parseFloat(getComputedStyle(nodeEl).width) / zoom
|
||
const startHeight = parseFloat(getComputedStyle(nodeEl).height) / zoom
|
||
|
||
const onMove = (ev: globalThis.PointerEvent) => {
|
||
const dx = (ev.clientX - startX) / zoom
|
||
const dy = (ev.clientY - startY) / 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%)",
|
||
}}
|
||
/>
|
||
)
|
||
}
|