完善和修复一下DraggableResizableWindow的bug

This commit is contained in:
2025-09-07 15:23:04 +08:00
parent fcf3c2933c
commit d43664c945
2 changed files with 120 additions and 33 deletions

View File

@@ -17,7 +17,7 @@ type TResizeDirection =
| 'bottom-right'; | 'bottom-right';
/** 窗口状态 */ /** 窗口状态 */
type WindowState = 'default' | 'minimized' | 'maximized'; type TWindowState = 'default' | 'minimized' | 'maximized';
/** 元素边界 */ /** 元素边界 */
interface IElementRect { interface IElementRect {
@@ -152,9 +152,13 @@ export class DraggableResizableWindow {
private mutationObserver: MutationObserver; private mutationObserver: MutationObserver;
private animationFrame?: number; private animationFrame?: number;
private state: WindowState = 'default'; private state: TWindowState = 'default';
private targetDefaultBounds: IElementRect; /** 元素信息 */
private maximizedBounds?: IElementRect; private targetBounds: IElementRect;
/** 最小化前的元素信息 */
private targetPreMinimizeBounds?: IElementRect;
/** 最大化前的元素信息 */
private targetPreMaximizedBounds?: IElementRect;
private taskbarElementId: string; private taskbarElementId: string;
constructor(options: IDraggableResizableOptions) { constructor(options: IDraggableResizableOptions) {
@@ -178,17 +182,20 @@ export class DraggableResizableWindow {
this.onResizeMove = options.onResizeMove; this.onResizeMove = options.onResizeMove;
this.onResizeEnd = options.onResizeEnd; this.onResizeEnd = options.onResizeEnd;
this.targetDefaultBounds = { requestAnimationFrame(() => {
this.targetBounds = {
width: this.target.offsetWidth, width: this.target.offsetWidth,
height: this.target.offsetHeight, height: this.target.offsetHeight,
top: this.target.offsetTop, top: this.target.offsetTop,
left: this.target.offsetLeft, left: this.target.offsetLeft,
}; };
});
this.taskbarElementId = options.taskbarElementId; this.taskbarElementId = options.taskbarElementId;
this.target.style.position = "absolute"; this.target.style.position = "absolute";
this.target.style.left = `${this.target.offsetLeft}px`; this.target.style.left = '0px';
this.target.style.top = `${this.target.offsetTop}px`; this.target.style.top = '0px';
this.target.style.transform = "translate(0px, 0px)"; this.target.style.transform = "translate(0px, 0px)";
this.init(); this.init();
@@ -216,11 +223,21 @@ export class DraggableResizableWindow {
} }
} }
/** ---------------- 拖拽 ---------------- */
private onMouseDownDrag = (e: MouseEvent) => { private onMouseDownDrag = (e: MouseEvent) => {
if (this.getResizeDirection(e)) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
if (e.target !== this.handle) return;
if (this.getResizeDirection(e)) return;
if (this.state === 'maximized') {
this.restore(() => this.startDrag(e));
} else {
this.startDrag(e);
}
}
private startDrag = (e: MouseEvent) => {
this.isDragging = true; this.isDragging = true;
this.startX = e.clientX; this.startX = e.clientX;
this.startY = e.clientY; this.startY = e.clientY;
@@ -237,6 +254,7 @@ export class DraggableResizableWindow {
}; };
private onMouseMoveDragRAF = (e: MouseEvent) => { private onMouseMoveDragRAF = (e: MouseEvent) => {
e.stopPropagation();
this.dragDX = e.clientX - this.startX; this.dragDX = e.clientX - this.startX;
this.dragDY = e.clientY - this.startY; this.dragDY = e.clientY - this.startY;
if (!this.pendingDrag) { if (!this.pendingDrag) {
@@ -263,7 +281,8 @@ export class DraggableResizableWindow {
this.onDragMove?.(newX, newY); this.onDragMove?.(newX, newY);
} }
private onMouseUpDrag = () => { private onMouseUpDrag = (e: MouseEvent) => {
e.stopPropagation();
if (!this.isDragging) return; if (!this.isDragging) return;
this.isDragging = false; this.isDragging = false;
@@ -331,7 +350,8 @@ export class DraggableResizableWindow {
if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY); 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); this.applyPosition(x, y, false);
} }
@@ -360,14 +380,20 @@ export class DraggableResizableWindow {
return snapPoints; return snapPoints;
} }
/** ---------------- 缩放 ---------------- */
private onMouseDownResize = (e: MouseEvent) => { private onMouseDownResize = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const dir = this.getResizeDirection(e); const dir = this.getResizeDirection(e);
if (!dir) return; if (!dir) return;
e.preventDefault();
this.startResize(e, dir); this.startResize(e, dir);
}; };
private onMouseLeave = (e: MouseEvent) => {
e.stopPropagation();
this.updateCursor(null);
};
private startResize(e: MouseEvent, dir: TResizeDirection) { private startResize(e: MouseEvent, dir: TResizeDirection) {
this.currentDirection = dir; this.currentDirection = dir;
const rect = this.target.getBoundingClientRect(); const rect = this.target.getBoundingClientRect();
@@ -388,6 +414,7 @@ export class DraggableResizableWindow {
} }
private onResizeDragRAF = (e: MouseEvent) => { private onResizeDragRAF = (e: MouseEvent) => {
e.stopPropagation();
this.resizeDX = e.clientX - this.startX; this.resizeDX = e.clientX - this.startX;
this.resizeDY = e.clientY - this.startY; this.resizeDY = e.clientY - this.startY;
if (!this.pendingResize) { if (!this.pendingResize) {
@@ -424,9 +451,7 @@ export class DraggableResizableWindow {
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth)); newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight)); newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
this.target.style.width = `${newWidth}px`; this.applyResizeBounds(newX, newY, newWidth, newHeight);
this.target.style.height = `${newHeight}px`;
this.applyPosition(newX, newY, false);
this.updateCursor(this.currentDirection); this.updateCursor(this.currentDirection);
@@ -439,7 +464,41 @@ export class DraggableResizableWindow {
}); });
} }
private onResizeEndHandler = () => { // 应用尺寸调整边界
private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number) {
// 最小/最大宽高限制
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
// 边界限制
if (!this.boundary || this.allowOverflow) {
this.currentX = newX;
this.currentY = newY;
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.applyPosition(newX, newY, false);
return;
}
if (this.boundary instanceof HTMLElement && this.containerRect) {
newX = Math.min(Math.max(0, newX), this.containerRect.width - newWidth);
newY = Math.min(Math.max(0, newY), this.containerRect.height - newHeight);
} else if (!(this.boundary instanceof HTMLElement) && this.boundary) {
if (this.boundary.minX !== undefined) newX = Math.max(newX, this.boundary.minX);
if (this.boundary.maxX !== undefined) newX = Math.min(newX, this.boundary.maxX);
if (this.boundary.minY !== undefined) newY = Math.max(newY, this.boundary.minY);
if (this.boundary.maxY !== undefined) newY = Math.min(newY, this.boundary.maxY);
}
this.currentX = newX;
this.currentY = newY;
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.applyPosition(newX, newY, false);
}
private onResizeEndHandler = (e?: MouseEvent) => {
e?.stopPropagation();
if (!this.currentDirection) return; if (!this.currentDirection) return;
this.onResizeEnd?.({ this.onResizeEnd?.({
width: this.target.offsetWidth, width: this.target.offsetWidth,
@@ -457,7 +516,7 @@ export class DraggableResizableWindow {
private getResizeDirection(e: MouseEvent): TResizeDirection | null { private getResizeDirection(e: MouseEvent): TResizeDirection | null {
const rect = this.target.getBoundingClientRect(); const rect = this.target.getBoundingClientRect();
const offset = 8; const offset = 4;
const x = e.clientX; const x = e.clientX;
const y = e.clientY; const y = e.clientY;
const top = y >= rect.top && y <= rect.top + offset; const top = y >= rect.top && y <= rect.top + offset;
@@ -487,6 +546,7 @@ export class DraggableResizableWindow {
} }
private onDocumentMouseMoveCursor = (e: MouseEvent) => { private onDocumentMouseMoveCursor = (e: MouseEvent) => {
e.stopPropagation();
if (this.currentDirection || this.isDragging) return; if (this.currentDirection || this.isDragging) return;
const dir = this.getResizeDirection(e); const dir = this.getResizeDirection(e);
this.updateCursor(dir); this.updateCursor(dir);
@@ -495,6 +555,7 @@ export class DraggableResizableWindow {
// 最小化到任务栏 // 最小化到任务栏
public minimize() { public minimize() {
if (this.state === 'minimized') return; if (this.state === 'minimized') return;
this.targetPreMinimizeBounds = { ...this.targetBounds }
this.state = 'minimized'; this.state = 'minimized';
const taskbar = document.querySelector(this.taskbarElementId); const taskbar = document.querySelector(this.taskbarElementId);
@@ -506,16 +567,19 @@ export class DraggableResizableWindow {
const startW = this.target.offsetWidth; const startW = this.target.offsetWidth;
const startH = this.target.offsetHeight; const startH = this.target.offsetHeight;
this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400); this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400, () => {
this.target.style.display = 'none';
});
} }
/** 最大化 */ /** 最大化 */
public maximize() { public maximize() {
if (this.state === 'maximized') return; if (this.state === 'maximized') return;
this.targetPreMinimizeBounds = { ...this.targetBounds }
this.state = 'maximized'; this.state = 'maximized';
const rect = this.target.getBoundingClientRect(); const rect = this.target.getBoundingClientRect();
this.targetDefaultBounds = { width: rect.width, height: rect.height, left: rect.left, top: rect.top }; this.targetBounds = { width: rect.width, height: rect.height, left: rect.left, top: rect.top };
const startX = this.currentX; const startX = this.currentX;
const startY = this.currentY; const startY = this.currentY;
@@ -531,17 +595,28 @@ export class DraggableResizableWindow {
} }
/** 恢复到默认窗体状态 */ /** 恢复到默认窗体状态 */
public restore() { public restore(onComplete?: () => void) {
if (this.state === 'default') return; if (this.state === 'default') return;
this.state = 'default'; this.state = 'default';
const b = this.targetDefaultBounds; let b: IElementRect;
if ((this.state as TWindowState) === 'minimized' && this.targetPreMinimizeBounds) {
// 最小化恢复,恢复到最小化前的状态
b = this.targetPreMinimizeBounds;
} else if ((this.state as TWindowState) === 'maximized' && this.targetPreMaximizedBounds) {
// 最大化恢复,恢复到最大化前的默认状态
b = this.targetPreMaximizedBounds;
} else {
b = this.targetBounds;
}
this.target.style.display = 'block';
const startX = this.currentX; const startX = this.currentX;
const startY = this.currentY; const startY = this.currentY;
const startW = this.target.offsetWidth; const startW = this.target.offsetWidth;
const startH = this.target.offsetHeight; const startH = this.target.offsetHeight;
this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300); this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300, onComplete);
} }
/** /**
@@ -598,14 +673,14 @@ export class DraggableResizableWindow {
} }
private updateDefaultBounds(left: number, top: number, width?: number, height?: number) { private updateDefaultBounds(left: number, top: number, width?: number, height?: number) {
this.targetDefaultBounds = { this.targetBounds = {
left, top, left, top,
width: width ?? this.target.offsetWidth, width: width ?? this.target.offsetWidth,
height: height ?? this.target.offsetHeight height: height ?? this.target.offsetHeight
}; };
} }
/** ---------------- Resize Observer ---------------- */ /** 监听元素变化 */
private observeResize(element: HTMLElement) { private observeResize(element: HTMLElement) {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.containerRect = element.getBoundingClientRect(); this.containerRect = element.getBoundingClientRect();
@@ -613,7 +688,9 @@ export class DraggableResizableWindow {
this.resizeObserver.observe(element); this.resizeObserver.observe(element);
} }
/** ---------------- 销毁 ---------------- */ /**
* 销毁实例
*/
public destroy() { public destroy() {
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag); if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
this.target.removeEventListener('mousedown', this.onMouseDownResize); this.target.removeEventListener('mousedown', this.onMouseDownResize);
@@ -626,6 +703,4 @@ export class DraggableResizableWindow {
this.mutationObserver.disconnect(); this.mutationObserver.disconnect();
cancelAnimationFrame(this.animationFrame ?? 0); cancelAnimationFrame(this.animationFrame ?? 0);
} }
private onMouseLeave = () => { this.updateCursor(null); };
} }

View File

@@ -56,6 +56,9 @@ export default class WindowFormImpl implements IWindowForm {
bt1.innerText = '最小化'; bt1.innerText = '最小化';
bt1.addEventListener('click', () => { bt1.addEventListener('click', () => {
win.minimize(); win.minimize();
setTimeout(() => {
win.restore();
}, 2000)
}) })
div.appendChild(bt1) div.appendChild(bt1)
const bt2 = document.createElement('button'); const bt2 = document.createElement('button');
@@ -73,6 +76,15 @@ export default class WindowFormImpl implements IWindowForm {
processManager.removeProcess(this.proc!) processManager.removeProcess(this.proc!)
}) })
div.appendChild(bt3) div.appendChild(bt3)
const bt4 = document.createElement('button');
bt4.innerText = '恢复';
bt4.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation()
win.restore();
})
div.appendChild(bt4)
const win = new DraggableResizableWindow({ const win = new DraggableResizableWindow({
target: dom, target: dom,