用于dom拖动位置,以及按住指定dom旋转
export interface DraggableRotatableConfig {
enableRotation?: boolean; // 是否开启旋转功能
dragAxis?: 'x' | 'y' | 'both'; // 拖动方向(x、y 或 both)
disabled?: boolean; // 是否禁用功能
usePercentage?: boolean; // 是否使用百分比
observerClass?: string, // 监听节点及其子元素变化
onUpdate?: (state: { x: number; y: number; rotation: number }) => void; // 实时回调函数
}
export interface State {
x?: number; // X 坐标(像素值或百分比)
y?: number; // Y 坐标(像素值或百分比)
rotation?: number; // 旋转角度
}
export class DraggableRotatableComponent {
private observer: MutationObserver | null; // 容器元素
private container: HTMLElement; // 容器元素
private content: HTMLElement; // 内容元素
private rotateHandle: HTMLElement; // 旋转触发元素
private isDragging: boolean = false; // 是否正在拖动
private isRotating: boolean = false; // 是否正在旋转
private currentX: number = 0; // 当前 X 坐标(像素值)
private currentY: number = 0; // 当前 Y 坐标(像素值)
private initialX: number = 0; // 初始 X 坐标
private initialY: number = 0; // 初始 Y 坐标
private rotation: number = 0; // 当前旋转角度
private startAngle: number = 0; // 开始旋转时的角度
private centerX: number = 0; // 旋转中心 X 坐标
private centerY: number = 0; // 旋转中心 Y 坐标
private zoomScale: number = 1; // 父级的缩放比例
private animationFrameId: number | null = null; // 动画帧 ID
private config: DraggableRotatableConfig; // 配置项
// 绑定事件监听器的引用,用于销毁时解绑
private startDragBound: (e: MouseEvent) => void;
private dragBound: (e: MouseEvent) => void;
private stopDragBound: () => void;
private startRotateBound: (e: MouseEvent) => void;
private rotateBound: (e: MouseEvent) => void;
private stopRotateBound: () => void;
private domChangeBound: (mutationsList: MutationRecord[], observer: MutationObserver) => void;
constructor(
container: HTMLElement,
config: DraggableRotatableConfig = {},
initialState: State = {} // 初始状态
) {
this.observer = null;
this.container = container;
this.container.style.transformOrigin = 'center center';
this.content = container.querySelector('.draggable-content') as HTMLElement;
this.rotateHandle = container.querySelector('.rotate-handle') as HTMLElement;
this.config = {
enableRotation: false, // 默认开启旋转功能
dragAxis: 'both', // 默认允许双向拖动
disabled: false, // 默认不禁用
usePercentage: true, // 默认不使用百分比
...config, // 覆盖默认配置
};
// 检查必要的元素是否存在
if (!this.content || !this.rotateHandle) {
throw new Error('Required elements not found!');
}
// 获取父级的缩放比例
this.zoomScale = this.getParentZoomScale(container);
// 绑定事件监听器的引用
this.startDragBound = this.startDrag.bind(this);
this.dragBound = this.drag.bind(this);
this.stopDragBound = this.stopDrag.bind(this);
this.startRotateBound = this.startRotate.bind(this);
this.rotateBound = this.rotate.bind(this);
this.stopRotateBound = this.stopRotate.bind(this);
this.domChangeBound = this.domChange.bind(this)
// 设置初始状态
this.setState(initialState);
// 初始化事件监听
this.init();
}
/**
* 初始化事件监听
*/
private init(): void {
if (this.config.disabled) {
return; // 如果禁用功能,则不绑定事件
}
if (this.config.observerClass) this.domChangeInit.bind(this)()
// 绑定拖动事件
this.container.addEventListener('mousedown', this.startDragBound);
document.addEventListener('mousemove', this.dragBound);
document.addEventListener('mouseup', this.stopDragBound);
// 如果开启旋转功能,则绑定旋转事件
if (this.config.enableRotation) {
this.rotateHandle.addEventListener('mousedown', this.startRotateBound);
document.addEventListener('mousemove', this.rotateBound);
document.addEventListener('mouseup', this.stopRotateBound);
} else {
// 如果禁用旋转功能,则隐藏旋转触发元素
this.rotateHandle.style.display = 'none';
}
}
/**
* 内容大小变化后重新修改位置和角度信息
*/
private domChangeInit() {
try {
// 内容变化监听
const MutationObserver = window.MutationObserver;
const config = { childList: true, subtree: true, attributes: true };
const targetdom = this.content.querySelector(this.config?.observerClass || '');
if (!targetdom) return;
this.observer = new MutationObserver(this.domChangeBound);
this.observer.observe(targetdom, config);
console.log('domChangeInit successfully');
} catch (_) {
console.log('domChangeInit error');
console.error(_);
}
}
private domChange() {
// const deltaX = Math.round((newRect.width - this.prevRect.width) / this.zoomScale);
// const deltaY = Math.round((newRect.height - this.prevRect.height) / this.zoomScale);
// console.log('deltaX:', newRect, 'deltaY:', this.prevRect);
// // 调整 currentX 和 currentY,确保内容位置不变
// this.currentX -= deltaX / 2;
// this.currentY -= deltaY / 2;
console.log('domSizeZoom');
// // 更新容器的位置和旋转角度
// this.updateTransform();
}
/**
* 开始拖动
* @param e 鼠标事件
*/
private startDrag(e: MouseEvent): void {
if (e.target !== this.rotateHandle) {
this.isDragging = true;
// 考虑缩放比例,计算初始位置
this.initialX = e.clientX / this.zoomScale - this.currentX;
this.initialY = e.clientY / this.zoomScale - this.currentY;
// 使用 requestAnimationFrame 开始动画
this.animationFrameId = requestAnimationFrame(() => this.updateTransform());
}
}
/**
* 拖动中
* @param e 鼠标事件
*/
private drag(e: MouseEvent): void {
if (this.isDragging) {
e.preventDefault();
// 根据配置限制拖动方向
if (this.config.dragAxis === 'x') {
this.currentX = e.clientX / this.zoomScale - this.initialX;
} else if (this.config.dragAxis === 'y') {
this.currentY = e.clientY / this.zoomScale - this.initialY;
} else {
this.currentX = e.clientX / this.zoomScale - this.initialX;
this.currentY = e.clientY / this.zoomScale - this.initialY;
}
// 使用 requestAnimationFrame 优化性能
if (!this.animationFrameId) {
this.animationFrameId = requestAnimationFrame(() => this.updateTransform());
}
}
}
/**
* 停止拖动
*/
private stopDrag(): void {
this.isDragging = false;
// 停止动画
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
/**
* 开始旋转
* @param e 鼠标事件
*/
private startRotate(e: MouseEvent): void {
this.isRotating = true;
// 计算旋转中心点
const rect = this.content.getBoundingClientRect();
this.centerX = rect.left + rect.width / 2;
this.centerY = rect.top + rect.height / 2;
// 计算初始角度
this.startAngle = this.getAngle(e.clientX, e.clientY);
}
/**
* 旋转中
* @param e 鼠标事件
*/
private rotate(e: MouseEvent): void {
if (this.isRotating) {
e.preventDefault();
// 计算当前角度
const currentAngle = this.getAngle(e.clientX, e.clientY);
this.rotation += currentAngle - this.startAngle; // 累积旋转角度
this.startAngle = currentAngle; // 更新起始角度
// 更新旋转角度
this.updateTransform();
}
}
/**
* 停止旋转
*/
private stopRotate(): void {
if (this.isRotating) {
this.isRotating = false;
// 松开时纠正角度到最近的 45 度倍数
this.correctRotation();
}
}
private getClientRect() {
// 获取内容的宽高
const rect = this.content.getBoundingClientRect();
const width = rect.width / this.zoomScale;
const height = rect.height / this.zoomScale;
// 计算中心点偏移
const offsetX = this.config.dragAxis === 'y' ? 0 : width / 2
const offsetY = this.config.dragAxis === 'x' ? 0 : height / 2
return { offsetX, offsetY }
}
/**
* 获取画布元素的宽高
* @returns
*/
private getClientParentRect() {
const parentRect = this.container.parentElement!.getBoundingClientRect();
const parentWidth = parentRect.width / this.zoomScale; // 考虑 zoom 缩放
const parentHeight = parentRect.height / this.zoomScale; // 考虑 zoom 缩放
return { parentWidth, parentHeight }
}
private setTransform() {
// 更新容器的位置和旋转角度
const transform = `translate(${this.currentX}px, ${this.currentY}px) rotate(${this.rotation}deg)`;;
this.container.style.transform = transform;
}
private setUpdateData() {
const { parentWidth, parentHeight } = this.getClientParentRect()
const { offsetX, offsetY } = this.getClientRect()
// 触发回调函数,返回当前状态
if (this.config.onUpdate) {
const state = {
x: this.config.usePercentage
? ((this.currentX + offsetX) / parentWidth) * 100 // 返回百分比
: this.currentX + offsetX, // 返回像素值
y: this.config.usePercentage
? (((this.currentY + offsetY) / parentHeight) * 100) // 返回百分比
: this.currentY + offsetY, // 返回像素值
rotation: this.rotation,
};
this.config.onUpdate(state);
}
}
/**
* 更新容器的位置和旋转角度
*/
private updateTransform(): void {
this.setTransform();
this.setUpdateData();
// 重置动画帧 ID
this.animationFrameId = null;
}
/**
* 纠正旋转角度到最近的 45 度倍数
*/
private correctRotation(): void {
// 计算最近的 45 度倍数
const targetRotation = Math.round(this.rotation / 45) * 45;
// 使用动画平滑过渡到目标角度
const animationDuration = 200; // 动画时长 200ms
const startRotation = this.rotation;
const startTime = performance.now();
const animate = (currentTime: number) => {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / animationDuration, 1);
// 插值计算当前角度
this.rotation = startRotation + (targetRotation - startRotation) * progress;
this.updateTransform();
// 继续动画直到完成
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
/**
* 计算鼠标位置与旋转中心点的角度
* @param clientX 鼠标的 X 坐标
* @param clientY 鼠标的 Y 坐标
* @returns 角度(单位:度)
*/
private getAngle(clientX: number, clientY: number): number {
// 计算鼠标位置与旋转中心点的角度
const deltaX = (clientX - this.centerX) / this.zoomScale;
const deltaY = (clientY - this.centerY) / this.zoomScale;
return (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
}
/**
* 获取父级的缩放比例
* @param element 当前元素
* @returns 缩放比例
*/
private getParentZoomScale(element: HTMLElement): number {
let scale = 1;
let currentElement: HTMLElement | null = element;
// 遍历父级元素,查找是否有 zoom 属性
while (currentElement) {
const zoomValue = window.getComputedStyle(currentElement).zoom;
if (zoomValue && !isNaN(parseFloat(zoomValue))) {
scale *= parseFloat(zoomValue);
}
currentElement = currentElement.parentElement;
}
return scale;
}
/**
* 更新配置
* @param config 新的配置项
*/
public updateConfig(config: DraggableRotatableConfig): void {
this.config = { ...this.config, ...config }; // 合并配置
// 如果禁用功能,则移除事件监听
if (this.config.disabled) {
this.destroy();
} else {
// 否则重新初始化事件监听
this.init();
}
}
/**
* 设置状态(外部更新旋转角度和位置)
* @param state 新的状态
*/
public setState(state: State): void {
// 获取画布宽高
const { parentWidth, parentHeight } = this.getClientParentRect();
// 获取内容的宽高
const { offsetX, offsetY } = this.getClientRect()
// 更新 X 坐标
if (state.x !== undefined) {
if (this.config.usePercentage) {
this.currentX = (state.x) / 100 * parentWidth - offsetX; // 百分比转换为像素值
} else {
this.currentX = state.x - offsetX; // 直接使用像素值
}
}
// 更新 Y 坐标
if (state.y !== undefined) {
if (this.config.usePercentage) {
this.currentY = (state.y) / 100 * parentHeight - offsetY; // 百分比转换为像素值
} else {
this.currentY = state.y - offsetY; // 直接使用像素值
}
}
// 更新旋转角度
if (state.rotation !== undefined) {
this.rotation = state.rotation;
}
// 更新位置和旋转角度
this.updateTransform();
}
/**
* 销毁实例,清理事件监听和引用
*/
public destroy(): void {
// 移除拖动事件
this.container.removeEventListener('mousedown', this.startDragBound);
document.removeEventListener('mousemove', this.dragBound);
document.removeEventListener('mouseup', this.stopDragBound);
// 移除旋转事件
this.rotateHandle.removeEventListener('mousedown', this.startRotateBound);
document.removeEventListener('mousemove', this.rotateBound);
document.removeEventListener('mouseup', this.stopRotateBound);
// 停止动画
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
this.observer?.disconnect();
// 清理引用
this.container = null!;
this.content = null!;
this.rotateHandle = null!;
}
}
使用方法
// 初始化组件
const draggableContainer: HTMLElement | null = document.querySelector('.draggable-container');
if (draggableContainer) {
const component = new DraggableRotatableComponent(
draggableContainer,
{
enableRotation: true, // 开启旋转功能
dragAxis: 'both', // 允许双向拖动
disabled: false, // 不禁用
usePercentage: true, // 使用百分比
onUpdate: (state) => {
console.log('Current state:', state); // 实时打印当前状态
},
},
{
x: 50, // 初始 X 坐标为 50%
y: 50, // 初始 Y 坐标为 50%
rotation: 45, // 初始旋转角度为 45 度
}
);
// 动态更新配置
// component.updateConfig({ enableRotation: false }); // 禁用旋转功能
// component.updateConfig({ dragAxis: 'x' }); // 仅允许沿 X 轴拖动
// component.updateConfig({ disabled: true }); // 禁用所有功能
// 销毁实例
// component.destroy();
} else {
console.error('Draggable container not found!');
}