保存一下
This commit is contained in:
@@ -11,8 +11,8 @@
|
||||
"name": "main",
|
||||
"title": "部门",
|
||||
"icon": "iconfont icon-setting",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"width": 200,
|
||||
"height": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
"name": "main",
|
||||
"title": "音乐",
|
||||
"icon": "iconfont icon-setting",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"width": 200,
|
||||
"height": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
297
src/core/utils/Draggable1.ts
Normal file
297
src/core/utils/Draggable1.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
141
src/core/utils/ResizableFull.ts
Normal file
141
src/core/utils/ResizableFull.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user