保存一下
This commit is contained in:
@@ -51,8 +51,6 @@ interface IDraggableResizableOptions {
|
||||
target: HTMLElement;
|
||||
/** 拖拽句柄 */
|
||||
handle?: HTMLElement;
|
||||
/** 拖拽模式 */
|
||||
mode?: 'transform' | 'position';
|
||||
/** 拖拽边界或容器元素 */
|
||||
boundary?: IBoundaryRect | HTMLElement;
|
||||
/** 移动步进(网格吸附) */
|
||||
@@ -104,12 +102,12 @@ interface IBoundaryRect {
|
||||
|
||||
/**
|
||||
* 拖拽 + 调整尺寸 + 最大最小化 通用类
|
||||
* 统一使用 position: absolute + transform: translate 实现拖拽
|
||||
*/
|
||||
export class DraggableResizableWindow {
|
||||
private handle?: HTMLElement;
|
||||
private target: HTMLElement;
|
||||
private boundary?: HTMLElement | IBoundaryRect;
|
||||
private mode: 'transform' | 'position';
|
||||
private snapGrid: number;
|
||||
private snapThreshold: number;
|
||||
private snapAnimation: boolean;
|
||||
@@ -120,45 +118,49 @@ export class DraggableResizableWindow {
|
||||
private onDragMove?: TDragMoveCallback;
|
||||
private onDragEnd?: TDragEndCallback;
|
||||
|
||||
private onResizeMove?: (data: IResizeCallbackData) => void;
|
||||
private onResizeEnd?: (data: IResizeCallbackData) => void;
|
||||
|
||||
private isDragging = false;
|
||||
private currentDirection: TResizeDirection | null = null;
|
||||
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private startWidth = 0;
|
||||
private startHeight = 0;
|
||||
private startTop = 0;
|
||||
private startLeft = 0;
|
||||
private offsetX = 0;
|
||||
private offsetY = 0;
|
||||
private currentX = 0;
|
||||
private currentY = 0;
|
||||
|
||||
private pendingDrag = false;
|
||||
private pendingResize = false;
|
||||
private dragDX = 0;
|
||||
private dragDY = 0;
|
||||
private resizeDX = 0;
|
||||
private resizeDY = 0;
|
||||
|
||||
private minWidth: number;
|
||||
private minHeight: number;
|
||||
private maxWidth: number;
|
||||
private maxHeight: number;
|
||||
|
||||
private containerRect?: DOMRect;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private mutationObserver: MutationObserver;
|
||||
private animationFrame?: number;
|
||||
|
||||
private currentDirection: TResizeDirection | 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 onResizeMove?: (data: IResizeCallbackData) => void;
|
||||
private onResizeEnd?: (data: IResizeCallbackData) => void;
|
||||
|
||||
private state: WindowState = 'default';
|
||||
/** 目标元素默认 bounds */
|
||||
private targetDefaultBounds: IElementRect;
|
||||
/** 最大化前保存 bounds */
|
||||
private maximizedBounds?: IElementRect;
|
||||
/** 最小化任务栏位置的元素ID */
|
||||
private taskbarElementId: string;
|
||||
|
||||
constructor(options: IDraggableResizableOptions) {
|
||||
// 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;
|
||||
@@ -169,7 +171,6 @@ export class DraggableResizableWindow {
|
||||
this.onDragMove = options.onDragMove;
|
||||
this.onDragEnd = options.onDragEnd;
|
||||
|
||||
// Resize
|
||||
this.minWidth = options.minWidth ?? 100;
|
||||
this.minHeight = options.minHeight ?? 50;
|
||||
this.maxWidth = options.maxWidth ?? window.innerWidth;
|
||||
@@ -177,35 +178,36 @@ export class DraggableResizableWindow {
|
||||
this.onResizeMove = options.onResizeMove;
|
||||
this.onResizeEnd = options.onResizeEnd;
|
||||
|
||||
this.targetDefaultBounds = { width: this.target.offsetWidth, height: this.target.offsetHeight, top: this.target.offsetTop, left: this.target.offsetLeft };
|
||||
this.targetDefaultBounds = {
|
||||
width: this.target.offsetWidth,
|
||||
height: this.target.offsetHeight,
|
||||
top: this.target.offsetTop,
|
||||
left: this.target.offsetLeft,
|
||||
};
|
||||
this.taskbarElementId = options.taskbarElementId;
|
||||
|
||||
this.init();
|
||||
this.target.style.position = "absolute";
|
||||
this.target.style.left = `${this.target.offsetLeft}px`;
|
||||
this.target.style.top = `${this.target.offsetTop}px`;
|
||||
this.target.style.transform = "translate(0px, 0px)";
|
||||
|
||||
// this.setDefaultCenterPosition();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/** 初始化事件 */
|
||||
private init() {
|
||||
if (this.handle) {
|
||||
this.handle.addEventListener('mousedown', this.onMouseDownDrag);
|
||||
}
|
||||
|
||||
this.target.addEventListener('mousedown', this.onMouseDownResize);
|
||||
this.target.addEventListener('mouseleave', this.onMouseLeave);
|
||||
document.addEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
||||
|
||||
if (this.boundary instanceof HTMLElement) {
|
||||
this.observeResize(this.boundary);
|
||||
}
|
||||
if (this.boundary instanceof HTMLElement) this.observeResize(this.boundary);
|
||||
|
||||
// 监听目标 DOM 是否被移除,自动销毁
|
||||
this.mutationObserver = new MutationObserver((mutations) => {
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node === this.target) {
|
||||
this.destroy();
|
||||
}
|
||||
mutation.removedNodes.forEach(node => {
|
||||
if (node === this.target) this.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -214,59 +216,43 @@ export class DraggableResizableWindow {
|
||||
}
|
||||
}
|
||||
|
||||
/** 将窗口设置在居中位置 */
|
||||
private setDefaultCenterPosition() {
|
||||
console.log(JSON.parse(JSON.stringify(this.containerRect)))
|
||||
const containerWidth = this.containerRect?.width ?? window.innerWidth;
|
||||
const containerHeight = this.containerRect?.height ?? window.innerHeight;
|
||||
const targetWidth = this.target.offsetWidth;
|
||||
const targetHeight = this.target.offsetHeight;
|
||||
|
||||
const x = (containerWidth - targetWidth) / 2;
|
||||
const y = (containerHeight - targetHeight) / 2;
|
||||
|
||||
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)`;
|
||||
}
|
||||
|
||||
this.currentX = x;
|
||||
this.currentY = y;
|
||||
this.updateDefaultBounds(x, y, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
/** ---------------- 拖拽 ---------------- */
|
||||
private onMouseDownDrag = (e: MouseEvent) => {
|
||||
if (this.getResizeDirection(e)) return; // 避免和 resize 冲突
|
||||
if (this.getResizeDirection(e)) return;
|
||||
e.preventDefault();
|
||||
this.startDrag(e);
|
||||
};
|
||||
|
||||
private startDrag(e: MouseEvent) {
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||
this.offsetX = rect.left - parentRect.left;
|
||||
this.offsetY = rect.top - parentRect.top;
|
||||
|
||||
document.addEventListener('mousemove', this.onMouseMoveDrag);
|
||||
document.addEventListener('mouseup', this.onMouseUpDrag);
|
||||
const style = window.getComputedStyle(this.target);
|
||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
||||
this.offsetX = matrix.m41;
|
||||
this.offsetY = matrix.m42;
|
||||
|
||||
this.onDragStart?.(this.offsetX, this.offsetY);
|
||||
}
|
||||
|
||||
private onMouseMoveDrag = (e: MouseEvent) => {
|
||||
document.addEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.addEventListener('mouseup', this.onMouseUpDrag);
|
||||
};
|
||||
|
||||
private onMouseMoveDragRAF = (e: MouseEvent) => {
|
||||
this.dragDX = e.clientX - this.startX;
|
||||
this.dragDY = e.clientY - this.startY;
|
||||
if (!this.pendingDrag) {
|
||||
this.pendingDrag = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.pendingDrag = false;
|
||||
this.applyDragFrame();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private applyDragFrame() {
|
||||
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;
|
||||
let newX = this.offsetX + this.dragDX;
|
||||
let newY = this.offsetY + this.dragDY;
|
||||
|
||||
if (this.snapGrid > 1) {
|
||||
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
|
||||
@@ -275,9 +261,10 @@ export class DraggableResizableWindow {
|
||||
|
||||
this.applyPosition(newX, newY, false);
|
||||
this.onDragMove?.(newX, newY);
|
||||
};
|
||||
}
|
||||
|
||||
private onMouseUpDrag = () => {
|
||||
console.log(111)
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
|
||||
@@ -294,27 +281,19 @@ export class DraggableResizableWindow {
|
||||
this.updateDefaultBounds(snapped.x, snapped.y);
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDrag);
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -332,86 +311,57 @@ export class DraggableResizableWindow {
|
||||
this.applyPosition(x, y, false);
|
||||
this.onDragMove?.(x, y);
|
||||
|
||||
if (progress < 1) {
|
||||
this.animationFrame = requestAnimationFrame(step);
|
||||
} else {
|
||||
this.applyPosition(targetX, targetY, true);
|
||||
this.onDragMove?.(targetX, targetY);
|
||||
onComplete?.();
|
||||
}
|
||||
if (progress < 1) this.animationFrame = requestAnimationFrame(step);
|
||||
else { this.applyPosition(targetX, targetY, true); this.onDragMove?.(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)) {
|
||||
x = Math.min(Math.max(x, 0), this.containerRect.width - rect.width);
|
||||
y = Math.min(Math.max(y, 0), this.containerRect.height - rect.height);
|
||||
} else if (!(this.boundary instanceof HTMLElement) && this.boundary) {
|
||||
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.currentX = x; this.currentY = y;
|
||||
this.applyPosition(x, y, false);
|
||||
}
|
||||
|
||||
private applySnapping(x: number, y: number) {
|
||||
let { x: snappedX, y: snappedY } = { x, y };
|
||||
|
||||
// 1. 容器吸附
|
||||
let snappedX = x, snappedY = y;
|
||||
const containerSnap = this.getSnapPoints();
|
||||
if (this.snapThreshold > 0) {
|
||||
for (const sx of containerSnap.x) {
|
||||
if (Math.abs(x - sx) <= this.snapThreshold) {
|
||||
snappedX = sx;
|
||||
break;
|
||||
for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapThreshold) { snappedX = sx; break; }
|
||||
for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapThreshold) { snappedY = sy; break; }
|
||||
}
|
||||
}
|
||||
for (const sy of containerSnap.y) {
|
||||
if (Math.abs(y - sy) <= this.snapThreshold) {
|
||||
snappedY = sy;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 窗口吸附 TODO
|
||||
|
||||
return { x: snappedX, y: snappedY };
|
||||
}
|
||||
|
||||
private getSnapPoints() {
|
||||
const snapPoints = { x: [] as number[], y: [] as number[] };
|
||||
|
||||
if (this.boundary instanceof HTMLElement && this.containerRect) {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
snapPoints.x = [0, this.containerRect.width - rect.width];
|
||||
snapPoints.y = [0, this.containerRect.height - rect.height];
|
||||
} else if (!(this.boundary instanceof HTMLElement) && this.boundary) {
|
||||
} else if (this.boundary && !(this.boundary instanceof HTMLElement)) {
|
||||
if (this.boundary.minX !== undefined) snapPoints.x.push(this.boundary.minX);
|
||||
if (this.boundary.maxX !== undefined) snapPoints.x.push(this.boundary.maxX);
|
||||
if (this.boundary.minY !== undefined) snapPoints.y.push(this.boundary.minY);
|
||||
if (this.boundary.maxY !== undefined) snapPoints.y.push(this.boundary.maxY);
|
||||
}
|
||||
|
||||
return snapPoints;
|
||||
}
|
||||
|
||||
/** ---------------- 缩放 ---------------- */
|
||||
private onMouseDownResize = (e: MouseEvent) => {
|
||||
const dir = this.getResizeDirection(e);
|
||||
if (!dir) return;
|
||||
@@ -422,65 +372,54 @@ export class DraggableResizableWindow {
|
||||
private startResize(e: MouseEvent, dir: TResizeDirection) {
|
||||
this.currentDirection = dir;
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||
const style = window.getComputedStyle(this.target);
|
||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
||||
this.offsetX = matrix.m41;
|
||||
this.offsetY = matrix.m42;
|
||||
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startWidth = rect.width;
|
||||
this.startHeight = rect.height;
|
||||
this.startTop = rect.top - parentRect.top;
|
||||
this.startLeft = rect.left - parentRect.left;
|
||||
this.startLeft = this.offsetX;
|
||||
this.startTop = this.offsetY;
|
||||
|
||||
document.addEventListener('mousemove', this.onResizeDrag);
|
||||
document.addEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.addEventListener('mouseup', this.onResizeEndHandler);
|
||||
}
|
||||
|
||||
private onResizeDrag = (e: MouseEvent) => {
|
||||
if (!this.currentDirection) return;
|
||||
private onResizeDragRAF = (e: MouseEvent) => {
|
||||
this.resizeDX = e.clientX - this.startX;
|
||||
this.resizeDY = e.clientY - this.startY;
|
||||
if (!this.pendingResize) {
|
||||
this.pendingResize = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.pendingResize = false;
|
||||
this.applyResizeFrame();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let deltaX = e.clientX - this.startX;
|
||||
let deltaY = e.clientY - this.startY;
|
||||
private applyResizeFrame() {
|
||||
if (!this.currentDirection) return;
|
||||
|
||||
let newWidth = this.startWidth;
|
||||
let newHeight = this.startHeight;
|
||||
let newTop = this.startTop;
|
||||
let newLeft = this.startLeft;
|
||||
let newX = this.startLeft;
|
||||
let newY = this.startTop;
|
||||
|
||||
const dx = this.resizeDX;
|
||||
const dy = this.resizeDY;
|
||||
|
||||
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;
|
||||
case 'right': newWidth += dx; break;
|
||||
case 'bottom': newHeight += dy; break;
|
||||
case 'bottom-right': newWidth += dx; newHeight += dy; break;
|
||||
case 'left': newWidth -= dx; newX += dx; break;
|
||||
case 'top': newHeight -= dy; newY += dy; break;
|
||||
case 'top-left': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break;
|
||||
case 'top-right': newWidth += dx; newHeight -= dy; newY += dy; break;
|
||||
case 'bottom-left': newWidth -= dx; newX += dx; newHeight += dy; break;
|
||||
}
|
||||
|
||||
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
|
||||
@@ -488,35 +427,32 @@ export class DraggableResizableWindow {
|
||||
|
||||
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.applyPosition(newX, newY, false);
|
||||
|
||||
this.updateCursor(this.currentDirection);
|
||||
|
||||
this.onResizeMove?.({
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
left: newX,
|
||||
top: newY,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
};
|
||||
|
||||
private onResizeEndHandler = () => {
|
||||
if (this.currentDirection) {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||
this.onResizeEnd?.({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
top: rect.top - parentRect.top,
|
||||
left: rect.left - parentRect.left,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
this.updateDefaultBounds(rect.left - parentRect.left, rect.top - parentRect.top, rect.width, rect.height);
|
||||
}
|
||||
|
||||
private onResizeEndHandler = () => {
|
||||
if (!this.currentDirection) return;
|
||||
this.onResizeEnd?.({
|
||||
width: this.target.offsetWidth,
|
||||
height: this.target.offsetHeight,
|
||||
left: this.currentX,
|
||||
top: this.currentY,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
this.updateDefaultBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight);
|
||||
this.currentDirection = null;
|
||||
this.updateCursor(null);
|
||||
document.removeEventListener('mousemove', this.onResizeDrag);
|
||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
||||
};
|
||||
|
||||
@@ -525,7 +461,6 @@ export class DraggableResizableWindow {
|
||||
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;
|
||||
@@ -539,24 +474,15 @@ export class DraggableResizableWindow {
|
||||
if (bottom) return 'bottom';
|
||||
if (left) return 'left';
|
||||
if (right) return 'right';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private updateCursor(direction: TResizeDirection | null) {
|
||||
if (!direction) {
|
||||
this.target.style.cursor = 'default';
|
||||
return;
|
||||
}
|
||||
if (!direction) { this.target.style.cursor = 'default'; return; }
|
||||
const cursorMap: Record<TResizeDirection, 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',
|
||||
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];
|
||||
}
|
||||
@@ -567,223 +493,66 @@ export class DraggableResizableWindow {
|
||||
this.updateCursor(dir);
|
||||
};
|
||||
|
||||
private onMouseLeave = () => {
|
||||
if (!this.currentDirection && !this.isDragging) this.updateCursor(null);
|
||||
};
|
||||
|
||||
private observeResize(container: HTMLElement) {
|
||||
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.containerRect = container.getBoundingClientRect();
|
||||
this.applyBoundary();
|
||||
});
|
||||
this.resizeObserver.observe(container);
|
||||
this.containerRect = container.getBoundingClientRect();
|
||||
}
|
||||
|
||||
/** 销毁实例 */
|
||||
public destroy() {
|
||||
// 拖拽解绑:只在 handle 上解绑
|
||||
if (this.handle) {
|
||||
this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
|
||||
}
|
||||
|
||||
// 调整尺寸解绑
|
||||
this.target.removeEventListener('mousedown', this.onMouseDownResize);
|
||||
this.target.removeEventListener('mouseleave', this.onMouseLeave);
|
||||
|
||||
// 全局事件解绑
|
||||
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDrag);
|
||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
||||
document.removeEventListener('mousemove', this.onResizeDrag);
|
||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
||||
|
||||
// 观察器清理
|
||||
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||
if (this.mutationObserver) this.mutationObserver.disconnect();
|
||||
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
||||
|
||||
// 所有属性置空,释放内存
|
||||
Object.keys(this).forEach(k => (this as any)[k] = null);
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/** 最小化到任务栏 */
|
||||
/** ---------------- 最小化/最大化 ---------------- */
|
||||
public minimize() {
|
||||
if (this.state === 'minimized') return;
|
||||
this.state = 'minimized';
|
||||
|
||||
// 获取任务栏位置
|
||||
const taskbarElement = document.querySelector(this.taskbarElementId)
|
||||
if (!taskbarElement) throw new Error('任务栏元素未找到');
|
||||
const rect = taskbarElement.getBoundingClientRect()
|
||||
const targetX = rect.left;
|
||||
const targetY = rect.top;
|
||||
const targetWidth = rect.width;
|
||||
const targetHeight = rect.height;
|
||||
|
||||
const startX = this.currentX;
|
||||
const startY = this.currentY;
|
||||
const startWidth = this.target.offsetWidth;
|
||||
const startHeight = this.target.offsetHeight;
|
||||
|
||||
const deltaX = targetX - startX;
|
||||
const deltaY = targetY - startY;
|
||||
const deltaW = targetWidth - startWidth;
|
||||
const deltaH = targetHeight - startHeight;
|
||||
const duration = 400; // Windows 风格稍慢
|
||||
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;
|
||||
const w = startWidth + deltaW * ease;
|
||||
const h = startHeight + deltaH * ease;
|
||||
|
||||
this.target.style.width = `${w}px`;
|
||||
this.target.style.height = `${h}px`;
|
||||
this.applyPosition(x, y, false);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
// 最终值
|
||||
this.target.style.width = `${targetWidth}px`;
|
||||
this.target.style.height = `${targetHeight}px`;
|
||||
this.applyPosition(targetX, targetY, true);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
const taskbar = document.getElementById(this.taskbarElementId);
|
||||
if (!taskbar) return;
|
||||
const rect = taskbar.getBoundingClientRect();
|
||||
this.animateTo(rect.left, rect.top, 200);
|
||||
this.target.style.width = '0px';
|
||||
this.target.style.height = '0px';
|
||||
}
|
||||
|
||||
/** 最大化窗口(带动画) */
|
||||
public maximize(withAnimation = true) {
|
||||
public maximize() {
|
||||
if (this.state === 'maximized') return;
|
||||
this.state = 'maximized';
|
||||
|
||||
// 保存原始 bounds
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
this.targetDefaultBounds = { width: rect.width, height: rect.height, top: rect.top, left: rect.left };
|
||||
const startWidth = rect.width;
|
||||
const startHeight = rect.height;
|
||||
const startX = this.currentX;
|
||||
const startY = this.currentY;
|
||||
|
||||
const targetWidth = this.containerRect?.width ?? window.innerWidth;
|
||||
const targetHeight = this.containerRect?.height ?? window.innerHeight;
|
||||
const targetX = 0;
|
||||
const targetY = 0;
|
||||
|
||||
if (!withAnimation) {
|
||||
this.target.style.width = `${targetWidth}px`;
|
||||
this.target.style.height = `${targetHeight}px`;
|
||||
this.applyPosition(targetX, targetY, true);
|
||||
return;
|
||||
const bounds = { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
|
||||
this.maximizedBounds = bounds;
|
||||
this.animateTo(bounds.left, bounds.top, 200);
|
||||
this.target.style.width = `${bounds.width}px`;
|
||||
this.target.style.height = `${bounds.height}px`;
|
||||
}
|
||||
|
||||
// 动画过渡
|
||||
const deltaX = targetX - startX;
|
||||
const deltaY = targetY - startY;
|
||||
const deltaW = targetWidth - startWidth;
|
||||
const deltaH = targetHeight - startHeight;
|
||||
const duration = 300;
|
||||
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;
|
||||
const w = startWidth + deltaW * ease;
|
||||
const h = startHeight + deltaH * ease;
|
||||
|
||||
this.target.style.width = `${w}px`;
|
||||
this.target.style.height = `${h}px`;
|
||||
this.applyPosition(x, y, false);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
this.target.style.width = `${targetWidth}px`;
|
||||
this.target.style.height = `${targetHeight}px`;
|
||||
this.applyPosition(targetX, targetY, true);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
/** 恢复窗口(带动画,从最大化或最小化) */
|
||||
public restore(withAnimation = true) {
|
||||
public restore() {
|
||||
if (this.state === 'default') return;
|
||||
this.state = 'default';
|
||||
const b = this.targetDefaultBounds;
|
||||
|
||||
const startWidth = this.target.offsetWidth || 0;
|
||||
const startHeight = this.target.offsetHeight || 0;
|
||||
const startLeft = this.currentX;
|
||||
const startTop = this.currentY;
|
||||
|
||||
if (!withAnimation) {
|
||||
this.animateTo(b.left, b.top, 200);
|
||||
this.target.style.width = `${b.width}px`;
|
||||
this.target.style.height = `${b.height}px`;
|
||||
this.applyPosition(b.left, b.top, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = b.left - startLeft;
|
||||
const deltaY = b.top - startTop;
|
||||
const deltaW = b.width - startWidth;
|
||||
const deltaH = b.height - startHeight;
|
||||
const duration = 300;
|
||||
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 = startLeft + deltaX * ease;
|
||||
const y = startTop + deltaY * ease;
|
||||
const w = startWidth + deltaW * ease;
|
||||
const h = startHeight + deltaH * ease;
|
||||
|
||||
this.target.style.width = `${w}px`;
|
||||
this.target.style.height = `${h}px`;
|
||||
this.applyPosition(x, y, false);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
this.target.style.width = `${b.width}px`;
|
||||
this.target.style.height = `${b.height}px`;
|
||||
this.applyPosition(b.left, b.top, true);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
|
||||
/** 更新默认 bounds */
|
||||
private updateDefaultBounds(x?: number, y?: number, width?: number, height?: number) {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
private updateDefaultBounds(left: number, top: number, width?: number, height?: number) {
|
||||
this.targetDefaultBounds = {
|
||||
left: x ?? rect.left,
|
||||
top: y ?? rect.top,
|
||||
width: width ?? rect.width,
|
||||
height: height ?? rect.height,
|
||||
left, top,
|
||||
width: width ?? this.target.offsetWidth,
|
||||
height: height ?? this.target.offsetHeight
|
||||
};
|
||||
}
|
||||
|
||||
/** ---------------- Resize Observer ---------------- */
|
||||
private observeResize(element: HTMLElement) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.containerRect = element.getBoundingClientRect();
|
||||
});
|
||||
this.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
/** ---------------- 销毁 ---------------- */
|
||||
public destroy() {
|
||||
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
|
||||
this.target.removeEventListener('mousedown', this.onMouseDownResize);
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
||||
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
||||
this.resizeObserver?.disconnect();
|
||||
this.mutationObserver.disconnect();
|
||||
cancelAnimationFrame(this.animationFrame ?? 0);
|
||||
}
|
||||
|
||||
private onMouseLeave = () => { this.updateCursor(null); };
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class WindowFormImpl implements IWindowForm {
|
||||
const win = new DraggableResizableWindow({
|
||||
target: dom,
|
||||
handle: div,
|
||||
mode: 'transform',
|
||||
snapAnimation: true,
|
||||
snapThreshold: 20,
|
||||
boundary: document.body,
|
||||
taskbarElementId: '#taskbar',
|
||||
|
||||
Reference in New Issue
Block a user