diff --git a/src/css/basic.css b/src/css/basic.css index 0272d6b..b82584c 100644 --- a/src/css/basic.css +++ b/src/css/basic.css @@ -45,7 +45,7 @@ body { background-color: var(--color-light); -webkit-font-smoothing: antialiased; /* 字体抗锯齿 */ text-rendering: optimizeLegibility; - overflow-x: hidden; + overflow: hidden; } /* ===== 排版元素 ===== */ diff --git a/src/ui/webComponents/WindowFormElement.ts b/src/ui/webComponents/WindowFormElement.ts index daa8da0..a0d5d66 100644 --- a/src/ui/webComponents/WindowFormElement.ts +++ b/src/ui/webComponents/WindowFormElement.ts @@ -42,10 +42,10 @@ interface IElementRect { width: number; /** 高度 */ height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; + /** x坐标 */ + x: number; + /** y坐标 */ + y: number; } /** 窗口自定义事件 */ @@ -75,38 +75,50 @@ export class WindowFormElement extends LitElement { @property({ type: Boolean }) closable = true @property({ type: Boolean, reflect: true }) focused: boolean = true @property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default' - @property({ type: Object }) dragContainer?: HTMLElement - @property({ type: Boolean }) allowOverflow = true // 允许窗口超出容器 - @property({ type: Number }) snapDistance = 20 // 吸附距离 - @property({ type: Boolean }) snapAnimation = true // 吸附动画 + @property({ type: Object }) dragContainer?: HTMLElement // 元素的父容器 + @property({ type: Number }) snapDistance = 0 // 吸附距离 + @property({ type: Boolean }) snapAnimation = false // 吸附动画 @property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms @property({ type: Number }) maxWidth?: number = Infinity - @property({ type: Number }) minWidth?: number = 0 + @property({ type: Number }) minWidth?: number = 200 @property({ type: Number }) maxHeight?: number = Infinity - @property({ type: Number }) minHeight?: number = 0 + @property({ type: Number }) minHeight?: number = 200 @property({ type: String }) taskbarElementId?: string @property({ type: Object }) wfData: any; private _listeners: Array<{ type: string; original: Function; wrapped: EventListener }> = [] // ==== 拖拽/缩放状态(内部变量,不触发渲染) ==== - private dragging = false - private resizeDir: TResizeDirection | null = null - private startX = 0 - private startY = 0 - private startWidth = 0 - private startHeight = 0 - private startX_host = 0 - private startY_host = 0 - + // 自身的x坐标 private x = 0 + // 自身的y坐标 private y = 0 - private preX = 0 - private preY = 0 + // 自身的宽度 private width = 640 + // 自身的高度 private height = 360 + + // 记录拖拽开始时自身x坐标 + private originalX = 0 + // 记录拖拽开始时自身y坐标 + private originalY = 0 + // 鼠标开始拖拽时自身宽度 + private originalWidth = 640 + // 鼠标开始拖拽时高度 + private originalHeight = 360 + + // 鼠标开始拖拽时x坐标 + private pointStartX = 0 + // 鼠标开始拖拽时x坐标 + private pointStartY = 0 + private animationFrame?: number + // 是否拖拽状态 + private dragging = false + // 是否缩放状态 private resizing = false + // 缩放方向 + private resizeDir: TResizeDirection | null = null // private get x() { // return this.wfData.state.x @@ -198,25 +210,28 @@ export class WindowFormElement extends LitElement { } override firstUpdated() { - console.log(this.wfData) - // wfem.addEventListener('windowFormFocus', this.windowFormFocusFun) - window.addEventListener('pointerup', this.onPointerUp) - window.addEventListener('pointermove', this.onPointerMove) - this.addEventListener('pointerdown', this.toggleFocus) + const { width, height } = this.getBoundingClientRect() + this.width = width || this.width + this.height = height || this.height const container = this.dragContainer || document.body const containerRect = container.getBoundingClientRect() this.x = containerRect.width / 2 - this.width / 2 this.y = containerRect.height / 2 - this.height / 2 + this.style.width = `${this.width}px` this.style.height = `${this.height}px` this.style.transform = `translate(${this.x}px, ${this.y}px)` + window.addEventListener('pointerup', this.onPointerUp) + window.addEventListener('pointermove', this.onPointerMove) + this.addEventListener('pointerdown', this.toggleFocus) + this.targetBounds = { - width: this.offsetWidth, - height: this.offsetHeight, - top: this.x, - left: this.y, + width: this.width, + height: this.height, + x: this.x, + y: this.y, } } @@ -257,10 +272,10 @@ export class WindowFormElement extends LitElement { e.preventDefault() this.dragging = true - this.startX = e.clientX - this.startY = e.clientY - this.preX = this.x - this.preY = this.y + this.pointStartX = e.clientX + this.pointStartY = e.clientY + this.originalX = this.x + this.originalY = this.y this.setPointerCapture?.(e.pointerId) this.dispatchEvent( @@ -272,15 +287,20 @@ export class WindowFormElement extends LitElement { ) } + /** + * 鼠标指针移动 + * @param e + */ private onPointerMove = (e: PointerEvent) => { if (this.dragging) { - const dx = e.clientX - this.startX - const dy = e.clientY - this.startY + const dx = e.clientX - this.pointStartX + const dy = e.clientY - this.pointStartY - const x = this.preX + dx - const y = this.preY + dy + const x = this.originalX + dx + const y = this.originalY + dy - this.applyPosition(x, y, false) + const pos = this.applyBoundary(x, y, e.clientX, e.clientY) + this.applyPosition(pos.x, pos.y) this.dispatchEvent( new CustomEvent('windowForm:dragMove', { detail: { x, y }, @@ -293,6 +313,10 @@ export class WindowFormElement extends LitElement { } } + /** + * 鼠标指针抬起 + * @param e + */ private onPointerUp = (e: PointerEvent) => { if (this.dragging) { this.dragUp(e) @@ -320,13 +344,14 @@ export class WindowFormElement extends LitElement { } /** - * 获取最近的吸附点 - * @param x 左上角起始点x - * @param y 左上角起始点y + * 根据传入的坐标点位计算吸附距离最近的坐标位置 + * @param x 坐标点 x + * @param y 坐标点 y + * @returns {x: number, y: number} 新的位置坐标 */ - private applySnapping(x: number, y: number) { - let snappedX = x, - snappedY = y + private calculateSnapping(x: number, y: number): { x: number, y: number} { + let snappedX = x + let snappedY = y const containerSnap = this.getSnapPoints() if (this.snapDistance > 0) { for (const sx of containerSnap.x) @@ -343,22 +368,28 @@ export class WindowFormElement extends LitElement { return { x: snappedX, y: snappedY } } + /** + * 拖拽结束 + * @param e + * @private + */ private dragUp(e: PointerEvent) { - const snapped = this.applySnapping(this.x, this.y) + const snapped = this.calculateSnapping(this.x, this.y) if (this.snapAnimation) { - this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { - this.updateTargetBounds(snapped.x, snapped.y) - this.dispatchEvent( - new CustomEvent('windowForm:dragEnd', { - detail: { x: snapped.x, y: snapped.y }, - bubbles: true, - composed: true, - }), - ) + this.animateTo(this.x, this.y, snapped.x, snapped.y, this.snapAnimationDuration, + (x, y) => { + this.applyPosition(x, y) + }, + (x, y) => { + this.applyPosition(snapped.x, snapped.y) + this.dispatchEvent(new CustomEvent('windowForm:dragEnd', { + detail: { x: snapped.x, y: snapped.y }, + bubbles: true, + composed: true, + })) }) } else { - this.applyPosition(snapped.x, snapped.y, true) - this.updateTargetBounds(snapped.x, snapped.y) + this.applyPosition(snapped.x, snapped.y) this.dispatchEvent( new CustomEvent('windowForm:dragEnd', { detail: { x: snapped.x, y: snapped.y }, @@ -369,29 +400,65 @@ export class WindowFormElement extends LitElement { } } - private applyPosition(x: number, y: number, isFinal: boolean) { + /** + * 根据鼠标指针的位置是否在容器边界内来限制窗口坐标 + * @param x 当前元素的左上角坐标 x + * @param y 当前元素的左上角坐标 y + * @param pointerX 当前鼠标在容器中的 X 坐标 + * @param pointerY 当前鼠标在容器中的 Y 坐标 + * @returns 限制后的坐标点 { x, y } + */ + private applyBoundary(x: number, y: number, pointerX: number, pointerY: number): { x: number; y: number } { + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + + // 限制指针在容器内 + const limitedPointerX = Math.min(Math.max(pointerX, containerRect.left), containerRect.right) + const limitedPointerY = Math.min(Math.max(pointerY, containerRect.top), containerRect.bottom) + + // 计算指针被限制后的偏移量 + const dx = limitedPointerX - pointerX + const dy = limitedPointerY - pointerY + + // 根据指针偏移调整窗口位置 + x += dx + y += dy + + return { x, y } + } + + /** + * 设置拖拽的窗口位置 + * @param x 当前元素的左上角坐标点 x + * @param y 当前元素的左上角坐标点 y + * @private + */ + private applyPosition(x: number, y: number) { this.x = x this.y = y this.style.transform = `translate(${x}px, ${y}px)` - if (isFinal) this.applyBoundary() } - private applyBoundary() { - if (this.allowOverflow) return - let { x, y } = { x: this.x, y: this.y } - - const rect = this.getBoundingClientRect() - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - x = Math.min(Math.max(x, 0), containerRect.width - rect.width) - y = Math.min(Math.max(y, 0), containerRect.height - rect.height) - - this.applyPosition(x, y, false) - } - - private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) { + /** + * 动画移动窗口 + * @param startX 窗口起始点 x + * @param startY 窗口起始点 y + * @param targetX 目标点 x + * @param targetY 目标点 y + * @param duration 动画时长 + * @param onMove 移动回调 + * @param onComplete 完成回调 + * @private + */ + private animateTo( + startX: number, + startY: number, + targetX: number, + targetY: number, + duration: number, + onMove?: (x: number, y: number) => void, + onComplete?: (x: number, y: number) => void + ) { if (this.animationFrame) cancelAnimationFrame(this.animationFrame) - const startX = this.x - const startY = this.y const deltaX = targetX - startX const deltaY = targetY - startY const startTime = performance.now() @@ -404,7 +471,7 @@ export class WindowFormElement extends LitElement { const x = startX + deltaX * ease const y = startY + deltaY * ease - this.applyPosition(x, y, false) + onMove?.(x, y) this.dispatchEvent( new CustomEvent('windowForm:dragMove', { detail: { x, y }, @@ -416,8 +483,7 @@ export class WindowFormElement extends LitElement { if (progress < 1) { this.animationFrame = requestAnimationFrame(step) } else { - this.applyPosition(targetX, targetY, true) - onComplete?.() + onComplete?.(targetX, targetY) } } this.animationFrame = requestAnimationFrame(step) @@ -432,14 +498,12 @@ export class WindowFormElement extends LitElement { e.stopPropagation() this.resizing = true this.resizeDir = dir - this.startX = e.clientX - this.startY = e.clientY - - const rect = this.getBoundingClientRect() - this.startWidth = rect.width - this.startHeight = rect.height - this.startX_host = rect.left - this.startY_host = rect.top + this.pointStartX = e.clientX + this.pointStartY = e.clientY + this.originalX = this.x + this.originalY = this.y + this.originalWidth = this.width + this.originalHeight = this.height const target = e.target as HTMLElement document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor @@ -454,59 +518,72 @@ export class WindowFormElement extends LitElement { ) } + /** + * 缩放 + * @param e + * @private + */ private performResize(e: PointerEvent) { if (!this.resizeDir || !this.resizing) return - let newWidth = this.startWidth - let newHeight = this.startHeight - let newX = this.startX_host - let newY = this.startY_host + let newWidth = this.originalWidth + let newHeight = this.originalHeight + let newX = this.originalX + let newY = this.originalY - const dx = e.clientX - this.startX - const dy = e.clientY - this.startY + const dx = e.clientX - this.pointStartX + const dy = e.clientY - this.pointStartY + // ====== 根据方向计算临时尺寸与位置 ====== switch (this.resizeDir) { - case 'r': + case 'r': // 右侧 newWidth += dx break - case 'b': + case 'b': // 下方 newHeight += dy break - case 'l': + case 'l': // 左侧 newWidth -= dx newX += dx break - case 't': + case 't': // 上方 newHeight -= dy newY += dy break - case 'tl': + case 'tl': // 左上角 newWidth -= dx newX += dx newHeight -= dy newY += dy break - case 'tr': + case 'tr': // 右上角 newWidth += dx newHeight -= dy newY += dy break - case 'bl': + case 'bl': // 左下角 newWidth -= dx newX += dx newHeight += dy break - case 'br': + case 'br': // 右下角 newWidth += dx newHeight += dy break } - const d = this.applyResizeBounds(newX, newY, newWidth, newHeight) + const { x, y, width, height } = this.applyResizeBounds(newX, newY, newWidth, newHeight, this.resizeDir) + this.x = x + this.y = y + this.width = width + this.height = height + this.style.width = `${this.width}px` + this.style.height = `${this.height}px` + this.style.transform = `translate(${this.x}px, ${this.y}px)` this.dispatchEvent( new CustomEvent('windowForm:resizeMove', { - detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top }, + detail: { dir: this.resizeDir, width: width, height: height, left: x, top: y }, bubbles: true, composed: true, }), @@ -514,68 +591,107 @@ export class WindowFormElement extends LitElement { } /** - * 应用尺寸调整边界 - * @param newX 新的X坐标 - * @param newY 新的Y坐标 - * @param newWidth 新的宽度 - * @param newHeight 新的高度 + * 计算尺寸调整约束逻辑,返回约束后的尺寸 + * @param x 坐标 x + * @param y 坐标 y + * @param width 宽度 + * @param height 高度 * @private + * @returns { x: number, y: number, width: number, height: number } 约束后的尺寸 */ private applyResizeBounds( - newX: number, - newY: number, - newWidth: number, - newHeight: number, + x: number, + y: number, + width: number, + height: number, + resizeDir: TResizeDirection ): { - left: number - top: number + x: number + y: number width: number height: number } { - // 最小/最大宽高限制 - if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth) - if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth) - if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight) - if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight) + const { minWidth = 100, minHeight = 100, maxWidth = Infinity, maxHeight = Infinity } = this - // 边界限制 - if (this.allowOverflow) { - this.x = newX - this.y = newY - this.width = newWidth - this.height = newHeight - this.style.width = `${newWidth}px` - this.style.height = `${newHeight}px` - this.applyPosition(newX, newY, false) + //#region 限制最大/最小尺寸 + // 限制宽度 + if (width < minWidth) { + // 左缩时要修正X坐标,否则会跳动 + if (resizeDir.includes('l')) x -= minWidth - width + width = minWidth + } else if (width > maxWidth) { + if (resizeDir.includes('l')) x += width - maxWidth + width = maxWidth + } - return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, + // 限制高度 + if (height < minHeight) { + if (resizeDir.includes('t')) y -= minHeight - height + height = minHeight + } else if (height > maxHeight) { + if (resizeDir.includes('t')) y += height - maxHeight + height = maxHeight + } + //#endregion + + //#region 限制在容器边界内 + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + const maxLeft = containerRect.width - width + const maxTop = containerRect.height - height + + // 左越界(从左侧缩放时) + if (x < 0) { + if (resizeDir.includes('l')) { + // 如果是往左拉出容器,锁定边界 + width += x // 因为x是负数,相当于减小宽度 + } + x = 0 + } + + // 顶部越界(从上侧缩放时) + if (y < 0) { + if (resizeDir.includes('t')) { + height += y // y是负数,相当于减小高度 + } + y = 0 + } + + // 右越界(从右侧缩放时) + if (x + width > containerRect.width) { + if (resizeDir.includes('r')) { + width = containerRect.width - x + } else { + x = Math.min(x, maxLeft) } } - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - newX = Math.min(Math.max(0, newX), containerRect.width - newWidth) - newY = Math.min(Math.max(0, newY), containerRect.height - newHeight) + // 底部越界(从下侧缩放时) + if (y + height > containerRect.height) { + if (resizeDir.includes('b')) { + height = containerRect.height - y + } else { + y = Math.min(y, maxTop) + } + } + //#endregion - this.x = newX - this.y = newY - this.width = newWidth - this.height = newHeight - this.style.width = `${newWidth}px` - this.style.height = `${newHeight}px` - this.applyPosition(newX, newY, false) + // 二次防护:确保不小于最小值 + width = Math.max(width, minWidth) + height = Math.max(height, minHeight) return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, + x, + y, + width, + height } } + /** + * 缩放结束 + * @param e + * @private + */ private resizeUp(e: PointerEvent) { if (!this.resizable) return @@ -617,12 +733,16 @@ export class WindowFormElement extends LitElement { startY, startW, startH, - rect.left, - rect.top, + rect.x, + rect.y, rect.width, rect.height, 400, - () => { + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) + }, + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) this.style.display = 'none' this.dispatchEvent( new CustomEvent('windowForm:stateChange:minimize', { @@ -667,7 +787,11 @@ export class WindowFormElement extends LitElement { targetW, targetH, 300, - () => { + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) + }, + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) this.dispatchEvent( new CustomEvent('windowForm:stateChange:maximize', { detail: { state: this.windowFormState }, @@ -713,12 +837,16 @@ export class WindowFormElement extends LitElement { startY, startW, startH, - b.left, - b.top, + b.x, + b.y, b.width, b.height, 300, - () => { + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) + }, + (x, y, w, h) => { + this.applyWindowStyle(x, y, w, h) onComplete?.() this.dispatchEvent( new CustomEvent('windowForm:stateChange:restore', { @@ -731,6 +859,24 @@ export class WindowFormElement extends LitElement { ) } + /** + * 应用窗体样式 + * @param x + * @param y + * @param w + * @param h + * @private + */ + private applyWindowStyle(x: number, y: number, w: number, h: number) { + this.width = w + this.height = h + this.x = x + this.y = y + this.style.width = `${w}px` + this.style.height = `${h}px` + this.style.transform = `translate(${x}px, ${y}px)` + } + private windowFormClose() { this.dispatchEvent( new CustomEvent('windowForm:close', { @@ -764,7 +910,8 @@ export class WindowFormElement extends LitElement { targetW: number, targetH: number, duration: number, - onComplete?: () => void, + onUpdating?: (x: number, y: number, w: number, h: number) => void, + onComplete?: (x: number, y: number, w: number, h: number) => void, ) { const startTime = performance.now() const step = (now: number) => { @@ -777,17 +924,14 @@ export class WindowFormElement extends LitElement { const w = startW + (targetW - startW) * ease const h = startH + (targetH - startH) * ease - this.style.width = `${w}px` - this.style.height = `${h}px` - this.applyPosition(x, y, false) + onUpdating?.(x, y, w, h) if (progress < 1) { requestAnimationFrame(step) } else { this.style.width = `${targetW}px` this.style.height = `${targetH}px` - this.applyPosition(targetX, targetY, true) - onComplete?.() + onComplete?.(x, y, w, h) this.dispatchEvent( new CustomEvent('windowForm:stateChange', { detail: { state: this.windowFormState }, @@ -800,10 +944,10 @@ export class WindowFormElement extends LitElement { requestAnimationFrame(step) } - private updateTargetBounds(left: number, top: number, width?: number, height?: number) { + private updateTargetBounds(x: number, y: number, width?: number, height?: number) { this.targetBounds = { - left, - top, + x, + y, width: width ?? this.offsetWidth, height: height ?? this.offsetHeight, }