|
1 | 1 | import { cva } from "class-variance-authority"; |
2 | 2 | import { observer } from "mobx-react-lite"; |
3 | | -import { type CSSProperties, useState } from "react"; |
| 3 | +import { type CSSProperties, type MouseEventHandler, useState } from "react"; |
4 | 4 | import { createPortal } from "react-dom"; |
5 | 5 |
|
6 | 6 | import { useWindowContext } from "@/routes/v2/shared/windows/ContentWindowStateContext"; |
@@ -57,24 +57,44 @@ function getContentHeight(model: WindowModel): string | number { |
57 | 57 | return model.size.height - HEADER_HEIGHT; |
58 | 58 | } |
59 | 59 |
|
60 | | -function ResizeHandle({ |
61 | | - onMouseDown, |
| 60 | +type ResizeDirection = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw"; |
| 61 | + |
| 62 | +const resizeHandleClasses: Record<ResizeDirection, string> = { |
| 63 | + n: "absolute top-0 left-3 right-3 h-1 cursor-n-resize", |
| 64 | + s: "absolute bottom-0 left-3 right-3 h-1 cursor-s-resize", |
| 65 | + e: "absolute right-0 top-3 bottom-3 w-1 cursor-e-resize", |
| 66 | + w: "absolute left-0 top-3 bottom-3 w-1 cursor-w-resize", |
| 67 | + ne: "absolute top-0 right-0 w-3 h-3 cursor-ne-resize", |
| 68 | + nw: "absolute top-0 left-0 w-3 h-3 cursor-nw-resize", |
| 69 | + se: "absolute bottom-0 right-0 w-3 h-3 cursor-se-resize hover:bg-gray-300 rounded-tl-sm transition-colors", |
| 70 | + sw: "absolute bottom-0 left-0 w-3 h-3 cursor-sw-resize", |
| 71 | +}; |
| 72 | + |
| 73 | +function ResizeHandles({ |
| 74 | + onResizeStart, |
62 | 75 | }: { |
63 | | - onMouseDown: React.MouseEventHandler; |
| 76 | + onResizeStart: (direction: ResizeDirection) => MouseEventHandler; |
64 | 77 | }) { |
65 | 78 | return ( |
66 | | - <div |
67 | | - className="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize hover:bg-gray-300 rounded-tl-sm transition-colors" |
68 | | - onMouseDown={onMouseDown} |
69 | | - > |
70 | | - <svg |
71 | | - className="w-full h-full text-gray-400" |
72 | | - viewBox="0 0 16 16" |
73 | | - fill="currentColor" |
74 | | - > |
75 | | - <path d="M14 14H12V12H14V14ZM14 10H12V8H14V10ZM10 14H8V12H10V14Z" /> |
76 | | - </svg> |
77 | | - </div> |
| 79 | + <> |
| 80 | + {(Object.keys(resizeHandleClasses) as ResizeDirection[]).map((dir) => ( |
| 81 | + <div |
| 82 | + key={dir} |
| 83 | + className={resizeHandleClasses[dir]} |
| 84 | + onMouseDown={onResizeStart(dir)} |
| 85 | + > |
| 86 | + {dir === "se" && ( |
| 87 | + <svg |
| 88 | + className="w-full h-full text-gray-400" |
| 89 | + viewBox="0 0 16 16" |
| 90 | + fill="currentColor" |
| 91 | + > |
| 92 | + <path d="M14 14H12V12H14V14ZM14 10H12V8H14V10ZM10 14H8V12H10V14Z" /> |
| 93 | + </svg> |
| 94 | + )} |
| 95 | + </div> |
| 96 | + ))} |
| 97 | + </> |
78 | 98 | ); |
79 | 99 | } |
80 | 100 |
|
@@ -105,38 +125,64 @@ export const FloatingWindow = observer(function FloatingWindow() { |
105 | 125 |
|
106 | 126 | const [isResizing, setIsResizing] = useState(false); |
107 | 127 |
|
108 | | - const handleResizeMouseDown = (e: React.MouseEvent) => { |
109 | | - e.preventDefault(); |
110 | | - e.stopPropagation(); |
111 | | - setIsResizing(true); |
112 | | - |
113 | | - const startX = e.clientX; |
114 | | - const startY = e.clientY; |
115 | | - const startWidth = model.size.width; |
116 | | - const startHeight = model.size.height; |
117 | | - |
118 | | - const onMouseMove = (moveE: MouseEvent) => { |
119 | | - const newWidth = Math.max( |
120 | | - model.minSize.width, |
121 | | - startWidth + (moveE.clientX - startX), |
122 | | - ); |
123 | | - const newHeight = Math.max( |
124 | | - model.minSize.height, |
125 | | - startHeight + (moveE.clientY - startY), |
126 | | - ); |
127 | | - model.updateSize({ width: newWidth, height: newHeight }); |
128 | | - }; |
129 | | - |
130 | | - const onMouseUp = () => { |
131 | | - setIsResizing(false); |
132 | | - document.removeEventListener("mousemove", onMouseMove); |
133 | | - document.removeEventListener("mouseup", onMouseUp); |
| 128 | + const handleResizeMouseDown = |
| 129 | + (direction: ResizeDirection): MouseEventHandler => |
| 130 | + (e) => { |
| 131 | + e.preventDefault(); |
| 132 | + e.stopPropagation(); |
| 133 | + setIsResizing(true); |
| 134 | + |
| 135 | + const startX = e.clientX; |
| 136 | + const startY = e.clientY; |
| 137 | + const startWidth = model.size.width; |
| 138 | + const startHeight = model.size.height; |
| 139 | + const startPosX = model.position.x; |
| 140 | + const startPosY = model.position.y; |
| 141 | + |
| 142 | + const resizeRight = direction.includes("e"); |
| 143 | + const resizeLeft = direction.includes("w"); |
| 144 | + const resizeBottom = direction.includes("s"); |
| 145 | + const resizeTop = direction.includes("n"); |
| 146 | + |
| 147 | + const onMouseMove = (moveE: MouseEvent) => { |
| 148 | + const dx = moveE.clientX - startX; |
| 149 | + const dy = moveE.clientY - startY; |
| 150 | + |
| 151 | + let newWidth = startWidth; |
| 152 | + let newHeight = startHeight; |
| 153 | + let newX = startPosX; |
| 154 | + let newY = startPosY; |
| 155 | + |
| 156 | + if (resizeRight) { |
| 157 | + newWidth = Math.max(model.minSize.width, startWidth + dx); |
| 158 | + } else if (resizeLeft) { |
| 159 | + newWidth = Math.max(model.minSize.width, startWidth - dx); |
| 160 | + newX = startPosX + (startWidth - newWidth); |
| 161 | + } |
| 162 | + |
| 163 | + if (resizeBottom) { |
| 164 | + newHeight = Math.max(model.minSize.height, startHeight + dy); |
| 165 | + } else if (resizeTop) { |
| 166 | + newHeight = Math.max(model.minSize.height, startHeight - dy); |
| 167 | + newY = startPosY + (startHeight - newHeight); |
| 168 | + } |
| 169 | + |
| 170 | + model.updateSize({ width: newWidth, height: newHeight }); |
| 171 | + if (resizeLeft || resizeTop) { |
| 172 | + model.updatePosition({ x: newX, y: newY }); |
| 173 | + } |
| 174 | + }; |
| 175 | + |
| 176 | + const onMouseUp = () => { |
| 177 | + setIsResizing(false); |
| 178 | + document.removeEventListener("mousemove", onMouseMove); |
| 179 | + document.removeEventListener("mouseup", onMouseUp); |
| 180 | + }; |
| 181 | + |
| 182 | + document.addEventListener("mousemove", onMouseMove); |
| 183 | + document.addEventListener("mouseup", onMouseUp); |
134 | 184 | }; |
135 | 185 |
|
136 | | - document.addEventListener("mousemove", onMouseMove); |
137 | | - document.addEventListener("mouseup", onMouseUp); |
138 | | - }; |
139 | | - |
140 | 186 | return ( |
141 | 187 | <> |
142 | 188 | <div |
@@ -167,7 +213,7 @@ export const FloatingWindow = observer(function FloatingWindow() { |
167 | 213 | </div> |
168 | 214 |
|
169 | 215 | {!model.isMaximized && ( |
170 | | - <ResizeHandle onMouseDown={handleResizeMouseDown} /> |
| 216 | + <ResizeHandles onResizeStart={handleResizeMouseDown} /> |
171 | 217 | )} |
172 | 218 | </div> |
173 | 219 |
|
|
0 commit comments