Files
vue-desktop/src/ui/webComponents/WindowFormElement.ts
2025-11-11 17:32:41 +08:00

1050 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { LitElement, html, css, unsafeCSS } from 'lit'
import { customElement, property } from 'lit/decorators.js';
import wfStyle from './css/wf.scss?inline'
type TWindowFormState = 'default' | 'minimized' | 'maximized';
/** 拖拽移动开始的回调 */
type TDragStartCallback = (x: number, y: number) => void;
/** 拖拽移动中的回调 */
type TDragMoveCallback = (x: number, y: number) => void;
/** 拖拽移动结束的回调 */
type TDragEndCallback = (x: number, y: number) => void;
/** 拖拽调整尺寸的方向 */
type TResizeDirection =
| 't' // 上
| 'b' // 下
| 'l' // 左
| 'r' // 右
| 'tl' // 左上
| 'tr' // 右上
| 'bl' // 左下
| 'br'; // 右下
/** 拖拽调整尺寸回调数据 */
interface IResizeCallbackData {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
/** 拖拽调整尺寸的方向 */
direction: TResizeDirection | null;
}
/** 元素边界 */
interface IElementRect {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** x坐标 */
x: number;
/** y坐标 */
y: number;
}
/** 窗口自定义事件 */
export interface WindowFormEventMap extends HTMLElementEventMap {
'windowForm:dragStart': CustomEvent<TDragStartCallback>;
'windowForm:dragMove': CustomEvent<TDragMoveCallback>;
'windowForm:dragEnd': CustomEvent<TDragEndCallback>;
'windowForm:resizeStart': CustomEvent<IResizeCallbackData>;
'windowForm:resizeMove': CustomEvent<IResizeCallbackData>;
'windowForm:resizeEnd': CustomEvent<IResizeCallbackData>;
'windowForm:stateChange': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:minimize': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>;
'windowForm:close': CustomEvent;
'windowForm:minimize': CustomEvent;
}
@customElement('window-form-element')
export class WindowFormElement extends LitElement {
// ==== 公共属性 ====
@property({ type: String }) wid: string
@property({ type: String }) override title = 'Window'
@property({ type: Boolean }) resizable = true
@property({ type: Boolean }) minimizable = true
@property({ type: Boolean }) maximizable = true
@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: 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 = 200
@property({ type: Number }) maxHeight?: number = Infinity
@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 }> = []
// ==== 拖拽/缩放状态(内部变量,不触发渲染) ====
// 自身的x坐标
private x = 0
// 自身的y坐标
private y = 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
// }
// private set x(value: number) {
// this.wfData.patch({ x: value })
// }
// private get y() {
// return this.wfData.state.y
// }
// private set y(value: number) {
// this.wfData.patch({ y: value })
// }
// private windowFormState: TWindowFormState = 'default';
/** 元素信息 */
private targetBounds: IElementRect
/** 最小化前的元素信息 */
private targetPreMinimizeBounds?: IElementRect
/** 最大化前的元素信息 */
private targetPreMaximizedBounds?: IElementRect
static override styles = css`
${unsafeCSS(wfStyle)}
`
protected override createRenderRoot() {
const root = this.attachShadow({ mode: 'closed' })
const sheet = new CSSStyleSheet()
sheet.replaceSync(wfStyle)
root.adoptedStyleSheets = [sheet]
return root
}
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler: (this: WindowFormElement, ev: WindowFormEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler: (ev: WindowFormEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
/**
* 添加受管理的事件监听
* @param type 事件类型
* @param handler 事件处理函数
*/
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler:
| ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any)
| ((ev: WindowFormEventMap[K]) => any),
options?: boolean | AddEventListenerOptions
) {
const wrapped: EventListener = (ev: Event) => {
(handler as any).call(this, ev as WindowFormEventMap[K])
}
this.addEventListener(type, wrapped, options)
this._listeners.push({ type, original: handler, wrapped })
}
public removeManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler:
| ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any)
| ((ev: WindowFormEventMap[K]) => any)
) {
const index = this._listeners.findIndex(
l => l.type === type && l.original === handler
)
if (index !== -1) {
const { type: t, wrapped } = this._listeners[index]
this.removeEventListener(t, wrapped)
this._listeners.splice(index, 1)
}
}
/**
* 移除所有受管理事件监听
*/
public removeAllManagedListeners() {
for (const { type, wrapped } of this._listeners) {
this.removeEventListener(type, wrapped)
}
this._listeners = []
}
override firstUpdated() {
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.width,
height: this.height,
x: this.x,
y: this.y,
}
}
override disconnectedCallback() {
super.disconnectedCallback()
window.removeEventListener('pointerup', this.onPointerUp)
window.removeEventListener('pointermove', this.onPointerMove)
this.removeEventListener('pointerdown', this.toggleFocus)
// wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun)
this.removeAllManagedListeners()
}
private windowFormFocusFun = (id: string) => {
if (id === this.wid) {
this.focused = true
} else {
this.focused = false
}
}
private toggleFocus = () => {
this.focused = !this.focused
// wfem.notifyEvent('windowFormFocus', this.wid)
this.dispatchEvent(
new CustomEvent('update:focused', {
detail: this.focused,
bubbles: true,
composed: true,
}),
)
}
// ====== 拖拽 ======
private onTitlePointerDown = (e: PointerEvent) => {
if (e.pointerType === 'mouse' && e.button !== 0) return
if ((e.target as HTMLElement).closest('.controls')) return
e.preventDefault()
this.dragging = true
this.pointStartX = e.clientX
this.pointStartY = e.clientY
this.originalX = this.x
this.originalY = this.y
this.setPointerCapture?.(e.pointerId)
this.dispatchEvent(
new CustomEvent('windowForm:dragStart', {
detail: { x: this.x, y: this.y },
bubbles: true,
composed: true,
}),
)
}
/**
* 鼠标指针移动
* @param e
*/
private onPointerMove = (e: PointerEvent) => {
if (this.dragging) {
const dx = e.clientX - this.pointStartX
const dy = e.clientY - this.pointStartY
const x = this.originalX + dx
const y = this.originalY + dy
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 },
bubbles: true,
composed: true,
}),
)
} else if (this.resizeDir) {
this.performResize(e)
}
}
/**
* 鼠标指针抬起
* @param e
*/
private onPointerUp = (e: PointerEvent) => {
if (this.dragging) {
this.dragUp(e)
}
if (this.resizeDir) {
this.resizeUp(e)
}
this.dragging = false
this.resizing = false
this.resizeDir = null
document.body.style.cursor = ''
try {
this.releasePointerCapture?.(e.pointerId)
} catch {}
}
/** 获取所有吸附点 */
private getSnapPoints() {
const snapPoints = { x: [] as number[], y: [] as number[] }
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
const rect = this.getBoundingClientRect()
snapPoints.x = [0, containerRect.width - rect.width]
snapPoints.y = [0, containerRect.height - rect.height]
return snapPoints
}
/**
* 根据传入的坐标点位计算吸附距离最近的坐标位置
* @param x 坐标点 x
* @param y 坐标点 y
* @returns {x: number, y: number} 新的位置坐标
*/
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)
if (Math.abs(x - sx) <= this.snapDistance) {
snappedX = sx
break
}
for (const sy of containerSnap.y)
if (Math.abs(y - sy) <= this.snapDistance) {
snappedY = sy
break
}
}
return { x: snappedX, y: snappedY }
}
/**
* 拖拽结束
* @param e
* @private
*/
private dragUp(e: PointerEvent) {
const snapped = this.calculateSnapping(this.x, this.y)
if (this.snapAnimation) {
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)
this.dispatchEvent(
new CustomEvent('windowForm:dragEnd', {
detail: { x: snapped.x, y: snapped.y },
bubbles: true,
composed: true,
}),
)
}
this.updateTargetBounds(this.x, this.y, this.width, this.height)
}
/**
* 根据鼠标指针的位置是否在容器边界内来限制窗口坐标
* @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)`
}
/**
* 动画移动窗口
* @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 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)
const x = startX + deltaX * ease
const y = startY + deltaY * ease
onMove?.(x, y)
this.dispatchEvent(
new CustomEvent('windowForm:dragMove', {
detail: { x, y },
bubbles: true,
composed: true,
}),
)
if (progress < 1) {
this.animationFrame = requestAnimationFrame(step)
} else {
onComplete?.(targetX, targetY)
}
}
this.animationFrame = requestAnimationFrame(step)
}
// ====== 缩放 ======
private startResize = (dir: TResizeDirection, e: PointerEvent) => {
if (!this.resizable) return
if (e.pointerType === 'mouse' && e.button !== 0) return
e.preventDefault()
e.stopPropagation()
this.resizing = true
this.resizeDir = dir
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
this.setPointerCapture?.(e.pointerId)
this.dispatchEvent(
new CustomEvent('windowForm:resizeStart', {
detail: { x: this.x, y: this.y, width: this.width, height: this.height, dir },
bubbles: true,
composed: true,
}),
)
}
/**
* 缩放
* @param e
* @private
*/
private performResize(e: PointerEvent) {
if (!this.resizeDir || !this.resizing) return
let newWidth = this.originalWidth
let newHeight = this.originalHeight
let newX = this.originalX
let newY = this.originalY
const dx = e.clientX - this.pointStartX
const dy = e.clientY - this.pointStartY
// ====== 根据方向计算临时尺寸与位置 ======
switch (this.resizeDir) {
case 'r': // 右侧
newWidth += dx
break
case 'b': // 下方
newHeight += dy
break
case 'l': // 左侧
newWidth -= dx
newX += dx
break
case 't': // 上方
newHeight -= dy
newY += dy
break
case 'tl': // 左上角
newWidth -= dx
newX += dx
newHeight -= dy
newY += dy
break
case 'tr': // 右上角
newWidth += dx
newHeight -= dy
newY += dy
break
case 'bl': // 左下角
newWidth -= dx
newX += dx
newHeight += dy
break
case 'br': // 右下角
newWidth += dx
newHeight += dy
break
}
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: width, height: height, left: x, top: y },
bubbles: true,
composed: true,
}),
)
}
/**
* 计算尺寸调整约束逻辑,返回约束后的尺寸
* @param x 坐标 x
* @param y 坐标 y
* @param width 宽度
* @param height 高度
* @private
* @returns { x: number, y: number, width: number, height: number } 约束后的尺寸
*/
private applyResizeBounds(
x: number,
y: number,
width: number,
height: number,
resizeDir: TResizeDirection
): {
x: number
y: number
width: number
height: number
} {
const { minWidth = 100, minHeight = 100, maxWidth = Infinity, maxHeight = Infinity } = this
//#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
}
// 限制高度
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)
}
}
// 底部越界(从下侧缩放时)
if (y + height > containerRect.height) {
if (resizeDir.includes('b')) {
height = containerRect.height - y
} else {
y = Math.min(y, maxTop)
}
}
//#endregion
// 二次防护:确保不小于最小值
width = Math.max(width, minWidth)
height = Math.max(height, minHeight)
return {
x,
y,
width,
height
}
}
/**
* 缩放结束
* @param e
* @private
*/
private resizeUp(e: PointerEvent) {
if (!this.resizable) return
this.updateTargetBounds(this.x, this.y, this.width, this.height)
this.dispatchEvent(
new CustomEvent('windowForm:resizeEnd', {
detail: {
dir: this.resizeDir,
width: this.width,
height: this.height,
left: this.x,
top: this.y,
},
bubbles: true,
composed: true,
}),
)
}
// ====== 窗口操作 ======
// 最小化到任务栏
private minimize() {
if (!this.taskbarElementId) return
if (this.windowFormState === 'minimized') return
this.targetPreMinimizeBounds = { ...this.targetBounds }
this.windowFormState = 'minimized'
const taskbar = document.querySelector(this.taskbarElementId)
if (!taskbar) throw new Error('任务栏元素未找到')
const rect = taskbar.getBoundingClientRect()
const startX = this.x
const startY = this.y
const startW = this.offsetWidth
const startH = this.offsetHeight
this.animateWindow(
startX,
startY,
startW,
startH,
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', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
})
)
}
)
}
/** 最大化 */
private maximize() {
if (this.windowFormState === 'maximized') {
this.restore()
return
}
this.targetPreMaximizedBounds = { ...this.targetBounds }
this.windowFormState = 'maximized'
const rect = this.getBoundingClientRect()
const startX = this.x
const startY = this.y
const startW = rect.width
const startH = rect.height
const targetX = 0
const targetY = 0
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
const targetW = containerRect?.width ?? window.innerWidth
const targetH = containerRect?.height ?? window.innerHeight
this.animateWindow(
startX,
startY,
startW,
startH,
targetX,
targetY,
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 },
bubbles: true,
composed: true,
}),
)
},
)
}
/** 恢复到默认窗体状态 */
private restore(onComplete?: () => void) {
if (this.windowFormState === 'default') return
let b: IElementRect
if (
(this.windowFormState as TWindowFormState) === 'minimized' &&
this.targetPreMinimizeBounds
) {
// 最小化恢复,恢复到最小化前的状态
b = this.targetPreMinimizeBounds
} else if (
(this.windowFormState as TWindowFormState) === 'maximized' &&
this.targetPreMaximizedBounds
) {
// 最大化恢复,恢复到最大化前的默认状态
b = this.targetPreMaximizedBounds
} else {
b = this.targetBounds
}
this.windowFormState = 'default'
this.style.display = 'block'
const startX = this.x
const startY = this.y
const startW = this.offsetWidth
const startH = this.offsetHeight
this.animateWindow(
startX,
startY,
startW,
startH,
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', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
}),
)
},
)
}
/**
* 应用窗体样式
* @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', {
bubbles: true,
composed: true,
}),
)
}
/**
* 窗体最大化、最小化和恢复默认 动画
* @param startX
* @param startY
* @param startW
* @param startH
* @param targetX
* @param targetY
* @param targetW
* @param targetH
* @param duration
* @param onComplete
* @private
*/
private animateWindow(
startX: number,
startY: number,
startW: number,
startH: number,
targetX: number,
targetY: number,
targetW: number,
targetH: number,
duration: number,
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) => {
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
const ease = 1 - Math.pow(1 - progress, 3)
const x = startX + (targetX - startX) * ease
const y = startY + (targetY - startY) * ease
const w = startW + (targetW - startW) * ease
const h = startH + (targetH - startH) * ease
onUpdating?.(x, y, w, h)
if (progress < 1) {
requestAnimationFrame(step)
} else {
this.style.width = `${targetW}px`
this.style.height = `${targetH}px`
onComplete?.(x, y, w, h)
this.dispatchEvent(
new CustomEvent('windowForm:stateChange', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
})
)
}
}
requestAnimationFrame(step)
}
private updateTargetBounds(x: number, y: number, width?: number, height?: number) {
this.targetBounds = {
x,
y,
width: width ?? this.offsetWidth,
height: height ?? this.offsetHeight,
}
}
// ====== 渲染 ======
override render() {
return html`
<div class="window">
<div class="titlebar" @pointerdown=${this.onTitlePointerDown}>
<div class="title" title=${this.title}>${this.title}</div>
<div class="controls">
${this.minimizable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.minimize()
}}
>
</button>`
: null}
${this.maximizable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.maximize()
}}
>
${this.windowFormState === 'maximized' ? '▣' : '▢'}
</button>`
: null}
${this.closable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.windowFormClose()
}}
>
</button>`
: null}
</div>
</div>
<div class="content"><slot></slot></div>
${this.resizable
? html`
<div
class="resizer t"
@pointerdown=${(e: PointerEvent) => this.startResize('t', e)}
></div>
<div
class="resizer b"
@pointerdown=${(e: PointerEvent) => this.startResize('b', e)}
></div>
<div
class="resizer r"
@pointerdown=${(e: PointerEvent) => this.startResize('r', e)}
></div>
<div
class="resizer l"
@pointerdown=${(e: PointerEvent) => this.startResize('l', e)}
></div>
<div
class="resizer tr"
@pointerdown=${(e: PointerEvent) => this.startResize('tr', e)}
></div>
<div
class="resizer tl"
@pointerdown=${(e: PointerEvent) => this.startResize('tl', e)}
></div>
<div
class="resizer br"
@pointerdown=${(e: PointerEvent) => this.startResize('br', e)}
></div>
<div
class="resizer bl"
@pointerdown=${(e: PointerEvent) => this.startResize('bl', e)}
></div>
`
: null}
</div>
`
}
}
declare global {
interface HTMLElementTagNameMap {
'window-form-element': WindowFormElement;
}
interface WindowFormElementEventMap extends WindowFormEventMap {}
}