From 164d777ebf2ed4307e8c45f038e494b470ef5f4a Mon Sep 17 00:00:00 2001 From: oceanzhu Date: Tue, 29 Nov 2022 11:36:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor(stage):=20=E5=B0=86=E8=A2=AB=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=85=83=E7=B4=A0=E5=92=8C=E6=8B=96=E6=8B=BD=E6=A1=86?= =?UTF-8?q?=E6=8A=BD=E5=8F=96=E5=87=BA=E6=9D=A5=E6=94=BE=E5=88=B0DragResiz?= =?UTF-8?q?eHelper=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge branch 'feature/ocean_stagerefactor_880128993' into 'master' refactor(stage): 在拖拽过程中,moveable会产生一些状态事件,DragResizeHelper对这些状态事件中对将被操作元素和拖拽框的状态和dom进行处理。 --- packages/stage/README.md | 7 +- packages/stage/src/DragResizeHelper.ts | 338 +++++++++++++++++++++ packages/stage/src/StageDragResize.ts | 168 +++------- packages/stage/src/StageMultiDragResize.ts | 121 ++------ packages/stage/src/TargetShadow.ts | 12 +- packages/stage/src/types.ts | 5 + 6 files changed, 413 insertions(+), 238 deletions(-) create mode 100644 packages/stage/src/DragResizeHelper.ts diff --git a/packages/stage/README.md b/packages/stage/README.md index 4b6d8153..446e3485 100644 --- a/packages/stage/README.md +++ b/packages/stage/README.md @@ -54,4 +54,9 @@ mask是一个盖在画布区域的一个蒙层,主要作用是隔离鼠标事 StageDragResize、StageMultiDragResize的父类,负责管理Moveable的配置

## TargetShadow -统一管理拖拽和高亮框,包括创建、更新、销毁。 +统一管理拖拽框和高亮框,包括创建、更新、销毁。 +

+## DragResizeHelper +- 拖拽/改变大小等操作发生时,moveable会抛出各种状态事件,DragResizeHelper负责响应这些事件,对目标节点target和拖拽节点targetShadow进行修改; +- 其中目标节点是DragResizeHelper直接改的,targetShadow作为直接被操作的拖拽框,是调用moveableHelper改的; +- 有个特殊情况是流式布局下,moveableHelper不支持,targetShadow也是DragResizeHelper直接改的 diff --git a/packages/stage/src/DragResizeHelper.ts b/packages/stage/src/DragResizeHelper.ts new file mode 100644 index 00000000..a77f50ca --- /dev/null +++ b/packages/stage/src/DragResizeHelper.ts @@ -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[] = []; + /** 响应拖拽的状态事件,修改绝对定位布局下targetShadow的dom。 + * 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 包含了拖拽节点的dom,moveableHelper会直接修改拖拽节点 + */ + 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 { + 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); + } + } + } +} diff --git a/packages/stage/src/StageDragResize.ts b/packages/stage/src/StageDragResize.ts index fb36fea3..6800b04c 100644 --- a/packages/stage/src/StageDragResize.ts +++ b/packages/stage/src/StageDragResize.ts @@ -18,14 +18,13 @@ /* eslint-disable no-param-reassign */ 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 TargetShadow from './TargetShadow'; import type { DelayedMarkContainer, GetRenderDocument, MarkContainerEnd, StageDragResizeConfig } from './types'; import { StageDragStatus } from './types'; -import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, up } from './util'; +import { calcValueByFontsize, down, getMode, getOffset, up } from './util'; /** * 管理单选操作,响应选中操作,初始化moveableOption参数并初始化moveable,处理moveable回调事件对组件进行更新 @@ -34,15 +33,11 @@ import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, up export default class StageDragResize extends MoveableOptionsManager { /** 目标节点 */ private target?: HTMLElement; - /** 目标节点在蒙层中的占位节点 */ - private targetShadow: TargetShadow; /** Moveable拖拽类实例 */ private moveable?: Moveable; /** 拖动状态 */ private dragStatus: StageDragStatus = StageDragStatus.END; - /** 流式布局下,目标节点的镜像节点 */ - private ghostEl: HTMLElement | undefined; - private moveableHelper?: MoveableHelper; + private dragResizeHelper: DragResizeHelper; private getRenderDocument: GetRenderDocument; private markContainerEnd: MarkContainerEnd; private delayedMarkContainer: DelayedMarkContainer; @@ -54,11 +49,9 @@ export default class StageDragResize extends MoveableOptionsManager { this.markContainerEnd = config.markContainerEnd; this.delayedMarkContainer = config.delayedMarkContainer; - this.targetShadow = new TargetShadow({ + this.dragResizeHelper = new DragResizeHelper({ container: config.container, updateDragEl: config.updateDragEl, - zIndex: ZIndex.DRAG_EL, - idPrefix: DRAG_EL_ID_PREFIX, }); this.on('update-moveable', () => { @@ -75,11 +68,8 @@ export default class StageDragResize extends MoveableOptionsManager { * @param event 鼠标事件 */ public select(el: HTMLElement, event?: MouseEvent): void { - const oldTarget = this.target; - this.target = el; - // 从不能拖动到能拖动的节点之间切换,要重新创建moveable,不然dragStart不生效 - if (!this.moveable || this.target !== oldTarget) { + if (!this.moveable || el !== this.target) { this.initMoveable(el); } else { this.updateMoveable(el); @@ -97,8 +87,6 @@ export default class StageDragResize extends MoveableOptionsManager { if (!this.moveable) return; if (!el) throw new Error('未选中任何节点'); - this.target = el; - const options: MoveableOptions = this.init(el); Object.entries(options).forEach(([key, value]) => { @@ -109,7 +97,7 @@ export default class StageDragResize extends MoveableOptionsManager { public clearSelectStatus(): void { if (!this.moveable) return; - this.targetShadow.destroyEl(); + this.dragResizeHelper.destroyShadowEl(); this.moveable.target = null; this.moveable.updateTarget(); } @@ -119,8 +107,7 @@ export default class StageDragResize extends MoveableOptionsManager { */ public destroy(): void { this.moveable?.destroy(); - this.destroyGhostEl(); - this.targetShadow.destroy(); + this.dragResizeHelper.destroy(); this.dragStatus = StageDragStatus.END; this.removeAllListeners(); } @@ -131,28 +118,24 @@ export default class StageDragResize extends MoveableOptionsManager { el.style.overflow = 'hidden'; } + this.target = el; this.mode = getMode(el); - this.destroyGhostEl(); - - this.targetShadow.update(el); + this.dragResizeHelper.updateShadowEl(el); + this.dragResizeHelper.setMode(this.mode); // 设置选中元素的周围元素,用于选中元素跟周围元素对齐辅助 - 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); return this.getOptions(false, { - target: this.targetShadow.el, + target: this.dragResizeHelper.getShadowEl(), }); } private initMoveable(el: HTMLElement) { const options: MoveableOptions = this.init(el); - this.moveableHelper = MoveableHelper.create({ - useBeforeRender: true, - useRender: false, - createAuto: true, - }); + this.dragResizeHelper.clear(); this.moveable?.destroy(); @@ -169,41 +152,19 @@ export default class StageDragResize extends MoveableOptionsManager { private bindResizeEvent(): void { if (!this.moveable) throw new Error('moveable 未初始化'); - const frame = { - left: 0, - top: 0, - }; - this.moveable .on('resizeStart', (e) => { if (!this.target) return; this.dragStatus = StageDragStatus.START; - this.moveableHelper?.onResizeStart(e); - - frame.top = this.target.offsetTop; - frame.left = this.target.offsetLeft; + this.dragResizeHelper.onResizeStart(e); }) .on('resize', (e) => { - const { width, height, drag } = e; - if (!this.moveable || !this.target || !this.targetShadow.el) return; + if (!this.moveable || !this.target || !this.dragResizeHelper.getShadowEl()) return; - const { beforeTranslate } = drag; this.dragStatus = StageDragStatus.ING; - // 流式布局 - 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`; + this.dragResizeHelper.onResize(e); }) .on('resizeEnd', () => { this.dragStatus = StageDragStatus.END; @@ -214,11 +175,6 @@ export default class StageDragResize extends MoveableOptionsManager { private bindDragEvent(): void { if (!this.moveable) throw new Error('moveable 未初始化'); - const frame = { - left: 0, - top: 0, - }; - let timeout: NodeJS.Timeout | undefined; this.moveable @@ -227,17 +183,10 @@ export default class StageDragResize extends MoveableOptionsManager { this.dragStatus = StageDragStatus.START; - this.moveableHelper?.onDragStart(e); - - if (this.mode === Mode.SORTABLE) { - this.ghostEl = this.generateGhostEl(this.target); - } - - frame.top = this.target.offsetTop; - frame.left = this.target.offsetLeft; + this.dragResizeHelper.onDragStart(e); }) .on('drag', (e) => { - if (!this.target || !this.targetShadow.el) return; + if (!this.target || !this.dragResizeHelper.getShadowEl()) return; if (timeout) { globalThis.clearTimeout(timeout); @@ -247,16 +196,7 @@ export default class StageDragResize extends MoveableOptionsManager { this.dragStatus = StageDragStatus.ING; - // 流式布局 - 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`; + this.dragResizeHelper.onDrag(e); }) .on('dragEnd', () => { if (timeout) { @@ -281,7 +221,7 @@ export default class StageDragResize extends MoveableOptionsManager { } this.dragStatus = StageDragStatus.END; - this.destroyGhostEl(); + this.dragResizeHelper.destroyGhostEl(); }); } @@ -291,18 +231,16 @@ export default class StageDragResize extends MoveableOptionsManager { this.moveable .on('rotateStart', (e) => { this.dragStatus = StageDragStatus.START; - this.moveableHelper?.onRotateStart(e); + this.dragResizeHelper.onRotateStart(e); }) .on('rotate', (e) => { - if (!this.target || !this.targetShadow.el) return; + if (!this.target || !this.dragResizeHelper.getShadowEl()) return; this.dragStatus = StageDragStatus.ING; - this.moveableHelper?.onRotate(e); - const frame = this.moveableHelper?.getFrame(e.target); - this.target.style.transform = frame?.toCSSObject().transform || ''; + this.dragResizeHelper.onRotate(e); }) .on('rotateEnd', (e) => { this.dragStatus = StageDragStatus.END; - const frame = this.moveableHelper?.getFrame(e.target); + const frame = this.dragResizeHelper?.getFrame(e.target); this.emit('update', { data: [ { @@ -322,18 +260,16 @@ export default class StageDragResize extends MoveableOptionsManager { this.moveable .on('scaleStart', (e) => { this.dragStatus = StageDragStatus.START; - this.moveableHelper?.onScaleStart(e); + this.dragResizeHelper.onScaleStart(e); }) .on('scale', (e) => { - if (!this.target || !this.targetShadow.el) return; + if (!this.target || !this.dragResizeHelper.getShadowEl()) return; this.dragStatus = StageDragStatus.ING; - this.moveableHelper?.onScale(e); - const frame = this.moveableHelper?.getFrame(e.target); - this.target.style.transform = frame?.toCSSObject().transform || ''; + this.dragResizeHelper.onScale(e); }) .on('scaleEnd', (e) => { this.dragStatus = StageDragStatus.END; - const frame = this.moveableHelper?.getFrame(e.target); + const frame = this.dragResizeHelper.getFrame(e.target); this.emit('update', { data: [ { @@ -348,8 +284,8 @@ export default class StageDragResize extends MoveableOptionsManager { } private sort(): void { - if (!this.target || !this.ghostEl) throw new Error('未知错误'); - const { top } = this.ghostEl.getBoundingClientRect(); + if (!this.target || !this.dragResizeHelper.getGhostEl()) throw new Error('未知错误'); + const { top } = this.dragResizeHelper.getGhostEl()!.getBoundingClientRect(); const { top: oriTop } = this.target.getBoundingClientRect(); const deltaTop = top - oriTop; 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 height = calcValueByFontsize(doc, this.target.clientHeight); - if (parentEl && this.mode === Mode.ABSOLUTE && this.targetShadow.el) { - const targetShadowHtmlEl = this.targetShadow.el as HTMLElement; + const shadowEl = this.dragResizeHelper.getShadowEl(); + if (parentEl && this.mode === Mode.ABSOLUTE && shadowEl) { + const targetShadowHtmlEl = shadowEl as HTMLElement; const targetShadowElOffsetLeft = targetShadowHtmlEl.offsetLeft || 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 { left: parentLeft, top: parentTop } = getOffset(parentEl); @@ -411,39 +348,4 @@ export default class StageDragResize extends MoveableOptionsManager { 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; - } } diff --git a/packages/stage/src/StageMultiDragResize.ts b/packages/stage/src/StageMultiDragResize.ts index 4c6038aa..766b20c1 100644 --- a/packages/stage/src/StageMultiDragResize.ts +++ b/packages/stage/src/StageMultiDragResize.ts @@ -16,13 +16,11 @@ * limitations under the License. */ -import type { OnDragStart, OnResizeStart } 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 TargetShadow from './TargetShadow'; import { GetRenderDocument, MoveableOptionsManagerConfig, StageDragStatus, StageMultiDragResizeConfig } from './types'; import { calcValueByFontsize, getMode } from './util'; @@ -31,13 +29,10 @@ export default class StageMultiDragResize extends MoveableOptionsManager { public container: HTMLElement; /** 多选:目标节点组 */ public targetList: HTMLElement[] = []; - /** 多选:目标节点在蒙层中的占位节点组 */ - public targetShadow: TargetShadow; /** Moveable多选拖拽类实例 */ public moveableForMulti?: Moveable; - /** 拖动状态 */ public dragStatus: StageDragStatus = StageDragStatus.END; - private multiMoveableHelper?: MoveableHelper; + private dragResizeHelper: DragResizeHelper; private getRenderDocument: GetRenderDocument; constructor(config: StageMultiDragResizeConfig) { @@ -51,11 +46,9 @@ export default class StageMultiDragResize extends MoveableOptionsManager { this.container = config.container; this.getRenderDocument = config.getRenderDocument; - this.targetShadow = new TargetShadow({ + this.dragResizeHelper = new DragResizeHelper({ container: config.container, updateDragEl: config.updateDragEl, - zIndex: ZIndex.DRAG_EL, - idPrefix: DRAG_EL_ID_PREFIX, }); this.on('update-moveable', () => { @@ -76,118 +69,49 @@ export default class StageMultiDragResize extends MoveableOptionsManager { this.mode = getMode(els[0]); 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.moveableForMulti?.destroy(); - this.multiMoveableHelper?.clear(); + this.dragResizeHelper.clear(); this.moveableForMulti = new Moveable( this.container, 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 - .on('resizeGroupStart', (params) => { - const { events } = params; - this.multiMoveableHelper?.onResizeGroupStart(params); - setFrames(events); + .on('resizeGroupStart', (e) => { + this.dragResizeHelper.onResizeGroupStart(e); this.dragStatus = StageDragStatus.START; }) - .on('resizeGroup', (params) => { - const { events } = params; - // 拖动过程更新 - 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); + .on('resizeGroup', (e) => { + this.dragResizeHelper.onResizeGroup(e); this.dragStatus = StageDragStatus.ING; }) .on('resizeGroupEnd', () => { this.update(true); this.dragStatus = StageDragStatus.END; }) - .on('dragGroupStart', (params) => { - const { events } = params; - this.multiMoveableHelper?.onDragGroupStart(params); - // 记录拖动前快照 - setFrames(events); + .on('dragGroupStart', (e) => { + this.dragResizeHelper.onDragGroupStart(e); this.dragStatus = StageDragStatus.START; }) - .on('dragGroup', (params) => { - const { events } = params; - // 拖动过程更新 - 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); + .on('dragGroup', (e) => { + this.dragResizeHelper.onDragGroup(e); this.dragStatus = StageDragStatus.ING; }) .on('dragGroupEnd', () => { this.update(); this.dragStatus = StageDragStatus.END; }) - .on('clickGroup', (params) => { - const { inputTarget, targets } = params; + .on('clickGroup', (e) => { + const { inputTarget, targets } = e; // 如果有多个元素被选中,同时点击的元素在选中元素中的其中一项,可能是多选态切换为该元素的单选态,抛事件给上一层继续判断是否切换 if (targets.length > 1 && targets.includes(inputTarget)) { 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('未选中任何节点'); this.targetList = eleList; + this.dragResizeHelper.setTargetList(eleList); const options = this.getOptions(true, { - target: this.targetShadow.els, + target: this.dragResizeHelper.getShadowEls(), }); Object.entries(options).forEach(([key, value]) => { @@ -239,7 +164,7 @@ export default class StageMultiDragResize extends MoveableOptionsManager { */ public clearSelectStatus(): void { if (!this.moveableForMulti) return; - this.targetShadow.destroyEls(); + this.dragResizeHelper.clearMultiSelectStatus(); this.moveableForMulti.target = null; this.moveableForMulti.updateTarget(); this.targetList = []; @@ -250,7 +175,7 @@ export default class StageMultiDragResize extends MoveableOptionsManager { */ public destroy(): void { this.moveableForMulti?.destroy(); - this.targetShadow.destroy(); + this.dragResizeHelper.destroy(); } /** diff --git a/packages/stage/src/TargetShadow.ts b/packages/stage/src/TargetShadow.ts index 168b52bb..3e16c1a0 100644 --- a/packages/stage/src/TargetShadow.ts +++ b/packages/stage/src/TargetShadow.ts @@ -16,15 +16,15 @@ * limitations under the License. */ 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'; /** * 将选中的节点修正定位后,添加一个操作节点到蒙层上 */ export default class TargetShadow { - public el?: TargetElement; - public els: TargetElement[] = []; + public el?: ShadowElement; + public els: ShadowElement[] = []; private idPrefix = 'target_calibrate_'; private container: HTMLElement; @@ -52,13 +52,13 @@ export default class TargetShadow { this.container.addEventListener('customScroll', this.scrollHandler); } - public update(target: TargetElement): TargetElement { + public update(target: ShadowElement): ShadowElement { this.el = this.updateEl(target, this.el); return this.el; } - public updateGroup(targetGroup: TargetElement[]): TargetElement[] { + public updateGroup(targetGroup: ShadowElement[]): ShadowElement[] { if (this.els.length > targetGroup.length) { this.els.slice(targetGroup.length - 1).forEach((el) => { el.remove(); @@ -88,7 +88,7 @@ export default class TargetShadow { this.destroyEls(); } - private updateEl(target: TargetElement, src?: TargetElement): TargetElement { + private updateEl(target: ShadowElement, src?: ShadowElement): ShadowElement { const el = src || globalThis.document.createElement('div'); el.id = `${this.idPrefix}${target.id}`; diff --git a/packages/stage/src/types.ts b/packages/stage/src/types.ts index 4423afbe..b1c9d7d1 100644 --- a/packages/stage/src/types.ts +++ b/packages/stage/src/types.ts @@ -126,6 +126,11 @@ export interface StageMultiDragResizeConfig { updateDragEl?: UpdateDragEl; } +export interface DragResizeHelperConfig { + container: HTMLElement; + updateDragEl?: UpdateDragEl; +} + /** 选择状态 */ export enum SelectStatus { /** 单选 */