保存一下

This commit is contained in:
2025-09-02 11:54:06 +08:00
parent d2b5b0e865
commit 981d93d28f
6 changed files with 768 additions and 441 deletions

View File

@@ -11,7 +11,6 @@
:iconInfo="appIcon" :gridTemplate="gridTemplate" :iconInfo="appIcon" :gridTemplate="gridTemplate"
@dblclick="runApp(appIcon)" @dblclick="runApp(appIcon)"
/> />
/>
</div> </div>
</div> </div>
<div class="task-bar"></div> <div class="task-bar"></div>

View File

@@ -1,297 +0,0 @@
type TDragStartCallback = (x: number, y: number) => void;
type TDragMoveCallback = (x: number, y: number) => void;
type TDragEndCallback = (x: number, y: number) => void;
interface IDraggableOptions {
handle: HTMLElement;
target: HTMLElement;
mode?: 'transform' | 'position';
boundary?: IBoundaryRect | HTMLElement;
snapGrid?: number;
snapThreshold?: number;
snapAnimation?: boolean;
snapAnimationDuration?: number;
onStart?: TDragStartCallback;
onMove?: TDragMoveCallback;
onEnd?: TDragEndCallback;
}
interface IBoundaryRect {
minX?: number;
maxX?: number;
minY?: number;
maxY?: number;
}
// 全局实例列表,用于窗口之间吸附
const DraggableInstances: Draggable[] = [];
export class Draggable {
private handle: HTMLElement;
private target: HTMLElement;
private boundary?: HTMLElement | IBoundaryRect;
private mode: 'transform' | 'position';
private snapGrid: number;
private snapThreshold: number;
private snapAnimation: boolean;
private snapAnimationDuration: number;
private onStart?: TDragStartCallback;
private onMove?: TDragMoveCallback;
private onEnd?: TDragEndCallback;
private isDragging = false;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private currentX = 0;
private currentY = 0;
private containerRect?: DOMRect;
private resizeObserver?: ResizeObserver;
private animationFrame?: number;
constructor(options: IDraggableOptions) {
this.handle = options.handle;
this.target = options.target;
this.boundary = options.boundary;
this.mode = options.mode ?? 'transform';
this.snapGrid = options.snapGrid ?? 1;
this.snapThreshold = options.snapThreshold ?? 10;
this.snapAnimation = options.snapAnimation ?? true;
this.snapAnimationDuration = options.snapAnimationDuration ?? 200;
this.onStart = options.onStart;
this.onMove = options.onMove;
this.onEnd = options.onEnd;
this.init();
// 注册全局实例
DraggableInstances.push(this);
}
/** 初始化 */
private init() {
this.handle.addEventListener('mousedown', this.onMouseDown);
if (this.boundary instanceof HTMLElement) {
this.observeResize(this.boundary);
}
}
/** 销毁 */
public destroy() {
this.handle.removeEventListener('mousedown', this.onMouseDown);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
this.resizeObserver?.disconnect();
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
const index = DraggableInstances.indexOf(this);
if (index >= 0) DraggableInstances.splice(index, 1);
}
/** 拖拽开始 */
private onMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
if (this.mode === 'position') {
const rect = this.target.getBoundingClientRect();
this.offsetX = rect.left;
this.offsetY = rect.top;
} else {
this.offsetX = this.currentX;
this.offsetY = this.currentY;
}
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
this.onStart?.(this.offsetX, this.offsetY);
};
/** 拖拽中(实时移动,不吸附) */
private onMouseMove = (e: MouseEvent) => {
if (!this.isDragging) return;
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
let newX = this.offsetX + dx;
let newY = this.offsetY + dy;
// 网格吸附
if (this.snapGrid > 1) {
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
newY = Math.round(newY / this.snapGrid) * this.snapGrid;
}
this.applyPosition(newX, newY, false);
this.onMove?.(newX, newY);
};
/** 拖拽结束,执行吸附 */
private onMouseUp = () => {
if (!this.isDragging) return;
this.isDragging = false;
const snapped = this.applySnapping(this.currentX, this.currentY);
if (this.snapAnimation) {
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
this.onEnd?.(snapped.x, snapped.y);
});
} else {
this.applyPosition(snapped.x, snapped.y, true);
this.onEnd?.(snapped.x, snapped.y);
}
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
};
/** 应用位置 */
private applyPosition(x: number, y: number, isFinal: boolean) {
this.currentX = x;
this.currentY = y;
if (this.mode === 'position') {
this.target.style.left = `${x}px`;
this.target.style.top = `${y}px`;
} else {
this.target.style.transform = `translate(${x}px, ${y}px)`;
}
if (isFinal) this.applyBoundary();
}
/** 平滑动画移动 */
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
const startX = this.currentX;
const startY = this.currentY;
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const startTime = performance.now();
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3); // easeOutCubic
const x = startX + deltaX * ease;
const y = startY + deltaY * ease;
this.applyPosition(x, y, false);
this.onMove?.(x, y);
if (progress < 1) {
this.animationFrame = requestAnimationFrame(step);
} else {
this.applyPosition(targetX, targetY, true);
this.onMove?.(targetX, targetY);
onComplete?.();
}
};
this.animationFrame = requestAnimationFrame(step);
}
/** 边界限制 */
private applyBoundary() {
if (!this.boundary) return;
let { x, y } = { x: this.currentX, y: this.currentY };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
const minX = 0;
const minY = 0;
const maxX = this.containerRect.width - rect.width;
const maxY = this.containerRect.height - rect.height;
x = Math.min(Math.max(x, minX), maxX);
y = Math.min(Math.max(y, minY), maxY);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary.minX !== undefined) x = Math.max(x, this.boundary.minX);
if (this.boundary.maxX !== undefined) x = Math.min(x, this.boundary.maxX);
if (this.boundary.minY !== undefined) y = Math.max(y, this.boundary.minY);
if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY);
}
this.currentX = x;
this.currentY = y;
this.applyPosition(x, y, false);
}
/** 吸附计算,包括边界和其他窗口 */
private applySnapping(x: number, y: number): { x: number; y: number } {
const snapPoints = this.getSnapPoints();
let snappedX = x;
let snappedY = y;
if (this.snapThreshold > 0) {
for (const sx of snapPoints.x) {
if (Math.abs(x - sx) <= this.snapThreshold) {
snappedX = sx;
break;
}
}
for (const sy of snapPoints.y) {
if (Math.abs(y - sy) <= this.snapThreshold) {
snappedY = sy;
break;
}
}
}
return { x: snappedX, y: snappedY };
}
/** 获取吸附点 */
private getSnapPoints(): { x: number[]; y: number[] } {
const snapX: number[] = [];
const snapY: number[] = [];
// 边界吸附
if (this.boundary instanceof HTMLElement && this.containerRect) {
const containerRect = this.containerRect;
const targetRect = this.target.getBoundingClientRect();
snapX.push(0, containerRect.width - targetRect.width);
snapY.push(0, containerRect.height - targetRect.height);
snapX.push(containerRect.width / 2 - targetRect.width / 2);
snapY.push(containerRect.height / 2 - targetRect.height / 2);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary?.minX !== undefined) snapX.push(this.boundary.minX);
if (this.boundary?.maxX !== undefined) snapX.push(this.boundary.maxX);
if (this.boundary?.minY !== undefined) snapY.push(this.boundary.minY);
if (this.boundary?.maxY !== undefined) snapY.push(this.boundary.maxY);
}
// 窗口之间吸附
for (const other of DraggableInstances) {
if (other === this) continue;
const rect = other.target.getBoundingClientRect();
snapX.push(rect.left, rect.right, rect.left + rect.width / 2);
snapY.push(rect.top, rect.bottom, rect.top + rect.height / 2);
}
return { x: snapX, y: snapY };
}
/** 监听 boundary 尺寸变化 */
private observeResize(element: HTMLElement) {
this.resizeObserver = new ResizeObserver(() => {
this.containerRect = element.getBoundingClientRect();
});
this.resizeObserver.observe(element);
this.containerRect = element.getBoundingClientRect();
}
}

View File

@@ -0,0 +1,527 @@
type TDragStartCallback = (x: number, y: number) => void;
type TDragMoveCallback = (x: number, y: number) => void;
type TDragEndCallback = (x: number, y: number) => void;
type ResizeDirection =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
interface ResizeCallbackData {
width: number;
height: number;
top: number;
left: number;
direction: ResizeDirection;
}
/** 拖拽参数 */
interface IDraggableOptions {
/** 拖拽/调整尺寸目标元素 */
target: HTMLElement;
/** 拖拽句柄 */
handle?: HTMLElement;
/** 拖拽模式 */
mode?: 'transform' | 'position';
/** 拖拽边界或容器元素 */
boundary?: IBoundaryRect | HTMLElement;
/** 移动步进(网格吸附) */
snapGrid?: number;
/** 关键点吸附阈值 */
snapThreshold?: number;
/** 是否开启吸附动画 */
snapAnimation?: boolean;
/** 拖拽结束吸附动画时长 */
snapAnimationDuration?: number;
/** 是否允许超出边界 */
allowOverflow?: boolean;
/** 拖拽回调 */
onStart?: TDragStartCallback;
onMove?: TDragMoveCallback;
onEnd?: TDragEndCallback;
/** 调整尺寸最小最大值 */
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
/** 调整尺寸回调 */
onResize?: (data: ResizeCallbackData) => void;
onResizeEnd?: (data: ResizeCallbackData) => void;
}
/** 拖拽范围边界 */
interface IBoundaryRect {
minX?: number;
maxX?: number;
minY?: number;
maxY?: number;
}
/**
* 拖拽 + 调整尺寸通用类
*/
export class DraggableResizable {
// ---------------- Drag 属性 ----------------
private handle?: HTMLElement;
private target: HTMLElement;
private boundary?: HTMLElement | IBoundaryRect;
private mode: 'transform' | 'position';
private snapGrid: number;
private snapThreshold: number;
private snapAnimation: boolean;
private snapAnimationDuration: number;
private allowOverflow: boolean;
private onStart?: TDragStartCallback;
private onMove?: TDragMoveCallback;
private onEnd?: TDragEndCallback;
private isDragging = false;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private currentX = 0;
private currentY = 0;
private containerRect?: DOMRect;
private resizeObserver?: ResizeObserver;
private mutationObserver: MutationObserver;
private animationFrame?: number;
// ---------------- Resize 属性 ----------------
private currentDirection: ResizeDirection | null = null;
private startWidth = 0;
private startHeight = 0;
private startTop = 0;
private startLeft = 0;
private minWidth: number;
private minHeight: number;
private maxWidth: number;
private maxHeight: number;
private onResize?: (data: ResizeCallbackData) => void;
private onResizeEnd?: (data: ResizeCallbackData) => void;
constructor(options: IDraggableOptions) {
// Drag
this.handle = options.handle;
this.target = options.target;
this.boundary = options.boundary;
this.mode = options.mode ?? 'transform';
this.snapGrid = options.snapGrid ?? 1;
this.snapThreshold = options.snapThreshold ?? 0;
this.snapAnimation = options.snapAnimation ?? false;
this.snapAnimationDuration = options.snapAnimationDuration ?? 200;
this.allowOverflow = options.allowOverflow ?? true;
this.onStart = options.onStart;
this.onMove = options.onMove;
this.onEnd = options.onEnd;
// Resize
this.minWidth = options.minWidth ?? 100;
this.minHeight = options.minHeight ?? 50;
this.maxWidth = options.maxWidth ?? window.innerWidth;
this.maxHeight = options.maxHeight ?? window.innerHeight;
this.onResize = options.onResize;
this.onResizeEnd = options.onResizeEnd;
// 自动监听 DOM 移除
this.mutationObserver = new MutationObserver(() => {
if (!document.body.contains(this.target)) this.destroy();
});
this.mutationObserver.observe(document.body, { childList: true, subtree: true });
this.init();
}
/** 初始化事件 */
private init() {
if (this.handle) {
this.handle.addEventListener('mousedown', this.onMouseDown);
} else {
this.target.addEventListener('mousedown', this.onMouseDown);
}
this.target.addEventListener('mousemove', this.onMouseMoveCursor);
this.target.addEventListener('mouseleave', this.onMouseLeave);
if (this.boundary instanceof HTMLElement) {
this.observeResize(this.boundary);
}
// 确保 target 是 absolute 或 relative
if (getComputedStyle(this.target).position === 'static') {
this.target.style.position = 'absolute';
}
}
// ---------------- Drag 方法 ----------------
private onMouseDown = (e: MouseEvent) => {
const dir = this.getResizeDirection(e);
if (dir) {
// 开始 Resize
e.preventDefault();
this.startResize(e, dir);
} else {
// 开始 Drag
e.preventDefault();
this.startDrag(e);
}
};
private startDrag(e: MouseEvent) {
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
if (this.mode === 'position') {
const rect = this.target.getBoundingClientRect();
this.offsetX = rect.left;
this.offsetY = rect.top;
} else {
this.offsetX = this.currentX;
this.offsetY = this.currentY;
}
document.addEventListener('mousemove', this.onDragMove);
document.addEventListener('mouseup', this.onDragEnd);
this.onStart?.(this.offsetX, this.offsetY);
}
private onDragMove = (e: MouseEvent) => {
if (!this.isDragging) return;
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
let newX = this.offsetX + dx;
let newY = this.offsetY + dy;
if (this.snapGrid > 1) {
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
newY = Math.round(newY / this.snapGrid) * this.snapGrid;
}
this.applyPosition(newX, newY, false);
this.onMove?.(newX, newY);
};
private onDragEnd = () => {
if (!this.isDragging) return;
this.isDragging = false;
const snapped = this.applySnapping(this.currentX, this.currentY);
if (this.snapAnimation) {
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
this.onEnd?.(snapped.x, snapped.y);
});
} else {
this.applyPosition(snapped.x, snapped.y, true);
this.onEnd?.(snapped.x, snapped.y);
}
document.removeEventListener('mousemove', this.onDragMove);
document.removeEventListener('mouseup', this.onDragEnd);
};
private applyPosition(x: number, y: number, isFinal: boolean) {
this.currentX = x;
this.currentY = y;
if (this.mode === 'position') {
this.target.style.left = `${x}px`;
this.target.style.top = `${y}px`;
} else {
this.target.style.transform = `translate(${x}px, ${y}px)`;
}
if (isFinal) this.applyBoundary();
}
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
const startX = this.currentX;
const startY = this.currentY;
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const startTime = performance.now();
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
const x = startX + deltaX * ease;
const y = startY + deltaY * ease;
this.applyPosition(x, y, false);
this.onMove?.(x, y);
if (progress < 1) {
this.animationFrame = requestAnimationFrame(step);
} else {
this.applyPosition(targetX, targetY, true);
this.onMove?.(targetX, targetY);
onComplete?.();
}
};
this.animationFrame = requestAnimationFrame(step);
}
private applyBoundary() {
if (!this.boundary || this.allowOverflow) return;
let { x, y } = { x: this.currentX, y: this.currentY };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
const minX = 0;
const minY = 0;
const maxX = this.containerRect.width - rect.width;
const maxY = this.containerRect.height - rect.height;
x = Math.min(Math.max(x, minX), maxX);
y = Math.min(Math.max(y, minY), maxY);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary.minX !== undefined) x = Math.max(x, this.boundary.minX);
if (this.boundary.maxX !== undefined) x = Math.min(x, this.boundary.maxX);
if (this.boundary.minY !== undefined) y = Math.max(y, this.boundary.minY);
if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY);
}
this.currentX = x;
this.currentY = y;
this.applyPosition(x, y, false);
}
private applySnapping(x: number, y: number) {
const snapPoints = this.getSnapPoints();
let snappedX = x;
let snappedY = y;
if (this.snapThreshold > 0) {
for (const sx of snapPoints.x) {
if (Math.abs(x - sx) <= this.snapThreshold) {
snappedX = sx;
break;
}
}
for (const sy of snapPoints.y) {
if (Math.abs(y - sy) <= this.snapThreshold) {
snappedY = sy;
break;
}
}
}
return { x: snappedX, y: snappedY };
}
private getSnapPoints() {
const snapX: number[] = [];
const snapY: number[] = [];
if (this.boundary instanceof HTMLElement && this.containerRect) {
const containerRect = this.containerRect;
const targetRect = this.target.getBoundingClientRect();
snapX.push(0, containerRect.width - targetRect.width);
snapY.push(0, containerRect.height - targetRect.height);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary?.minX !== undefined) snapX.push(this.boundary.minX);
if (this.boundary?.maxX !== undefined) snapX.push(this.boundary.maxX);
if (this.boundary?.minY !== undefined) snapY.push(this.boundary.minY);
if (this.boundary?.maxY !== undefined) snapY.push(this.boundary.maxY);
}
return { x: snapX, y: snapY };
}
private observeResize(element: HTMLElement) {
this.resizeObserver = new ResizeObserver(() => {
this.containerRect = element.getBoundingClientRect();
if (!this.allowOverflow) this.applyBoundary();
});
this.resizeObserver.observe(element);
this.containerRect = element.getBoundingClientRect();
}
// ---------------- Resize 方法 ----------------
private getResizeDirection(e: MouseEvent): ResizeDirection | null {
const rect = this.target.getBoundingClientRect();
const offset = 8;
const x = e.clientX;
const y = e.clientY;
const top = y >= rect.top && y <= rect.top + offset;
const bottom = y >= rect.bottom - offset && y <= rect.bottom;
const left = x >= rect.left && x <= rect.left + offset;
const right = x >= rect.right - offset && x <= rect.right;
if (top && left) return 'top-left';
if (top && right) return 'top-right';
if (bottom && left) return 'bottom-left';
if (bottom && right) return 'bottom-right';
if (top) return 'top';
if (bottom) return 'bottom';
if (left) return 'left';
if (right) return 'right';
return null;
}
private updateCursor(direction: ResizeDirection | null) {
if (!direction) {
this.target.style.cursor = 'default';
return;
}
const cursorMap: Record<ResizeDirection, string> = {
top: 'ns-resize',
bottom: 'ns-resize',
left: 'ew-resize',
right: 'ew-resize',
'top-left': 'nwse-resize',
'top-right': 'nesw-resize',
'bottom-left': 'nesw-resize',
'bottom-right': 'nwse-resize',
};
this.target.style.cursor = cursorMap[direction];
}
private onMouseMoveCursor = (e: MouseEvent) => {
if (this.currentDirection || this.isDragging) return;
const dir = this.getResizeDirection(e);
this.updateCursor(dir);
};
private onMouseLeave = () => {
if (!this.currentDirection && !this.isDragging) this.updateCursor(null);
};
private startResize(e: MouseEvent, dir: ResizeDirection) {
this.currentDirection = dir;
const rect = this.target.getBoundingClientRect();
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = rect.width;
this.startHeight = rect.height;
this.startTop = rect.top;
this.startLeft = rect.left;
document.addEventListener('mousemove', this.onResizeDrag);
document.addEventListener('mouseup', this.onResizeEndHandler);
}
private onResizeDrag = (e: MouseEvent) => {
if (!this.currentDirection) return;
let deltaX = e.clientX - this.startX;
let deltaY = e.clientY - this.startY;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newTop = this.startTop;
let newLeft = this.startLeft;
switch (this.currentDirection) {
case 'right':
newWidth = this.startWidth + deltaX;
break;
case 'bottom':
newHeight = this.startHeight + deltaY;
break;
case 'bottom-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight + deltaY;
break;
case 'left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
break;
case 'top':
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'bottom-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight + deltaY;
break;
}
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.target.style.top = `${newTop}px`;
this.target.style.left = `${newLeft}px`;
this.onResize?.({
width: newWidth,
height: newHeight,
top: newTop,
left: newLeft,
direction: this.currentDirection,
});
};
private onResizeEndHandler = () => {
if (this.currentDirection) {
const rect = this.target.getBoundingClientRect();
this.onResizeEnd?.({
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left,
direction: this.currentDirection,
});
}
this.currentDirection = null;
this.updateCursor(null);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
};
/** 销毁 */
public destroy() {
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDown);
this.target.removeEventListener('mousedown', this.onMouseDown);
this.target.removeEventListener('mousemove', this.onMouseMoveCursor);
this.target.removeEventListener('mouseleave', this.onMouseLeave);
document.removeEventListener('mousemove', this.onDragMove);
document.removeEventListener('mouseup', this.onDragEnd);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
this.resizeObserver?.disconnect();
this.mutationObserver.disconnect();
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
}
}

224
src/core/utils/Resizable.ts Normal file
View File

@@ -0,0 +1,224 @@
type ResizeDirection =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
interface ResizeCallbackData {
width: number;
height: number;
top: number;
left: number;
direction: ResizeDirection;
}
interface ResizableOptions {
target: HTMLElement; // 要改变尺寸的元素
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
onResize?: (data: ResizeCallbackData) => void; // 拖拽中回调
onResizeEnd?: (data: ResizeCallbackData) => void; // 拖拽结束回调
}
export class Resizable {
private target: HTMLElement;
private minWidth: number;
private minHeight: number;
private maxWidth: number;
private maxHeight: number;
private currentDirection: ResizeDirection | null = null;
private startX = 0;
private startY = 0;
private startWidth = 0;
private startHeight = 0;
private startTop = 0;
private startLeft = 0;
private onResize?: (data: ResizeCallbackData) => void;
private onResizeEnd?: (data: ResizeCallbackData) => void;
constructor(options: ResizableOptions) {
this.target = options.target;
this.minWidth = options.minWidth || 100;
this.minHeight = options.minHeight || 50;
this.maxWidth = options.maxWidth || window.innerWidth;
this.maxHeight = options.maxHeight || window.innerHeight;
this.onResize = options.onResize;
this.onResizeEnd = options.onResizeEnd;
this.init();
}
private init() {
this.target.style.position = 'absolute';
this.target.addEventListener('mousedown', this.onMouseDown);
this.target.addEventListener('mousemove', this.onMouseMove);
this.target.addEventListener('mouseleave', this.onMouseLeave);
}
private getDirection(e: MouseEvent): ResizeDirection | null {
const rect = this.target.getBoundingClientRect();
const offset = 8;
const x = e.clientX;
const y = e.clientY;
const top = y >= rect.top && y <= rect.top + offset;
const bottom = y >= rect.bottom - offset && y <= rect.bottom;
const left = x >= rect.left && x <= rect.left + offset;
const right = x >= rect.right - offset && x <= rect.right;
if (top && left) return 'top-left';
if (top && right) return 'top-right';
if (bottom && left) return 'bottom-left';
if (bottom && right) return 'bottom-right';
if (top) return 'top';
if (bottom) return 'bottom';
if (left) return 'left';
if (right) return 'right';
return null;
}
private updateCursor(direction: ResizeDirection | null) {
if (!direction) {
this.target.style.cursor = 'default';
return;
}
const cursorMap: Record<ResizeDirection, string> = {
top: 'ns-resize',
bottom: 'ns-resize',
left: 'ew-resize',
right: 'ew-resize',
'top-left': 'nwse-resize',
'top-right': 'nesw-resize',
'bottom-left': 'nesw-resize',
'bottom-right': 'nwse-resize',
};
this.target.style.cursor = cursorMap[direction];
}
private onMouseMove = (e: MouseEvent) => {
if (this.currentDirection) return;
const dir = this.getDirection(e);
this.updateCursor(dir);
};
private onMouseLeave = () => {
if (!this.currentDirection) this.updateCursor(null);
};
private onMouseDown = (e: MouseEvent) => {
const dir = this.getDirection(e);
if (!dir) return;
e.preventDefault();
this.currentDirection = dir;
const rect = this.target.getBoundingClientRect();
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = rect.width;
this.startHeight = rect.height;
this.startTop = rect.top;
this.startLeft = rect.left;
document.addEventListener('mousemove', this.onMouseDrag);
document.addEventListener('mouseup', this.onMouseUp);
};
private onMouseDrag = (e: MouseEvent) => {
if (!this.currentDirection) return;
let deltaX = e.clientX - this.startX;
let deltaY = e.clientY - this.startY;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newTop = this.startTop;
let newLeft = this.startLeft;
switch (this.currentDirection) {
case 'right':
newWidth = this.startWidth + deltaX;
break;
case 'bottom':
newHeight = this.startHeight + deltaY;
break;
case 'bottom-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight + deltaY;
break;
case 'left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
break;
case 'top':
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'bottom-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight + deltaY;
break;
}
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.target.style.top = `${newTop}px`;
this.target.style.left = `${newLeft}px`;
// 拖拽中回调
this.onResize?.({
width: newWidth,
height: newHeight,
top: newTop,
left: newLeft,
direction: this.currentDirection,
});
};
private onMouseUp = () => {
if (this.currentDirection) {
const rect = this.target.getBoundingClientRect();
this.onResizeEnd?.({
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left,
direction: this.currentDirection,
});
}
this.currentDirection = null;
this.updateCursor(null);
document.removeEventListener('mousemove', this.onMouseDrag);
document.removeEventListener('mouseup', this.onMouseUp);
};
public destroy() {
this.target.removeEventListener('mousedown', this.onMouseDown);
this.target.removeEventListener('mousemove', this.onMouseMove);
this.target.removeEventListener('mouseleave', this.onMouseLeave);
}
}

View File

@@ -1,141 +0,0 @@
export interface IResizeBoundaryFull {
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
}
export type ResizeDirection = 'right' | 'bottom' | 'left' | 'top' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export interface ResizableFullOptions {
handle: HTMLElement; // 拖拽手柄
target: HTMLElement; // 被调整大小的目标
direction?: ResizeDirection; // 调整方向
boundary?: IResizeBoundaryFull; // 宽高限制
onStart?: (width: number, height: number) => void;
onResize?: (width: number, height: number) => void;
onEnd?: (width: number, height: number) => void;
}
export class ResizableFull {
private handle: HTMLElement;
private target: HTMLElement;
private direction: ResizeDirection;
private boundary?: IResizeBoundaryFull;
private dragging = false;
private startX = 0;
private startY = 0;
private startWidth = 0;
private startHeight = 0;
private currentWidth = 0;
private currentHeight = 0;
private onStart?: (width: number, height: number) => void;
private onResize?: (width: number, height: number) => void;
private onEnd?: (width: number, height: number) => void;
constructor(options: ResizableFullOptions) {
this.handle = options.handle;
this.target = options.target;
this.direction = options.direction ?? 'right';
this.boundary = options.boundary;
this.onStart = options.onStart;
this.onResize = options.onResize;
this.onEnd = options.onEnd;
this.init();
}
private init() {
this.handle.addEventListener('mousedown', this.onMouseDown);
}
private onMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.dragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = this.target.offsetWidth;
this.startHeight = this.target.offsetHeight;
this.currentWidth = this.startWidth;
this.currentHeight = this.startHeight;
this.onStart?.(this.startWidth, this.startHeight);
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
};
private onMouseMove = (e: MouseEvent) => {
if (!this.dragging) return;
let deltaX = e.clientX - this.startX;
let deltaY = e.clientY - this.startY;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
switch (this.direction) {
case 'right':
newWidth += deltaX;
break;
case 'left':
newWidth -= deltaX;
break;
case 'bottom':
newHeight += deltaY;
break;
case 'top':
newHeight -= deltaY;
break;
case 'top-left':
newWidth -= deltaX;
newHeight -= deltaY;
break;
case 'top-right':
newWidth += deltaX;
newHeight -= deltaY;
break;
case 'bottom-left':
newWidth -= deltaX;
newHeight += deltaY;
break;
case 'bottom-right':
newWidth += deltaX;
newHeight += deltaY;
break;
}
if (this.boundary) {
if (this.boundary.minWidth !== undefined) newWidth = Math.max(newWidth, this.boundary.minWidth);
if (this.boundary.maxWidth !== undefined) newWidth = Math.min(newWidth, this.boundary.maxWidth);
if (this.boundary.minHeight !== undefined) newHeight = Math.max(newHeight, this.boundary.minHeight);
if (this.boundary.maxHeight !== undefined) newHeight = Math.min(newHeight, this.boundary.maxHeight);
}
this.currentWidth = newWidth;
this.currentHeight = newHeight;
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.onResize?.(newWidth, newHeight);
};
private onMouseUp = () => {
if (!this.dragging) return;
this.dragging = false;
this.onEnd?.(this.currentWidth, this.currentHeight);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
};
public destroy() {
this.handle.removeEventListener('mousedown', this.onMouseDown);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
}
}

View File

@@ -6,6 +6,8 @@ import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts
import type { WindowFormPos } from '@/core/window/types/WindowFormTypes.ts' import type { WindowFormPos } from '@/core/window/types/WindowFormTypes.ts'
import { processManager } from '@/core/process/ProcessManager.ts' import { processManager } from '@/core/process/ProcessManager.ts'
import { Draggable } from '@/core/utils/Draggable.ts' import { Draggable } from '@/core/utils/Draggable.ts'
import { Resizable } from '@/core/utils/Resizable.ts'
import { DraggableResizable } from '@/core/utils/DraggableResizable.ts'
export default class WindowFormImpl implements IWindowForm { export default class WindowFormImpl implements IWindowForm {
private readonly _id: string = uuidV4(); private readonly _id: string = uuidV4();
@@ -46,9 +48,22 @@ export default class WindowFormImpl implements IWindowForm {
dom.style.height = `${this.height}px`; dom.style.height = `${this.height}px`;
dom.style.zIndex = '100'; dom.style.zIndex = '100';
dom.style.backgroundColor = 'white'; dom.style.backgroundColor = 'white';
new Draggable( { // new Draggable( {
handle: dom, // handle: dom,
// target: dom,
// mode: 'position',
// snapThreshold: 20,
// boundary: document.body
// })
// new Resizable({
// target: dom,
// onResizeEnd: (data) => {
// console.log(data)
// }
// })
new DraggableResizable({
target: dom, target: dom,
handle: dom,
mode: 'position', mode: 'position',
snapThreshold: 20, snapThreshold: 20,
boundary: document.body boundary: document.body