refactor(stage): 将被操作元素和拖拽框抽取出来放到DragResizeHelper中

Squash merge branch 'feature/ocean_stagerefactor_880128993' into 'master'
refactor(stage): 在拖拽过程中,moveable会产生一些状态事件,DragResizeHelper对这些状态事件中对将被操作元素和拖拽框的状态和dom进行处理。
This commit is contained in:
oceanzhu 2022-11-29 11:36:59 +08:00
parent 3291530b32
commit 164d777ebf
6 changed files with 413 additions and 238 deletions

View File

@ -54,4 +54,9 @@ mask是一个盖在画布区域的一个蒙层主要作用是隔离鼠标事
StageDragResize、StageMultiDragResize的父类负责管理Moveable的配置 StageDragResize、StageMultiDragResize的父类负责管理Moveable的配置
<br/><br/> <br/><br/>
## TargetShadow ## TargetShadow
统一管理拖拽和高亮框,包括创建、更新、销毁。 统一管理拖拽框和高亮框,包括创建、更新、销毁。
<br/><br/>
## DragResizeHelper
- 拖拽/改变大小等操作发生时moveable会抛出各种状态事件DragResizeHelper负责响应这些事件对目标节点target和拖拽节点targetShadow进行修改
- 其中目标节点是DragResizeHelper直接改的targetShadow作为直接被操作的拖拽框是调用moveableHelper改的
- 有个特殊情况是流式布局下moveableHelper不支持targetShadow也是DragResizeHelper直接改的

View File

@ -0,0 +1,338 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
OnDrag,
OnDragGroup,
OnDragGroupStart,
OnDragStart,
OnResize,
OnResizeGroup,
OnResizeGroupStart,
OnResizeStart,
OnRotate,
OnRotateStart,
OnScale,
OnScaleStart,
} from 'moveable';
import MoveableHelper from 'moveable-helper';
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const';
import TargetShadow from './TargetShadow';
import { DragResizeHelperConfig, TargetElement } from './types';
import { getAbsolutePosition, getOffset } from './util';
/**
* /moveable会抛出各种状态事件DragResizeHelper负责响应这些事件target和拖拽节点targetShadow进行修改
* DragResizeHelper直接改的targetShadow作为直接被操作的拖拽框moveableHelper改的
* moveableHelper不支持targetShadow也是DragResizeHelper直接改的
*/
export default class DragResizeHelper {
/** 目标节点在蒙层上的占位节点,用于跟鼠标交互,避免鼠标事件直接作用到目标节点 */
private targetShadow: TargetShadow;
/** 要操作的原始目标节点 */
private target!: HTMLElement;
/** 多选:目标节点组 */
private targetList: HTMLElement[] = [];
/** targetShadowdom
* MoveableHelper里面的方法是成员属性DragResizeHelper用继承的方式将无法通过super去调这些方法 */
private moveableHelper: MoveableHelper;
/** 流式布局下,目标节点的镜像节点 */
private ghostEl: HTMLElement | undefined;
/** 用于记录节点被改变前的位置 */
private frameSnapShot = {
left: 0,
top: 0,
};
/** 多选模式下的多个节点 */
private framesSnapShot: { left: number; top: number; id: string }[] = [];
/** 布局方式:流式布局、绝对定位、固定定位 */
private mode: Mode = Mode.ABSOLUTE;
constructor(config: DragResizeHelperConfig) {
this.moveableHelper = MoveableHelper.create({
useBeforeRender: true,
useRender: false,
createAuto: true,
});
this.targetShadow = new TargetShadow({
container: config.container,
updateDragEl: config.updateDragEl,
zIndex: ZIndex.DRAG_EL,
idPrefix: DRAG_EL_ID_PREFIX,
});
}
public destroy(): void {
this.targetShadow.destroy();
this.destroyGhostEl();
this.moveableHelper.clear();
}
public destroyShadowEl(): void {
this.targetShadow.destroyEl();
}
public getShadowEl(): TargetElement | undefined {
return this.targetShadow.el;
}
public updateShadowEl(el: HTMLElement): void {
this.destroyGhostEl();
this.target = el;
this.targetShadow.update(el);
}
public setMode(mode: Mode): void {
this.mode = mode;
}
/**
*
* @param e dommoveableHelper会直接修改拖拽节点
*/
public onResizeStart(e: OnResizeStart): void {
this.moveableHelper.onResizeStart(e);
this.frameSnapShot.top = this.target.offsetTop;
this.frameSnapShot.left = this.target.offsetLeft;
}
public onResize(e: OnResize): void {
const { width, height, drag } = e;
const { beforeTranslate } = drag;
// 流式布局
if (this.mode === Mode.SORTABLE) {
this.target.style.top = '0px';
if (this.targetShadow.el) {
this.targetShadow.el.style.width = `${width}px`;
this.targetShadow.el.style.height = `${height}px`;
}
} else {
this.moveableHelper.onResize(e);
this.target.style.left = `${this.frameSnapShot.left + beforeTranslate[0]}px`;
this.target.style.top = `${this.frameSnapShot.top + beforeTranslate[1]}px`;
}
this.target.style.width = `${width}px`;
this.target.style.height = `${height}px`;
}
public onDragStart(e: OnDragStart): void {
this.moveableHelper.onDragStart(e);
if (this.mode === Mode.SORTABLE) {
this.ghostEl = this.generateGhostEl(this.target);
}
this.frameSnapShot.top = this.target.offsetTop;
this.frameSnapShot.left = this.target.offsetLeft;
}
public onDrag(e: OnDrag): void {
// 流式布局
if (this.ghostEl) {
this.ghostEl.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1]}px`;
return;
}
this.moveableHelper.onDrag(e);
this.target.style.left = `${this.frameSnapShot.left + e.beforeTranslate[0]}px`;
this.target.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1]}px`;
}
public onRotateStart(e: OnRotateStart): void {
this.moveableHelper.onRotateStart(e);
}
public onRotate(e: OnRotate): void {
this.moveableHelper.onRotate(e);
const frame = this.moveableHelper.getFrame(e.target);
this.target.style.transform = frame?.toCSSObject().transform || '';
}
public onScaleStart(e: OnScaleStart): void {
this.moveableHelper.onScaleStart(e);
}
public onScale(e: OnScale): void {
this.moveableHelper.onScale(e);
const frame = this.moveableHelper.getFrame(e.target);
this.target.style.transform = frame?.toCSSObject().transform || '';
}
public getGhostEl(): HTMLElement | undefined {
return this.ghostEl;
}
public destroyGhostEl(): void {
this.ghostEl?.remove();
this.ghostEl = undefined;
}
public clear(): void {
this.moveableHelper.clear();
}
public getFrame(el: HTMLElement | SVGElement): ReturnType<MoveableHelper['getFrame']> {
return this.moveableHelper.getFrame(el);
}
public getShadowEls(): TargetElement[] {
return this.targetShadow.els;
}
public updateGroup(els: HTMLElement[]): void {
this.targetList = els;
this.framesSnapShot = [];
this.targetShadow.updateGroup(els);
}
public setTargetList(targetList: HTMLElement[]): void {
this.targetList = targetList;
}
public clearMultiSelectStatus(): void {
this.targetList = [];
this.targetShadow.destroyEls();
}
public onResizeGroupStart(e: OnResizeGroupStart): void {
const { events } = e;
this.moveableHelper.onResizeGroupStart(e);
this.setFramesSnapShot(events);
}
/**
*
*/
public onResizeGroup(e: OnResizeGroup): void {
const { events } = e;
// 拖动过程更新
events.forEach((ev) => {
const { width, height, beforeTranslate } = ev.drag;
const frameSnapShot = this.framesSnapShot.find(
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!frameSnapShot) return;
const targeEl = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!targeEl) return;
// 元素与其所属组同时加入多选列表时,只更新父元素
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
if (!isParentIncluded) {
// 更新页面元素位置
targeEl.style.left = `${frameSnapShot.left + beforeTranslate[0]}px`;
targeEl.style.top = `${frameSnapShot.top + beforeTranslate[1]}px`;
}
// 更新页面元素大小
targeEl.style.width = `${width}px`;
targeEl.style.height = `${height}px`;
});
this.moveableHelper.onResizeGroup(e);
}
public onDragGroupStart(e: OnDragGroupStart): void {
const { events } = e;
this.moveableHelper.onDragGroupStart(e);
// 记录拖动前快照
this.setFramesSnapShot(events);
}
public onDragGroup(e: OnDragGroup): void {
const { events } = e;
// 拖动过程更新
events.forEach((ev) => {
const frameSnapShot = this.framesSnapShot.find(
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!frameSnapShot) return;
const targeEl = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!targeEl) return;
// 元素与其所属组同时加入多选列表时,只更新父元素
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
if (!isParentIncluded) {
// 更新页面元素位置
targeEl.style.left = `${frameSnapShot.left + ev.beforeTranslate[0]}px`;
targeEl.style.top = `${frameSnapShot.top + ev.beforeTranslate[1]}px`;
}
});
this.moveableHelper.onDragGroup(e);
}
/**
*
*/
private setFramesSnapShot(events: OnDragStart[] | OnResizeStart[]): void {
// 同一组被选中的目标元素多次拖拽和改变大小会触发多次setFramesSnapShot只有第一次可以设置成功
if (this.framesSnapShot.length > 0) return;
// 记录拖动前快照
events.forEach((ev) => {
// 实际目标元素
const matchEventTarget = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!matchEventTarget) return;
this.framesSnapShot.push({
left: matchEventTarget.offsetLeft,
top: matchEventTarget.offsetTop,
id: matchEventTarget.id,
});
});
}
/**
*
*/
private generateGhostEl(el: HTMLElement): HTMLElement {
if (this.ghostEl) {
this.destroyGhostEl();
}
const ghostEl = el.cloneNode(true) as HTMLElement;
this.setGhostElChildrenId(ghostEl);
const { top, left } = getAbsolutePosition(el, getOffset(el));
ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
ghostEl.style.zIndex = ZIndex.GHOST_EL;
ghostEl.style.opacity = '.5';
ghostEl.style.position = 'absolute';
ghostEl.style.left = `${left}px`;
ghostEl.style.top = `${top}px`;
el.after(ghostEl);
return ghostEl;
}
private setGhostElChildrenId(el: Element): void {
for (const child of Array.from(el.children)) {
if (child.id) {
child.id = `${GHOST_EL_ID_PREFIX}${child.id}`;
}
if (child.children.length) {
this.setGhostElChildrenId(child);
}
}
}
}

View File

@ -18,14 +18,13 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Moveable, { MoveableOptions } from 'moveable'; import Moveable, { MoveableOptions } from 'moveable';
import MoveableHelper from 'moveable-helper';
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const'; import { Mode } from './const';
import DragResizeHelper from './DragResizeHelper';
import MoveableOptionsManager from './MoveableOptionsManager'; import MoveableOptionsManager from './MoveableOptionsManager';
import TargetShadow from './TargetShadow';
import type { DelayedMarkContainer, GetRenderDocument, MarkContainerEnd, StageDragResizeConfig } from './types'; import type { DelayedMarkContainer, GetRenderDocument, MarkContainerEnd, StageDragResizeConfig } from './types';
import { StageDragStatus } from './types'; import { StageDragStatus } from './types';
import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, up } from './util'; import { calcValueByFontsize, down, getMode, getOffset, up } from './util';
/** /**
* moveableOption参数并初始化moveablemoveable回调事件对组件进行更新 * moveableOption参数并初始化moveablemoveable回调事件对组件进行更新
@ -34,15 +33,11 @@ import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, up
export default class StageDragResize extends MoveableOptionsManager { export default class StageDragResize extends MoveableOptionsManager {
/** 目标节点 */ /** 目标节点 */
private target?: HTMLElement; private target?: HTMLElement;
/** 目标节点在蒙层中的占位节点 */
private targetShadow: TargetShadow;
/** Moveable拖拽类实例 */ /** Moveable拖拽类实例 */
private moveable?: Moveable; private moveable?: Moveable;
/** 拖动状态 */ /** 拖动状态 */
private dragStatus: StageDragStatus = StageDragStatus.END; private dragStatus: StageDragStatus = StageDragStatus.END;
/** 流式布局下,目标节点的镜像节点 */ private dragResizeHelper: DragResizeHelper;
private ghostEl: HTMLElement | undefined;
private moveableHelper?: MoveableHelper;
private getRenderDocument: GetRenderDocument; private getRenderDocument: GetRenderDocument;
private markContainerEnd: MarkContainerEnd; private markContainerEnd: MarkContainerEnd;
private delayedMarkContainer: DelayedMarkContainer; private delayedMarkContainer: DelayedMarkContainer;
@ -54,11 +49,9 @@ export default class StageDragResize extends MoveableOptionsManager {
this.markContainerEnd = config.markContainerEnd; this.markContainerEnd = config.markContainerEnd;
this.delayedMarkContainer = config.delayedMarkContainer; this.delayedMarkContainer = config.delayedMarkContainer;
this.targetShadow = new TargetShadow({ this.dragResizeHelper = new DragResizeHelper({
container: config.container, container: config.container,
updateDragEl: config.updateDragEl, updateDragEl: config.updateDragEl,
zIndex: ZIndex.DRAG_EL,
idPrefix: DRAG_EL_ID_PREFIX,
}); });
this.on('update-moveable', () => { this.on('update-moveable', () => {
@ -75,11 +68,8 @@ export default class StageDragResize extends MoveableOptionsManager {
* @param event * @param event
*/ */
public select(el: HTMLElement, event?: MouseEvent): void { public select(el: HTMLElement, event?: MouseEvent): void {
const oldTarget = this.target;
this.target = el;
// 从不能拖动到能拖动的节点之间切换要重新创建moveable不然dragStart不生效 // 从不能拖动到能拖动的节点之间切换要重新创建moveable不然dragStart不生效
if (!this.moveable || this.target !== oldTarget) { if (!this.moveable || el !== this.target) {
this.initMoveable(el); this.initMoveable(el);
} else { } else {
this.updateMoveable(el); this.updateMoveable(el);
@ -97,8 +87,6 @@ export default class StageDragResize extends MoveableOptionsManager {
if (!this.moveable) return; if (!this.moveable) return;
if (!el) throw new Error('未选中任何节点'); if (!el) throw new Error('未选中任何节点');
this.target = el;
const options: MoveableOptions = this.init(el); const options: MoveableOptions = this.init(el);
Object.entries(options).forEach(([key, value]) => { Object.entries(options).forEach(([key, value]) => {
@ -109,7 +97,7 @@ export default class StageDragResize extends MoveableOptionsManager {
public clearSelectStatus(): void { public clearSelectStatus(): void {
if (!this.moveable) return; if (!this.moveable) return;
this.targetShadow.destroyEl(); this.dragResizeHelper.destroyShadowEl();
this.moveable.target = null; this.moveable.target = null;
this.moveable.updateTarget(); this.moveable.updateTarget();
} }
@ -119,8 +107,7 @@ export default class StageDragResize extends MoveableOptionsManager {
*/ */
public destroy(): void { public destroy(): void {
this.moveable?.destroy(); this.moveable?.destroy();
this.destroyGhostEl(); this.dragResizeHelper.destroy();
this.targetShadow.destroy();
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
this.removeAllListeners(); this.removeAllListeners();
} }
@ -131,28 +118,24 @@ export default class StageDragResize extends MoveableOptionsManager {
el.style.overflow = 'hidden'; el.style.overflow = 'hidden';
} }
this.target = el;
this.mode = getMode(el); this.mode = getMode(el);
this.destroyGhostEl(); this.dragResizeHelper.updateShadowEl(el);
this.dragResizeHelper.setMode(this.mode);
this.targetShadow.update(el);
// 设置选中元素的周围元素,用于选中元素跟周围元素对齐辅助 // 设置选中元素的周围元素,用于选中元素跟周围元素对齐辅助
const elementGuidelines: any = this.target?.parentElement?.children || []; const elementGuidelines: HTMLElement[] = Array.prototype.slice.call(this.target?.parentElement?.children) || [];
this.setElementGuidelines([this.target as HTMLElement], elementGuidelines); this.setElementGuidelines([this.target as HTMLElement], elementGuidelines);
return this.getOptions(false, { return this.getOptions(false, {
target: this.targetShadow.el, target: this.dragResizeHelper.getShadowEl(),
}); });
} }
private initMoveable(el: HTMLElement) { private initMoveable(el: HTMLElement) {
const options: MoveableOptions = this.init(el); const options: MoveableOptions = this.init(el);
this.moveableHelper = MoveableHelper.create({ this.dragResizeHelper.clear();
useBeforeRender: true,
useRender: false,
createAuto: true,
});
this.moveable?.destroy(); this.moveable?.destroy();
@ -169,41 +152,19 @@ export default class StageDragResize extends MoveableOptionsManager {
private bindResizeEvent(): void { private bindResizeEvent(): void {
if (!this.moveable) throw new Error('moveable 未初始化'); if (!this.moveable) throw new Error('moveable 未初始化');
const frame = {
left: 0,
top: 0,
};
this.moveable this.moveable
.on('resizeStart', (e) => { .on('resizeStart', (e) => {
if (!this.target) return; if (!this.target) return;
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
this.moveableHelper?.onResizeStart(e); this.dragResizeHelper.onResizeStart(e);
frame.top = this.target.offsetTop;
frame.left = this.target.offsetLeft;
}) })
.on('resize', (e) => { .on('resize', (e) => {
const { width, height, drag } = e; if (!this.moveable || !this.target || !this.dragResizeHelper.getShadowEl()) return;
if (!this.moveable || !this.target || !this.targetShadow.el) return;
const { beforeTranslate } = drag;
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
// 流式布局 this.dragResizeHelper.onResize(e);
if (this.mode === Mode.SORTABLE) {
this.target.style.top = '0px';
this.targetShadow.el.style.width = `${width}px`;
this.targetShadow.el.style.height = `${height}px`;
} else {
this.moveableHelper?.onResize(e);
this.target.style.left = `${frame.left + beforeTranslate[0]}px`;
this.target.style.top = `${frame.top + beforeTranslate[1]}px`;
}
this.target.style.width = `${width}px`;
this.target.style.height = `${height}px`;
}) })
.on('resizeEnd', () => { .on('resizeEnd', () => {
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
@ -214,11 +175,6 @@ export default class StageDragResize extends MoveableOptionsManager {
private bindDragEvent(): void { private bindDragEvent(): void {
if (!this.moveable) throw new Error('moveable 未初始化'); if (!this.moveable) throw new Error('moveable 未初始化');
const frame = {
left: 0,
top: 0,
};
let timeout: NodeJS.Timeout | undefined; let timeout: NodeJS.Timeout | undefined;
this.moveable this.moveable
@ -227,17 +183,10 @@ export default class StageDragResize extends MoveableOptionsManager {
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
this.moveableHelper?.onDragStart(e); this.dragResizeHelper.onDragStart(e);
if (this.mode === Mode.SORTABLE) {
this.ghostEl = this.generateGhostEl(this.target);
}
frame.top = this.target.offsetTop;
frame.left = this.target.offsetLeft;
}) })
.on('drag', (e) => { .on('drag', (e) => {
if (!this.target || !this.targetShadow.el) return; if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
if (timeout) { if (timeout) {
globalThis.clearTimeout(timeout); globalThis.clearTimeout(timeout);
@ -247,16 +196,7 @@ export default class StageDragResize extends MoveableOptionsManager {
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
// 流式布局 this.dragResizeHelper.onDrag(e);
if (this.ghostEl) {
this.ghostEl.style.top = `${frame.top + e.beforeTranslate[1]}px`;
return;
}
this.moveableHelper?.onDrag(e);
this.target.style.left = `${frame.left + e.beforeTranslate[0]}px`;
this.target.style.top = `${frame.top + e.beforeTranslate[1]}px`;
}) })
.on('dragEnd', () => { .on('dragEnd', () => {
if (timeout) { if (timeout) {
@ -281,7 +221,7 @@ export default class StageDragResize extends MoveableOptionsManager {
} }
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
this.destroyGhostEl(); this.dragResizeHelper.destroyGhostEl();
}); });
} }
@ -291,18 +231,16 @@ export default class StageDragResize extends MoveableOptionsManager {
this.moveable this.moveable
.on('rotateStart', (e) => { .on('rotateStart', (e) => {
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
this.moveableHelper?.onRotateStart(e); this.dragResizeHelper.onRotateStart(e);
}) })
.on('rotate', (e) => { .on('rotate', (e) => {
if (!this.target || !this.targetShadow.el) return; if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
this.moveableHelper?.onRotate(e); this.dragResizeHelper.onRotate(e);
const frame = this.moveableHelper?.getFrame(e.target);
this.target.style.transform = frame?.toCSSObject().transform || '';
}) })
.on('rotateEnd', (e) => { .on('rotateEnd', (e) => {
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
const frame = this.moveableHelper?.getFrame(e.target); const frame = this.dragResizeHelper?.getFrame(e.target);
this.emit('update', { this.emit('update', {
data: [ data: [
{ {
@ -322,18 +260,16 @@ export default class StageDragResize extends MoveableOptionsManager {
this.moveable this.moveable
.on('scaleStart', (e) => { .on('scaleStart', (e) => {
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
this.moveableHelper?.onScaleStart(e); this.dragResizeHelper.onScaleStart(e);
}) })
.on('scale', (e) => { .on('scale', (e) => {
if (!this.target || !this.targetShadow.el) return; if (!this.target || !this.dragResizeHelper.getShadowEl()) return;
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
this.moveableHelper?.onScale(e); this.dragResizeHelper.onScale(e);
const frame = this.moveableHelper?.getFrame(e.target);
this.target.style.transform = frame?.toCSSObject().transform || '';
}) })
.on('scaleEnd', (e) => { .on('scaleEnd', (e) => {
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
const frame = this.moveableHelper?.getFrame(e.target); const frame = this.dragResizeHelper.getFrame(e.target);
this.emit('update', { this.emit('update', {
data: [ data: [
{ {
@ -348,8 +284,8 @@ export default class StageDragResize extends MoveableOptionsManager {
} }
private sort(): void { private sort(): void {
if (!this.target || !this.ghostEl) throw new Error('未知错误'); if (!this.target || !this.dragResizeHelper.getGhostEl()) throw new Error('未知错误');
const { top } = this.ghostEl.getBoundingClientRect(); const { top } = this.dragResizeHelper.getGhostEl()!.getBoundingClientRect();
const { top: oriTop } = this.target.getBoundingClientRect(); const { top: oriTop } = this.target.getBoundingClientRect();
const deltaTop = top - oriTop; const deltaTop = top - oriTop;
if (Math.abs(deltaTop) >= this.target.clientHeight / 2) { if (Math.abs(deltaTop) >= this.target.clientHeight / 2) {
@ -381,12 +317,13 @@ export default class StageDragResize extends MoveableOptionsManager {
const width = calcValueByFontsize(doc, this.target.clientWidth); const width = calcValueByFontsize(doc, this.target.clientWidth);
const height = calcValueByFontsize(doc, this.target.clientHeight); const height = calcValueByFontsize(doc, this.target.clientHeight);
if (parentEl && this.mode === Mode.ABSOLUTE && this.targetShadow.el) { const shadowEl = this.dragResizeHelper.getShadowEl();
const targetShadowHtmlEl = this.targetShadow.el as HTMLElement; if (parentEl && this.mode === Mode.ABSOLUTE && shadowEl) {
const targetShadowHtmlEl = shadowEl as HTMLElement;
const targetShadowElOffsetLeft = targetShadowHtmlEl.offsetLeft || 0; const targetShadowElOffsetLeft = targetShadowHtmlEl.offsetLeft || 0;
const targetShadowElOffsetTop = targetShadowHtmlEl.offsetTop || 0; const targetShadowElOffsetTop = targetShadowHtmlEl.offsetTop || 0;
const frame = this.moveableHelper?.getFrame(this.targetShadow.el); const frame = this.dragResizeHelper.getFrame(shadowEl);
const [translateX, translateY] = frame?.properties.transform.translate.value; const [translateX, translateY] = frame?.properties.transform.translate.value;
const { left: parentLeft, top: parentTop } = getOffset(parentEl); const { left: parentLeft, top: parentTop } = getOffset(parentEl);
@ -411,39 +348,4 @@ export default class StageDragResize extends MoveableOptionsManager {
parentEl, parentEl,
}); });
} }
private generateGhostEl(el: HTMLElement): HTMLElement {
if (this.ghostEl) {
this.destroyGhostEl();
}
const ghostEl = el.cloneNode(true) as HTMLElement;
this.setGhostElChildrenId(ghostEl);
const { top, left } = getAbsolutePosition(el, getOffset(el));
ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
ghostEl.style.zIndex = ZIndex.GHOST_EL;
ghostEl.style.opacity = '.5';
ghostEl.style.position = 'absolute';
ghostEl.style.left = `${left}px`;
ghostEl.style.top = `${top}px`;
el.after(ghostEl);
return ghostEl;
}
private setGhostElChildrenId(el: Element) {
for (const child of Array.from(el.children)) {
if (child.id) {
child.id = `${GHOST_EL_ID_PREFIX}${child.id}`;
}
if (child.children.length) {
this.setGhostElChildrenId(child);
}
}
}
private destroyGhostEl(): void {
this.ghostEl?.remove();
this.ghostEl = undefined;
}
} }

View File

@ -16,13 +16,11 @@
* limitations under the License. * limitations under the License.
*/ */
import type { OnDragStart, OnResizeStart } from 'moveable';
import Moveable from 'moveable'; import Moveable from 'moveable';
import MoveableHelper from 'moveable-helper';
import { DRAG_EL_ID_PREFIX, Mode, ZIndex } from './const'; import { DRAG_EL_ID_PREFIX, Mode } from './const';
import DragResizeHelper from './DragResizeHelper';
import MoveableOptionsManager from './MoveableOptionsManager'; import MoveableOptionsManager from './MoveableOptionsManager';
import TargetShadow from './TargetShadow';
import { GetRenderDocument, MoveableOptionsManagerConfig, StageDragStatus, StageMultiDragResizeConfig } from './types'; import { GetRenderDocument, MoveableOptionsManagerConfig, StageDragStatus, StageMultiDragResizeConfig } from './types';
import { calcValueByFontsize, getMode } from './util'; import { calcValueByFontsize, getMode } from './util';
@ -31,13 +29,10 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
public container: HTMLElement; public container: HTMLElement;
/** 多选:目标节点组 */ /** 多选:目标节点组 */
public targetList: HTMLElement[] = []; public targetList: HTMLElement[] = [];
/** 多选:目标节点在蒙层中的占位节点组 */
public targetShadow: TargetShadow;
/** Moveable多选拖拽类实例 */ /** Moveable多选拖拽类实例 */
public moveableForMulti?: Moveable; public moveableForMulti?: Moveable;
/** 拖动状态 */
public dragStatus: StageDragStatus = StageDragStatus.END; public dragStatus: StageDragStatus = StageDragStatus.END;
private multiMoveableHelper?: MoveableHelper; private dragResizeHelper: DragResizeHelper;
private getRenderDocument: GetRenderDocument; private getRenderDocument: GetRenderDocument;
constructor(config: StageMultiDragResizeConfig) { constructor(config: StageMultiDragResizeConfig) {
@ -51,11 +46,9 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
this.container = config.container; this.container = config.container;
this.getRenderDocument = config.getRenderDocument; this.getRenderDocument = config.getRenderDocument;
this.targetShadow = new TargetShadow({ this.dragResizeHelper = new DragResizeHelper({
container: config.container, container: config.container,
updateDragEl: config.updateDragEl, updateDragEl: config.updateDragEl,
zIndex: ZIndex.DRAG_EL,
idPrefix: DRAG_EL_ID_PREFIX,
}); });
this.on('update-moveable', () => { this.on('update-moveable', () => {
@ -76,118 +69,49 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
this.mode = getMode(els[0]); this.mode = getMode(els[0]);
this.targetList = els; this.targetList = els;
this.targetShadow.updateGroup(els); this.dragResizeHelper.updateGroup(els);
// 设置周围元素,用于选中元素跟周围元素的对齐辅助 // 设置周围元素,用于选中元素跟周围元素的对齐辅助
const elementGuidelines: any = this.targetList[0].parentElement?.children || []; const elementGuidelines: HTMLElement[] =
Array.prototype.slice.call(this.targetList[0].parentElement?.children) || [];
this.setElementGuidelines(this.targetList, elementGuidelines); this.setElementGuidelines(this.targetList, elementGuidelines);
this.moveableForMulti?.destroy(); this.moveableForMulti?.destroy();
this.multiMoveableHelper?.clear(); this.dragResizeHelper.clear();
this.moveableForMulti = new Moveable( this.moveableForMulti = new Moveable(
this.container, this.container,
this.getOptions(true, { this.getOptions(true, {
target: this.targetShadow.els, target: this.dragResizeHelper.getShadowEls(),
}), }),
); );
this.multiMoveableHelper = MoveableHelper.create({
useBeforeRender: true,
useRender: false,
createAuto: true,
});
const frames: { left: number; top: number; id: string }[] = [];
const setFrames = (events: OnDragStart[] | OnResizeStart[]) => {
// 记录拖动前快照
events.forEach((ev) => {
// 实际目标元素
const matchEventTarget = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!matchEventTarget) return;
frames.push({
left: matchEventTarget.offsetLeft,
top: matchEventTarget.offsetTop,
id: matchEventTarget.id,
});
});
};
this.moveableForMulti this.moveableForMulti
.on('resizeGroupStart', (params) => { .on('resizeGroupStart', (e) => {
const { events } = params; this.dragResizeHelper.onResizeGroupStart(e);
this.multiMoveableHelper?.onResizeGroupStart(params);
setFrames(events);
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
}) })
.on('resizeGroup', (params) => { .on('resizeGroup', (e) => {
const { events } = params; this.dragResizeHelper.onResizeGroup(e);
// 拖动过程更新
events.forEach((ev) => {
const { width, height, beforeTranslate } = ev.drag;
const frameSnapShot = frames.find(
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!frameSnapShot) return;
const targeEl = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!targeEl) return;
// 元素与其所属组同时加入多选列表时,只更新父元素
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
if (!isParentIncluded) {
// 更新页面元素位置
targeEl.style.left = `${frameSnapShot.left + beforeTranslate[0]}px`;
targeEl.style.top = `${frameSnapShot.top + beforeTranslate[1]}px`;
}
// 更新页面元素位置
targeEl.style.width = `${width}px`;
targeEl.style.height = `${height}px`;
});
this.multiMoveableHelper?.onResizeGroup(params);
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
}) })
.on('resizeGroupEnd', () => { .on('resizeGroupEnd', () => {
this.update(true); this.update(true);
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
}) })
.on('dragGroupStart', (params) => { .on('dragGroupStart', (e) => {
const { events } = params; this.dragResizeHelper.onDragGroupStart(e);
this.multiMoveableHelper?.onDragGroupStart(params);
// 记录拖动前快照
setFrames(events);
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
}) })
.on('dragGroup', (params) => { .on('dragGroup', (e) => {
const { events } = params; this.dragResizeHelper.onDragGroup(e);
// 拖动过程更新
events.forEach((ev) => {
const frameSnapShot = frames.find(
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!frameSnapShot) return;
const targeEl = this.targetList.find(
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
);
if (!targeEl) return;
// 元素与其所属组同时加入多选列表时,只更新父元素
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
if (!isParentIncluded) {
// 更新页面元素位置
targeEl.style.left = `${frameSnapShot.left + ev.beforeTranslate[0]}px`;
targeEl.style.top = `${frameSnapShot.top + ev.beforeTranslate[1]}px`;
}
});
this.multiMoveableHelper?.onDragGroup(params);
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
}) })
.on('dragGroupEnd', () => { .on('dragGroupEnd', () => {
this.update(); this.update();
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
}) })
.on('clickGroup', (params) => { .on('clickGroup', (e) => {
const { inputTarget, targets } = params; const { inputTarget, targets } = e;
// 如果有多个元素被选中,同时点击的元素在选中元素中的其中一项,可能是多选态切换为该元素的单选态,抛事件给上一层继续判断是否切换 // 如果有多个元素被选中,同时点击的元素在选中元素中的其中一项,可能是多选态切换为该元素的单选态,抛事件给上一层继续判断是否切换
if (targets.length > 1 && targets.includes(inputTarget)) { if (targets.length > 1 && targets.includes(inputTarget)) {
this.emit('change-to-select', inputTarget.id.replace(DRAG_EL_ID_PREFIX, '')); this.emit('change-to-select', inputTarget.id.replace(DRAG_EL_ID_PREFIX, ''));
@ -223,9 +147,10 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
if (!eleList) throw new Error('未选中任何节点'); if (!eleList) throw new Error('未选中任何节点');
this.targetList = eleList; this.targetList = eleList;
this.dragResizeHelper.setTargetList(eleList);
const options = this.getOptions(true, { const options = this.getOptions(true, {
target: this.targetShadow.els, target: this.dragResizeHelper.getShadowEls(),
}); });
Object.entries(options).forEach(([key, value]) => { Object.entries(options).forEach(([key, value]) => {
@ -239,7 +164,7 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
*/ */
public clearSelectStatus(): void { public clearSelectStatus(): void {
if (!this.moveableForMulti) return; if (!this.moveableForMulti) return;
this.targetShadow.destroyEls(); this.dragResizeHelper.clearMultiSelectStatus();
this.moveableForMulti.target = null; this.moveableForMulti.target = null;
this.moveableForMulti.updateTarget(); this.moveableForMulti.updateTarget();
this.targetList = []; this.targetList = [];
@ -250,7 +175,7 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
*/ */
public destroy(): void { public destroy(): void {
this.moveableForMulti?.destroy(); this.moveableForMulti?.destroy();
this.targetShadow.destroy(); this.dragResizeHelper.destroy();
} }
/** /**

View File

@ -16,15 +16,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { Mode, ZIndex } from './const'; import { Mode, ZIndex } from './const';
import type { TargetElement, TargetShadowConfig, UpdateDragEl } from './types'; import type { TargetElement as ShadowElement, TargetShadowConfig, UpdateDragEl } from './types';
import { getTargetElStyle, isFixedParent } from './util'; import { getTargetElStyle, isFixedParent } from './util';
/** /**
* *
*/ */
export default class TargetShadow { export default class TargetShadow {
public el?: TargetElement; public el?: ShadowElement;
public els: TargetElement[] = []; public els: ShadowElement[] = [];
private idPrefix = 'target_calibrate_'; private idPrefix = 'target_calibrate_';
private container: HTMLElement; private container: HTMLElement;
@ -52,13 +52,13 @@ export default class TargetShadow {
this.container.addEventListener('customScroll', this.scrollHandler); this.container.addEventListener('customScroll', this.scrollHandler);
} }
public update(target: TargetElement): TargetElement { public update(target: ShadowElement): ShadowElement {
this.el = this.updateEl(target, this.el); this.el = this.updateEl(target, this.el);
return this.el; return this.el;
} }
public updateGroup(targetGroup: TargetElement[]): TargetElement[] { public updateGroup(targetGroup: ShadowElement[]): ShadowElement[] {
if (this.els.length > targetGroup.length) { if (this.els.length > targetGroup.length) {
this.els.slice(targetGroup.length - 1).forEach((el) => { this.els.slice(targetGroup.length - 1).forEach((el) => {
el.remove(); el.remove();
@ -88,7 +88,7 @@ export default class TargetShadow {
this.destroyEls(); this.destroyEls();
} }
private updateEl(target: TargetElement, src?: TargetElement): TargetElement { private updateEl(target: ShadowElement, src?: ShadowElement): ShadowElement {
const el = src || globalThis.document.createElement('div'); const el = src || globalThis.document.createElement('div');
el.id = `${this.idPrefix}${target.id}`; el.id = `${this.idPrefix}${target.id}`;

View File

@ -126,6 +126,11 @@ export interface StageMultiDragResizeConfig {
updateDragEl?: UpdateDragEl; updateDragEl?: UpdateDragEl;
} }
export interface DragResizeHelperConfig {
container: HTMLElement;
updateDragEl?: UpdateDragEl;
}
/** 选择状态 */ /** 选择状态 */
export enum SelectStatus { export enum SelectStatus {
/** 单选 */ /** 单选 */