diff --git a/src/core/apps/department/app.json b/src/core/apps/department/app.json index e91b057..b8f6d65 100644 --- a/src/core/apps/department/app.json +++ b/src/core/apps/department/app.json @@ -11,8 +11,8 @@ "name": "main", "title": "部门", "icon": "iconfont icon-setting", - "width": 800, - "height": 600 + "width": 200, + "height": 100 } ] } \ No newline at end of file diff --git a/src/core/apps/music/app.json b/src/core/apps/music/app.json index a48339d..5093f01 100644 --- a/src/core/apps/music/app.json +++ b/src/core/apps/music/app.json @@ -11,8 +11,8 @@ "name": "main", "title": "音乐", "icon": "iconfont icon-setting", - "width": 800, - "height": 600 + "width": 200, + "height": 100 } ] } \ No newline at end of file diff --git a/src/core/utils/Draggable.ts b/src/core/utils/Draggable.ts index bea8aab..1cd82c7 100644 --- a/src/core/utils/Draggable.ts +++ b/src/core/utils/Draggable.ts @@ -13,7 +13,17 @@ interface IDraggableOptions { /** 拖拽模式 */ mode?: 'transform' | 'position'; /** 拖拽的边界或容器元素 */ - boundary?: IBoundary | HTMLElement; + boundary?: IBoundaryRect | HTMLElement; + /** 移动步进(网格吸附) */ + snapGrid?: number; + /** 关键点吸附(边界/中心等) */ + snapThreshold?: number; + /** 是否开启吸附动画(拖拽结束时) */ + snapAnimation?: boolean; + /** 拖拽结束时吸附动画时长(ms,默认 200) */ + snapAnimationDuration?: number; + /** 是否允许拖拽超出容器范围 */ + allowOverflow?: boolean; /** 拖拽开始回调 */ onStart?: TDragStartCallback; /** 拖拽移动中回调 */ @@ -23,7 +33,7 @@ interface IDraggableOptions { } /** 拖拽的范围边界 */ -interface IBoundary { +interface IBoundaryRect { /** 最小 X 坐标 */ minX?: number; /** 最大 X 坐标 */ @@ -40,160 +50,278 @@ interface IBoundary { export class Draggable { private handle: HTMLElement; private target: HTMLElement; - private mode: 'transform' | 'position'; - private boundary?: IBoundary; - private containerElement?: HTMLElement; - private containerBounds?: IBoundary; - - private startX = 0; - private startY = 0; - private originX = 0; - private originY = 0; - private currentX = 0; - private currentY = 0; - private dragging = false; - + private boundary?: HTMLElement | IBoundaryRect; + private mode: "transform" | "position"; + private snapGrid: number; + private snapThreshold: number; private onStart?: TDragStartCallback; private onMove?: TDragMoveCallback; private onEnd?: TDragEndCallback; + private snapAnimation: boolean; + private snapAnimationDuration: number; + private allowOverflow: boolean; + 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; constructor(options: IDraggableOptions) { this.handle = options.handle; this.target = options.target; - this.mode = options.mode ?? 'transform'; + 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; - // 判断 boundary 类型 - if (options.boundary instanceof HTMLElement) { - this.containerElement = options.boundary; - this.observeResize(); // 监听容器和目标变化 - } else { - this.boundary = options.boundary; - } - - if (this.mode === 'position') { - const computed = window.getComputedStyle(this.target); - if (computed.position === 'static') { - this.target.style.position = 'absolute'; + // 自动监听 DOM 移除 + this.mutationObserver = new MutationObserver(() => { + if (!document.body.contains(this.target)) { + this.destroy(); } - } - - this.handle.addEventListener('mousedown', this.onMouseDown); - } - - /** 监听容器和目标大小变化 */ - private observeResize() { - this.resizeObserver = new ResizeObserver(() => { - this.updateContainerBounds(); }); + this.mutationObserver.observe(document.body, { childList: true, subtree: true }); - if (this.containerElement) { - this.resizeObserver.observe(this.containerElement); - } - - // 监听目标大小变化 - this.resizeObserver.observe(this.target); - - this.updateContainerBounds(); + this.init(); } - /** 更新边界 */ - private updateContainerBounds() { - if (!this.containerElement) return; - - const containerRect = this.containerElement.getBoundingClientRect(); - const targetRect = this.target.getBoundingClientRect(); - - if (this.mode === 'transform') { - this.containerBounds = { - minX: containerRect.left + window.scrollX, - maxX: containerRect.right + window.scrollX - targetRect.width, - minY: containerRect.top + window.scrollY, - maxY: containerRect.bottom + window.scrollY - targetRect.height, - }; - } else { - this.containerBounds = { - minX: 0, - minY: 0, - maxX: containerRect.width - targetRect.width, - maxY: containerRect.height - targetRect.height, - }; + /** 初始化事件 */ + 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); + this.mutationObserver.disconnect(); + } + + /** 开始拖拽 */ private onMouseDown = (e: MouseEvent) => { e.preventDefault(); - this.handle.style.cursor = 'move'; - this.dragging = true; - - const rect = this.target.getBoundingClientRect(); - this.originX = rect.left + window.scrollX; - this.originY = rect.top + window.scrollY; - - if (this.mode === 'position') { - const style = window.getComputedStyle(this.target); - this.originX = parseFloat(style.left) || 0; - this.originY = parseFloat(style.top) || 0; - } + this.isDragging = true; this.startX = e.clientX; this.startY = e.clientY; - this.currentX = this.originX; - this.currentY = this.originY; - - this.onStart?.(this.currentX, this.currentY); - - document.addEventListener('mousemove', this.onMouseMove); - document.addEventListener('mouseup', this.onMouseUp); - }; - - private onMouseMove = (e: MouseEvent) => { - if (!this.dragging) return; - - let newX = this.originX + (e.clientX - this.startX); - let newY = this.originY + (e.clientY - this.startY); - - const bounds = this.containerBounds || this.boundary; - if (bounds) { - if (bounds.minX !== undefined) newX = Math.max(newX, bounds.minX); - if (bounds.maxX !== undefined) newX = Math.min(newX, bounds.maxX); - if (bounds.minY !== undefined) newY = Math.max(newY, bounds.minY); - if (bounds.maxY !== undefined) newY = Math.min(newY, bounds.maxY); - } - - if (this.mode === 'transform') { - this.target.style.transform = `translate(${newX}px, ${newY}px)`; + if (this.mode === "position") { + const rect = this.target.getBoundingClientRect(); + this.offsetX = rect.left; + this.offsetY = rect.top; } else { - this.target.style.left = `${newX}px`; - this.target.style.top = `${newY}px`; + this.offsetX = this.currentX; + this.offsetY = this.currentY; } - this.currentX = newX; - this.currentY = newY; + document.addEventListener("mousemove", this.onMouseMove); + document.addEventListener("mouseup", this.onMouseUp); - this.onMove?.(this.currentX, this.currentY); + 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 = () => { - this.dragging = false; - this.handle.style.cursor = 'default'; + if (!this.isDragging) return; + this.isDragging = false; - this.onEnd?.(this.currentX, this.currentY); + const snapped = this.applySnapping(this.currentX, this.currentY); - document.removeEventListener('mousemove', this.onMouseMove); - document.removeEventListener('mouseup', this.onMouseUp); + 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); }; - public destroy() { - this.handle.removeEventListener('mousedown', this.onMouseDown); - document.removeEventListener('mousemove', this.onMouseMove); - document.removeEventListener('mouseup', this.onMouseUp); - this.resizeObserver?.disconnect(); + /** 应用位置 */ + 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); + + // 缓动函数(easeOutCubic) + 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): { 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); + snapX.push(containerRect.width - targetRect.width); + + // 上下边界 + snapY.push(0); + snapY.push(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); + } + + 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(); } } - diff --git a/src/core/utils/Draggable1.ts b/src/core/utils/Draggable1.ts new file mode 100644 index 0000000..bd7e2d0 --- /dev/null +++ b/src/core/utils/Draggable1.ts @@ -0,0 +1,297 @@ +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(); + } +} diff --git a/src/core/utils/ResizableFull.ts b/src/core/utils/ResizableFull.ts new file mode 100644 index 0000000..8e53d66 --- /dev/null +++ b/src/core/utils/ResizableFull.ts @@ -0,0 +1,141 @@ +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); + } +} diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts index 3efad28..d651a35 100644 --- a/src/core/window/impl/WindowFormImpl.ts +++ b/src/core/window/impl/WindowFormImpl.ts @@ -50,6 +50,8 @@ export default class WindowFormImpl implements IWindowForm { handle: dom, target: dom, mode: 'position', + snapThreshold: 20, + boundary: document.body }) this.desktopRootDom.appendChild(dom);