初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
/**
* Moveable Events Handler
* 拖拽与缩放事件处理逻辑
*/
import type Moveable from 'moveable';
import type { HTMLEditor } from '../editor';
import { createStyleChangeCommand } from '../historyManager/commands';
export class MoveableEventsHandler {
private editor: HTMLEditor;
constructor(editor: HTMLEditor) {
this.editor = editor;
}
/**
* 绑定拖拽事件
*/
bindDragEvents(instance: Moveable) {
let originalTransform: string | null = null;
let originTransition : string | null = null;
instance.on('dragStart', ({ target, inputEvent }) => {
const el = target as HTMLElement;
originalTransform = el.style.transform || '';
originTransition = el.style.transition || '';
try {
inputEvent?.preventDefault();
} catch {}
el.style.userSelect = 'none';
el.style.transition = 'none';
this.editor.setDragging(true);
});
instance.on('drag', ({ target, transform }) => {
const el = target as HTMLElement;
el.style.transform = transform;
this.editor.emit('styleChange', el, { transform });
});
instance.on('dragEnd', ({ target }) => {
const el = target as HTMLElement;
// 记录历史
if (this.editor.historyManager && originalTransform !== null) {
const newTransform = el.style.transform || '';
if (originalTransform !== newTransform) {
const command = createStyleChangeCommand(el, 'transform', originalTransform, newTransform);
command.execute();
this.editor.historyManager.push(command);
}
}
el.style.transition = originTransition || '';
this.editor.setDragging(false);
this.editor.emit('contentChange');
originalTransform = null;
originTransition = null;
});
}
/**
* 绑定缩放事件
*/
bindScaleEvents(instance: Moveable) {
let originalTransform: string | null = null;
instance.on('scaleStart', (e) => {
this.editor.setResizing(true);
e.target.blur();
// 记录初始状态
const el = e.target as HTMLElement;
originalTransform = el.style.transform || '';
});
instance.on('scale', ({ target, transform, drag }) => {
const el = target as HTMLElement;
el.style.transform = drag.transform;
this.editor.emit('styleChange', el, {
transform: drag && drag.transform ? drag.transform : transform,
});
});
instance.on('scaleEnd', ({ target }) => {
const el = target as HTMLElement;
// 记录历史
if (this.editor.historyManager && originalTransform !== null) {
const newTransform = el.style.transform || '';
if (originalTransform !== newTransform) {
const command = createStyleChangeCommand(el, 'transform', originalTransform, newTransform);
command.execute();
this.editor.historyManager.push(command);
}
}
this.editor.setResizing(false);
this.editor.emit('contentChange');
originalTransform = null;
});
}
/**
* 绑定缩放事件
*/
bindResizeEvents(instance: Moveable) {
let originalSize = { width: '0px', height: '0px' };
let originalTransform: string | null = '';
let originTransition : string | null = null;
instance.on('resizeStart', (e) => {
const ele = e.target as HTMLElement;
const style = window.getComputedStyle(ele);
this.editor.setResizing(true);
e.target.blur();
// 记录初始状态
originalTransform = e.target.style.transform || '';
originTransition = e.target.style.transition || '';
// 记录初始大小
originalSize = {
width: style.width,
height: style.height,
};
})
instance.on('resize', (e) => {
const el = e.target as HTMLElement;
el.style.width = e.width + 'px'
el.style.height = e.height + 'px';
el.style.transform = e.transform;
this.editor.emit('styleChange', el, {
transform: e && e.transform ? e.transform : '',
});
});
instance.on('resizeEnd', (e) => {
const el = e.target as HTMLElement;
this.editor.setResizing(false);
this.editor.emit('contentChange');
const newTransform = el.style.transform
if (originalTransform !== newTransform) {
//
this.editor.historyManager?.beginBatch();
const command = createStyleChangeCommand(el, 'transform', originalTransform, newTransform);
command.execute();
this.editor.historyManager?.push(command);
const sizeCommand = createStyleChangeCommand(el, 'width', originalSize.width, el.style.width);
sizeCommand.execute();
this.editor.historyManager?.push(sizeCommand);
const heightCommand = createStyleChangeCommand(el, 'height', originalSize.height, el.style.height);
heightCommand.execute();
this.editor.historyManager?.push(heightCommand);
this.editor.historyManager?.endBatch();
}
el.style.transition = originTransition || '';
originalTransform = null;
originTransition = null;
});
}
/**
* 绑定所有事件
*/
bindAllEvents(instance: Moveable) {
this.bindDragEvents(instance);
this.bindScaleEvents(instance);
this.bindResizeEvents(instance);
}
}

View File

@@ -0,0 +1,74 @@
export class MoveableGuidelinesHandler {
/**
* 计算自动对齐参考线
*/
static calculateAutoGuidelines(
element: HTMLElement,
container: HTMLElement,
elementGuidelinesOption: HTMLElement[] | undefined
): HTMLElement[] {
if (elementGuidelinesOption) {
return elementGuidelinesOption;
}
return Array.from(container.querySelectorAll<HTMLElement>('*')).filter((el) => {
if (el === element) return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const visible =
style.display !== 'none' &&
style.visibility !== 'hidden' &&
rect.width > 0 &&
rect.height > 0;
return visible;
});
}
/**
* 计算水平标尺线
*/
static calculateHorizontalGuidelines(
container: HTMLElement,
horizontalGuidelinesOption: number[] | undefined
): number[] {
if (horizontalGuidelinesOption) {
return horizontalGuidelinesOption;
}
return [
0,
Math.round(container.clientHeight / 2),
container.clientHeight,
];
}
/**
* 计算垂直标尺线
*/
static calculateVerticalGuidelines(
container: HTMLElement,
verticalGuidelinesOption: number[] | undefined
): number[] {
if (verticalGuidelinesOption) {
return verticalGuidelinesOption;
}
return [
0,
Math.round(container.clientWidth / 2),
container.clientWidth,
];
}
/**
* 获取容器元素
*/
static getContainer(
element: HTMLElement,
editorContainer: HTMLElement | null,
snapContainerOption: HTMLElement | null
): HTMLElement {
const root = element.ownerDocument?.body || document.body;
return snapContainerOption ?? (editorContainer || element.parentElement || root);
}
}

View File

@@ -0,0 +1,193 @@
/**
* Moveable Manager
* 实现选中元素的拖拽与四角缩放
*/
import Moveable from 'moveable';
import { type HTMLEditor } from '../editor';
import type { MoveableOptions } from '../../types';
import { MoveableEventsHandler } from './events';
import { MoveableGuidelinesHandler } from './guidelines';
export class MoveableManager {
private editor: HTMLEditor;
private instance: Moveable | null = null;
private options: MoveableOptions;
private eventsHandler: MoveableEventsHandler;
// 记录启用前的属性,便于恢复
private originalState: {
contenteditable?: string | null;
userSelect?: string | null;
transformOrigin?: string | null;
} = {};
constructor(editor: HTMLEditor, options: MoveableOptions = {}) {
this.editor = editor;
this.eventsHandler = new MoveableEventsHandler(editor);
this.options = {
draggable: true,
scalable: false,
resizable: true,
renderDirections: ['nw', 'ne', 'sw', 'se', 'n', 's', 'w', 'e'],
keepRatio: false,
throttleDrag: 0,
throttleResize: 0,
throttleScale: 0,
// 默认开启吸附与标尺线
snappable: true,
snapCenter: true,
snapThreshold: 5,
snapGridWidth: undefined,
snapGridHeight: undefined,
snapContainer: null,
elementGuidelines: undefined,
horizontalGuidelines: undefined,
verticalGuidelines: undefined,
snapDirections: {
left: true,
top: true,
right: true,
bottom: true,
center: true,
middle: true,
},
...options,
};
}
enableFor(element: HTMLElement, options?: Partial<MoveableOptions>): void {
this.destroy();
// 启用前准备:禁用 contenteditable 与选择,避免拖拽被当作文本选择
this.prepareElement(element);
const mergedOptions: MoveableOptions = { ...this.options, ...(options || {}) };
// 获取容器元素
const container = MoveableGuidelinesHandler.getContainer(
element,
this.editor.container,
mergedOptions.snapContainer ?? null
);
// 计算自动对齐参考线
const autoGuidelines = MoveableGuidelinesHandler.calculateAutoGuidelines(
element,
container,
mergedOptions.elementGuidelines
);
// 计算水平标尺线
const hGuides = MoveableGuidelinesHandler.calculateHorizontalGuidelines(
container,
mergedOptions.horizontalGuidelines
);
// 计算垂直标尺线
const vGuides = MoveableGuidelinesHandler.calculateVerticalGuidelines(
container,
mergedOptions.verticalGuidelines
);
const root = element.ownerDocument?.body || document.body;
this.instance = new Moveable(root, {
target: element,
draggable: mergedOptions.draggable,
scalable: mergedOptions.scalable,
resizable: mergedOptions.resizable,
edgeDraggable: true,
checkInput: true,
origin: false,
// 缩放手柄
renderDirections: mergedOptions.renderDirections,
keepRatio: mergedOptions.keepRatio,
// 性能相关
throttleDrag: mergedOptions.throttleDrag,
throttleScale: mergedOptions.throttleScale,
// 吸附与对齐线
snappable: mergedOptions.snappable,
snapContainer: container,
elementGuidelines: autoGuidelines,
horizontalGuidelines: hGuides,
verticalGuidelines: vGuides,
// 提高阈值,避免吸附过强导致"拖不动"的感觉
snapThreshold: mergedOptions.snapThreshold ?? 10,
snapGridWidth: mergedOptions.snapGridWidth,
snapGridHeight: mergedOptions.snapGridHeight,
snapDirections: mergedOptions.snapDirections,
});
// 绑定拖拽和缩放事件
this.eventsHandler.bindAllEvents(this.instance);
}
update() {
if (this.instance) {
this.instance.updateRect();
}
}
destroy() {
if (this.instance) {
this.instance.destroy();
this.instance = null;
}
// 恢复元素的原始状态
const el = this.editor.selectedElement;
if (el) {
this.restoreElement(el);
}
}
private prepareElement(element: HTMLElement) {
// 保存原始状态
this.originalState.contenteditable =
element.getAttribute('contenteditable');
this.originalState.userSelect = element.style.userSelect || null;
this.originalState.transformOrigin = element.style.transformOrigin || null;
element.setAttribute('contenteditable', 'false');
element.style.userSelect = 'none';
(element.style as any).touchAction = 'none';
}
private restoreElement(element: HTMLElement) {
// 恢复 contenteditable
if (this.originalState.contenteditable != null) {
if (this.originalState.contenteditable === '') {
element.removeAttribute('contenteditable');
} else {
element.setAttribute(
'contenteditable',
this.originalState.contenteditable
);
}
} else {
element.removeAttribute('contenteditable');
}
// 恢复 user-select
if (this.originalState.userSelect != null) {
element.style.userSelect = this.originalState.userSelect || '';
} else {
element.style.removeProperty('user-select');
}
// 恢复 transform-origin
if (this.originalState.transformOrigin != null) {
element.style.transformOrigin = this.originalState.transformOrigin || '';
} else {
element.style.removeProperty('transform-origin');
}
// 清理 will-change
element.style.removeProperty('will-change');
// 清空记录
this.originalState = {};
}
}