feat(stage): 支持多选组件并将多个组件拖入指定容器中

fix #405
This commit is contained in:
roymondchen 2023-03-06 15:19:43 +08:00
parent e3af0b2914
commit 449efcc56b
6 changed files with 107 additions and 72 deletions

View File

@ -25,6 +25,7 @@ import { Id } from '@tmagic/schema';
import { addClassName, getDocument, removeClassNameByClassName } from '@tmagic/utils'; import { addClassName, getDocument, removeClassNameByClassName } from '@tmagic/utils';
import { CONTAINER_HIGHLIGHT_CLASS_NAME, GHOST_EL_ID_PREFIX, GuidesType, MouseButton, PAGE_CLASS } from './const'; import { CONTAINER_HIGHLIGHT_CLASS_NAME, GHOST_EL_ID_PREFIX, GuidesType, MouseButton, PAGE_CLASS } from './const';
import DragResizeHelper from './DragResizeHelper';
import StageDragResize from './StageDragResize'; import StageDragResize from './StageDragResize';
import StageHighlight from './StageHighlight'; import StageHighlight from './StageHighlight';
import StageMultiDragResize from './StageMultiDragResize'; import StageMultiDragResize from './StageMultiDragResize';
@ -101,27 +102,30 @@ export default class ActionManager extends EventEmitter {
this.getRenderDocument = config.getRenderDocument; this.getRenderDocument = config.getRenderDocument;
this.isContainer = config.isContainer; this.isContainer = config.isContainer;
const createDrHelper = () =>
new DragResizeHelper({
container: config.container,
updateDragEl: config.updateDragEl,
});
this.dr = new StageDragResize({ this.dr = new StageDragResize({
container: config.container, container: config.container,
disabledDragStart: config.disabledDragStart, disabledDragStart: config.disabledDragStart,
moveableOptions: this.changeCallback(config.moveableOptions),
dragResizeHelper: createDrHelper(),
getRootContainer: config.getRootContainer, getRootContainer: config.getRootContainer,
getRenderDocument: config.getRenderDocument, getRenderDocument: config.getRenderDocument,
updateDragEl: config.updateDragEl, markContainerEnd: this.markContainerEnd.bind(this),
markContainerEnd: () => this.markContainerEnd(), delayedMarkContainer: this.delayedMarkContainer.bind(this),
delayedMarkContainer: (event: MouseEvent, exclude: Element[]) => {
if (this.canAddToContainer()) {
return this.delayedMarkContainer(event, exclude);
}
return undefined;
},
moveableOptions: this.changeCallback(config.moveableOptions),
}); });
this.multiDr = new StageMultiDragResize({ this.multiDr = new StageMultiDragResize({
container: config.container, container: config.container,
multiMoveableOptions: config.multiMoveableOptions, multiMoveableOptions: config.multiMoveableOptions,
dragResizeHelper: createDrHelper(),
getRootContainer: config.getRootContainer, getRootContainer: config.getRootContainer,
getRenderDocument: config.getRenderDocument, getRenderDocument: config.getRenderDocument,
updateDragEl: config.updateDragEl, markContainerEnd: this.markContainerEnd.bind(this),
delayedMarkContainer: this.delayedMarkContainer.bind(this),
}); });
this.highlightLayer = new StageHighlight({ this.highlightLayer = new StageHighlight({
container: config.container, container: config.container,
@ -320,11 +324,14 @@ export default class ActionManager extends EventEmitter {
* @param excludeElList * @param excludeElList
* @returns timeoutIdtimeout * @returns timeoutIdtimeout
*/ */
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout { public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
if (this.canAddToContainer()) {
return globalThis.setTimeout(() => { return globalThis.setTimeout(() => {
this.addContainerHighlightClassName(event, excludeElList); this.addContainerHighlightClassName(event, excludeElList);
}, this.containerHighlightDuration); }, this.containerHighlightDuration);
} }
return undefined;
}
public destroy(): void { public destroy(): void {
this.container.removeEventListener('mousedown', this.mouseDownHandler); this.container.removeEventListener('mousedown', this.mouseDownHandler);
@ -466,8 +473,8 @@ export default class ActionManager extends EventEmitter {
}); });
this.multiDr this.multiDr
.on('update', (data: UpdateEventData, parentEl: HTMLElement | null) => { .on('update', (data: UpdateEventData) => {
this.emit('multi-update', data, parentEl); this.emit('multi-update', data);
}) })
.on('change-to-select', async (id: Id) => { .on('change-to-select', async (id: Id) => {
// 如果还在多选状态,不触发切换到单选 // 如果还在多选状态,不触发切换到单选

View File

@ -34,8 +34,8 @@ import MoveableHelper from 'moveable-helper';
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const'; import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const';
import TargetShadow from './TargetShadow'; import TargetShadow from './TargetShadow';
import { DragResizeHelperConfig, TargetElement } from './types'; import { DragResizeHelperConfig, Rect, TargetElement } from './types';
import { getAbsolutePosition, getOffset } from './util'; import { calcValueByFontsize, getAbsolutePosition, getOffset } from './util';
/** /**
* /moveable会抛出各种状态事件DragResizeHelper负责响应这些事件target和拖拽节点targetShadow进行修改 * /moveable会抛出各种状态事件DragResizeHelper负责响应这些事件target和拖拽节点targetShadow进行修改
@ -282,6 +282,44 @@ export default class DragResizeHelper {
this.moveableHelper.onDragGroup(e); this.moveableHelper.onDragGroup(e);
} }
public getUpdatedElRect(el: HTMLElement, parentEl: HTMLElement | null, doc: Document): Rect {
const offset = this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : { left: el.offsetLeft, top: el.offsetTop };
let left = calcValueByFontsize(doc, offset.left);
let top = calcValueByFontsize(doc, offset.top);
const width = calcValueByFontsize(doc, el.clientWidth);
const height = calcValueByFontsize(doc, el.clientHeight);
let shadowEl = this.getShadowEl();
const shadowEls = this.getShadowEls();
if (shadowEls.length) {
shadowEl = shadowEls.find((item) => item.id.endsWith(el.id));
}
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.getFrame(shadowEl);
const [translateX, translateY] = frame?.properties.transform.translate.value;
const { left: parentLeft, top: parentTop } = getOffset(parentEl);
left =
calcValueByFontsize(doc, targetShadowElOffsetLeft) +
parseFloat(translateX) -
calcValueByFontsize(doc, parentLeft);
top =
calcValueByFontsize(doc, targetShadowElOffsetTop) +
parseFloat(translateY) -
calcValueByFontsize(doc, parentTop);
}
return { width, height, left, top };
}
/** /**
* *
*/ */

View File

@ -189,7 +189,10 @@ export default class StageCore extends EventEmitter {
/** /**
* @deprecated delayedMarkContainer代替 * @deprecated delayedMarkContainer代替
*/ */
public getAddContainerHighlightClassNameTimeout(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout { public getAddContainerHighlightClassNameTimeout(
event: MouseEvent,
excludeElList: Element[] = [],
): NodeJS.Timeout | undefined {
return this.delayedMarkContainer(event, excludeElList); return this.delayedMarkContainer(event, excludeElList);
} }
@ -200,7 +203,7 @@ export default class StageCore extends EventEmitter {
* @param excludeElList * @param excludeElList
* @returns timeoutIdtimeout * @returns timeoutIdtimeout
*/ */
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout { public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
return this.actionManager.delayedMarkContainer(event, excludeElList); return this.actionManager.delayedMarkContainer(event, excludeElList);
} }
@ -331,8 +334,8 @@ export default class StageCore extends EventEmitter {
// 先保证画布内完成渲染,再通知外部更新 // 先保证画布内完成渲染,再通知外部更新
setTimeout(() => this.emit('select', el)); setTimeout(() => this.emit('select', el));
}) })
.on('multi-update', (data: UpdateEventData, parentEl: HTMLElement | null) => { .on('multi-update', (data: UpdateEventData) => {
this.emit('update', { data, parentEl }); this.emit('update', data);
}); });
} }

View File

@ -24,7 +24,7 @@ import DragResizeHelper from './DragResizeHelper';
import MoveableOptionsManager from './MoveableOptionsManager'; import MoveableOptionsManager from './MoveableOptionsManager';
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, getMode, getOffset, up } from './util'; import { down, getMode, up } from './util';
/** /**
* moveableOption参数并初始化moveablemoveable回调事件对组件进行更新 * moveableOption参数并初始化moveablemoveable回调事件对组件进行更新
@ -51,10 +51,7 @@ export default class StageDragResize extends MoveableOptionsManager {
this.delayedMarkContainer = config.delayedMarkContainer; this.delayedMarkContainer = config.delayedMarkContainer;
this.disabledDragStart = config.disabledDragStart; this.disabledDragStart = config.disabledDragStart;
this.dragResizeHelper = new DragResizeHelper({ this.dragResizeHelper = config.dragResizeHelper;
container: config.container,
updateDragEl: config.updateDragEl,
});
this.on('update-moveable', () => { this.on('update-moveable', () => {
if (this.moveable) { if (this.moveable) {
@ -315,40 +312,13 @@ export default class StageDragResize extends MoveableOptionsManager {
if (!doc) return; if (!doc) return;
const offset = const rect = this.dragResizeHelper.getUpdatedElRect(this.target, parentEl, doc);
this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : { left: this.target.offsetLeft, top: this.target.offsetTop };
let left = calcValueByFontsize(doc, offset.left);
let top = calcValueByFontsize(doc, offset.top);
const width = calcValueByFontsize(doc, this.target.clientWidth);
const height = calcValueByFontsize(doc, this.target.clientHeight);
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.dragResizeHelper.getFrame(shadowEl);
const [translateX, translateY] = frame?.properties.transform.translate.value;
const { left: parentLeft, top: parentTop } = getOffset(parentEl);
left =
calcValueByFontsize(doc, targetShadowElOffsetLeft) +
parseFloat(translateX) -
calcValueByFontsize(doc, parentLeft);
top =
calcValueByFontsize(doc, targetShadowElOffsetTop) +
parseFloat(translateY) -
calcValueByFontsize(doc, parentTop);
}
this.emit('update', { this.emit('update', {
data: [ data: [
{ {
el: this.target, el: this.target,
style: isResize ? { left, top, width, height } : { left, top }, style: isResize ? rect : { left: rect.left, top: rect.top },
}, },
], ],
parentEl, parentEl,

View File

@ -21,8 +21,15 @@ import Moveable from 'moveable';
import { DRAG_EL_ID_PREFIX, Mode } from './const'; import { DRAG_EL_ID_PREFIX, Mode } from './const';
import DragResizeHelper from './DragResizeHelper'; import DragResizeHelper from './DragResizeHelper';
import MoveableOptionsManager from './MoveableOptionsManager'; import MoveableOptionsManager from './MoveableOptionsManager';
import { GetRenderDocument, MoveableOptionsManagerConfig, StageDragStatus, StageMultiDragResizeConfig } from './types'; import {
import { calcValueByFontsize, getMode } from './util'; DelayedMarkContainer,
GetRenderDocument,
MarkContainerEnd,
MoveableOptionsManagerConfig,
StageDragStatus,
StageMultiDragResizeConfig,
} from './types';
import { getMode } from './util';
export default class StageMultiDragResize extends MoveableOptionsManager { export default class StageMultiDragResize extends MoveableOptionsManager {
/** 画布容器 */ /** 画布容器 */
@ -34,6 +41,8 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
public dragStatus: StageDragStatus = StageDragStatus.END; public dragStatus: StageDragStatus = StageDragStatus.END;
private dragResizeHelper: DragResizeHelper; private dragResizeHelper: DragResizeHelper;
private getRenderDocument: GetRenderDocument; private getRenderDocument: GetRenderDocument;
private delayedMarkContainer: DelayedMarkContainer;
private markContainerEnd: MarkContainerEnd;
constructor(config: StageMultiDragResizeConfig) { constructor(config: StageMultiDragResizeConfig) {
const moveableOptionsManagerConfig: MoveableOptionsManagerConfig = { const moveableOptionsManagerConfig: MoveableOptionsManagerConfig = {
@ -43,13 +52,12 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
}; };
super(moveableOptionsManagerConfig); super(moveableOptionsManagerConfig);
this.delayedMarkContainer = config.delayedMarkContainer;
this.markContainerEnd = config.markContainerEnd;
this.container = config.container; this.container = config.container;
this.getRenderDocument = config.getRenderDocument; this.getRenderDocument = config.getRenderDocument;
this.dragResizeHelper = new DragResizeHelper({ this.dragResizeHelper = config.dragResizeHelper;
container: config.container,
updateDragEl: config.updateDragEl,
});
this.on('update-moveable', () => { this.on('update-moveable', () => {
if (this.moveableForMulti) { if (this.moveableForMulti) {
@ -85,6 +93,8 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
}), }),
); );
let timeout: NodeJS.Timeout | undefined;
this.moveableForMulti this.moveableForMulti
.on('resizeGroupStart', (e) => { .on('resizeGroupStart', (e) => {
this.dragResizeHelper.onResizeGroupStart(e); this.dragResizeHelper.onResizeGroupStart(e);
@ -103,11 +113,18 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
this.dragStatus = StageDragStatus.START; this.dragStatus = StageDragStatus.START;
}) })
.on('dragGroup', (e) => { .on('dragGroup', (e) => {
if (timeout) {
globalThis.clearTimeout(timeout);
timeout = undefined;
}
timeout = this.delayedMarkContainer(e.inputEvent, this.targetList);
this.dragResizeHelper.onDragGroup(e); this.dragResizeHelper.onDragGroup(e);
this.dragStatus = StageDragStatus.ING; this.dragStatus = StageDragStatus.ING;
}) })
.on('dragGroupEnd', () => { .on('dragGroupEnd', () => {
this.update(); const parentEl = this.markContainerEnd();
this.update(false, parentEl);
this.dragStatus = StageDragStatus.END; this.dragStatus = StageDragStatus.END;
}) })
.on('clickGroup', (e) => { .on('clickGroup', (e) => {
@ -182,22 +199,19 @@ export default class StageMultiDragResize extends MoveableOptionsManager {
* *
* @param isResize * @param isResize
*/ */
private update(isResize = false): void { private update(isResize = false, parentEl: HTMLElement | null = null): void {
if (this.targetList.length === 0) return; if (this.targetList.length === 0) return;
const doc = this.getRenderDocument(); const doc = this.getRenderDocument();
if (!doc) return; if (!doc) return;
const data = this.targetList.map((targetItem) => { const data = this.targetList.map((targetItem) => {
const left = calcValueByFontsize(doc, targetItem.offsetLeft); const rect = this.dragResizeHelper.getUpdatedElRect(targetItem, parentEl, doc);
const top = calcValueByFontsize(doc, targetItem.offsetTop);
const width = calcValueByFontsize(doc, targetItem.clientWidth);
const height = calcValueByFontsize(doc, targetItem.clientHeight);
return { return {
el: targetItem, el: targetItem,
style: isResize ? { left, top, width, height } : { left, top }, style: isResize ? rect : { left: rect.left, top: rect.top },
}; };
}); });
this.emit('update', data, null); this.emit('update', { data, parentEl });
} }
} }

View File

@ -22,6 +22,7 @@ import Core from '@tmagic/core';
import type { Id, MApp, MContainer, MNode } from '@tmagic/schema'; import type { Id, MApp, MContainer, MNode } from '@tmagic/schema';
import { GuidesType, ZIndex } from './const'; import { GuidesType, ZIndex } from './const';
import DragResizeHelper from './DragResizeHelper';
import StageCore from './StageCore'; import StageCore from './StageCore';
export type TargetElement = HTMLElement | SVGElement; export type TargetElement = HTMLElement | SVGElement;
@ -112,21 +113,23 @@ export interface StageMaskConfig {
export interface StageDragResizeConfig { export interface StageDragResizeConfig {
container: HTMLElement; container: HTMLElement;
dragResizeHelper: DragResizeHelper;
moveableOptions?: CustomizeMoveableOptions; moveableOptions?: CustomizeMoveableOptions;
disabledDragStart?: boolean; disabledDragStart?: boolean;
getRootContainer: GetRootContainer; getRootContainer: GetRootContainer;
getRenderDocument: GetRenderDocument; getRenderDocument: GetRenderDocument;
markContainerEnd: MarkContainerEnd; markContainerEnd: MarkContainerEnd;
delayedMarkContainer: DelayedMarkContainer; delayedMarkContainer: DelayedMarkContainer;
updateDragEl?: UpdateDragEl;
} }
export interface StageMultiDragResizeConfig { export interface StageMultiDragResizeConfig {
container: HTMLElement; container: HTMLElement;
dragResizeHelper: DragResizeHelper;
multiMoveableOptions?: CustomizeMoveableOptions; multiMoveableOptions?: CustomizeMoveableOptions;
getRootContainer: GetRootContainer; getRootContainer: GetRootContainer;
getRenderDocument: GetRenderDocument; getRenderDocument: GetRenderDocument;
updateDragEl?: UpdateDragEl; markContainerEnd: MarkContainerEnd;
delayedMarkContainer: DelayedMarkContainer;
} }
export interface DragResizeHelperConfig { export interface DragResizeHelperConfig {