diff --git a/src/core/desktop/DesktopProcess.ts b/src/core/desktop/DesktopProcess.ts index 79571a0..7e46763 100644 --- a/src/core/desktop/DesktopProcess.ts +++ b/src/core/desktop/DesktopProcess.ts @@ -84,6 +84,7 @@ export class DesktopProcess extends ProcessImpl { dom.style.width = `${this._width}px` dom.style.height = `${this._height}px` dom.style.position = 'relative' + dom.style.overflow = 'hidden' this._desktopRootDom = dom const app = createApp(DesktopComponent, { process: this }) diff --git a/src/core/utils/Draggable.ts b/src/core/utils/Draggable.ts new file mode 100644 index 0000000..bea8aab --- /dev/null +++ b/src/core/utils/Draggable.ts @@ -0,0 +1,199 @@ +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?: IBoundary | HTMLElement; + /** 拖拽开始回调 */ + onStart?: TDragStartCallback; + /** 拖拽移动中回调 */ + onMove?: TDragMoveCallback; + /** 拖拽结束回调 */ + onEnd?: TDragEndCallback; +} + +/** 拖拽的范围边界 */ +interface IBoundary { + /** 最小 X 坐标 */ + minX?: number; + /** 最大 X 坐标 */ + maxX?: number; + /** 最小 Y 坐标 */ + minY?: number; + /** 最大 Y 坐标 */ + maxY?: number; +} + +/** + * 拖拽功能通用类 + */ +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 onStart?: TDragStartCallback; + private onMove?: TDragMoveCallback; + private onEnd?: TDragEndCallback; + + private resizeObserver?: ResizeObserver; + + constructor(options: IDraggableOptions) { + this.handle = options.handle; + this.target = options.target; + this.mode = options.mode ?? 'transform'; + 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'; + } + } + + this.handle.addEventListener('mousedown', this.onMouseDown); + } + + /** 监听容器和目标大小变化 */ + private observeResize() { + this.resizeObserver = new ResizeObserver(() => { + this.updateContainerBounds(); + }); + + if (this.containerElement) { + this.resizeObserver.observe(this.containerElement); + } + + // 监听目标大小变化 + this.resizeObserver.observe(this.target); + + this.updateContainerBounds(); + } + + /** 更新边界 */ + 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 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.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)`; + } else { + this.target.style.left = `${newX}px`; + this.target.style.top = `${newY}px`; + } + + this.currentX = newX; + this.currentY = newY; + + this.onMove?.(this.currentX, this.currentY); + }; + + private onMouseUp = () => { + this.dragging = false; + this.handle.style.cursor = 'default'; + + this.onEnd?.(this.currentX, this.currentY); + + 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(); + } +} + diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts index 8920dbc..3efad28 100644 --- a/src/core/window/impl/WindowFormImpl.ts +++ b/src/core/window/impl/WindowFormImpl.ts @@ -5,6 +5,7 @@ import type { IWindowForm } from '@/core/window/IWindowForm.ts' import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts' import type { WindowFormPos } from '@/core/window/types/WindowFormTypes.ts' import { processManager } from '@/core/process/ProcessManager.ts' +import { Draggable } from '@/core/utils/Draggable.ts' export default class WindowFormImpl implements IWindowForm { private readonly _id: string = uuidV4(); @@ -45,6 +46,11 @@ export default class WindowFormImpl implements IWindowForm { dom.style.height = `${this.height}px`; dom.style.zIndex = '100'; dom.style.backgroundColor = 'white'; + new Draggable( { + handle: dom, + target: dom, + mode: 'position', + }) this.desktopRootDom.appendChild(dom); }