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 { CONTAINER_HIGHLIGHT_CLASS_NAME, GHOST_EL_ID_PREFIX, GuidesType, MouseButton, PAGE_CLASS } from './const';
import DragResizeHelper from './DragResizeHelper';
import StageDragResize from './StageDragResize';
import StageHighlight from './StageHighlight';
import StageMultiDragResize from './StageMultiDragResize';
@ -101,27 +102,30 @@ export default class ActionManager extends EventEmitter {
this.getRenderDocument = config.getRenderDocument;
this.isContainer = config.isContainer;
const createDrHelper = () =>
new DragResizeHelper({
container: config.container,
updateDragEl: config.updateDragEl,
});
this.dr = new StageDragResize({
container: config.container,
disabledDragStart: config.disabledDragStart,
moveableOptions: this.changeCallback(config.moveableOptions),
dragResizeHelper: createDrHelper(),
getRootContainer: config.getRootContainer,
getRenderDocument: config.getRenderDocument,
updateDragEl: config.updateDragEl,
markContainerEnd: () => this.markContainerEnd(),
delayedMarkContainer: (event: MouseEvent, exclude: Element[]) => {
if (this.canAddToContainer()) {
return this.delayedMarkContainer(event, exclude);
}
return undefined;
},
moveableOptions: this.changeCallback(config.moveableOptions),
markContainerEnd: this.markContainerEnd.bind(this),
delayedMarkContainer: this.delayedMarkContainer.bind(this),
});
this.multiDr = new StageMultiDragResize({
container: config.container,
multiMoveableOptions: config.multiMoveableOptions,
dragResizeHelper: createDrHelper(),
getRootContainer: config.getRootContainer,
getRenderDocument: config.getRenderDocument,
updateDragEl: config.updateDragEl,
markContainerEnd: this.markContainerEnd.bind(this),
delayedMarkContainer: this.delayedMarkContainer.bind(this),
});
this.highlightLayer = new StageHighlight({
container: config.container,
@ -320,10 +324,13 @@ export default class ActionManager extends EventEmitter {
* @param excludeElList
* @returns timeoutIdtimeout
*/
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout {
return globalThis.setTimeout(() => {
this.addContainerHighlightClassName(event, excludeElList);
}, this.containerHighlightDuration);
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
if (this.canAddToContainer()) {
return globalThis.setTimeout(() => {
this.addContainerHighlightClassName(event, excludeElList);
}, this.containerHighlightDuration);
}
return undefined;
}
public destroy(): void {
@ -466,8 +473,8 @@ export default class ActionManager extends EventEmitter {
});
this.multiDr
.on('update', (data: UpdateEventData, parentEl: HTMLElement | null) => {
this.emit('multi-update', data, parentEl);
.on('update', (data: UpdateEventData) => {
this.emit('multi-update', data);
})
.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 TargetShadow from './TargetShadow';
import { DragResizeHelperConfig, TargetElement } from './types';
import { getAbsolutePosition, getOffset } from './util';
import { DragResizeHelperConfig, Rect, TargetElement } from './types';
import { calcValueByFontsize, getAbsolutePosition, getOffset } from './util';
/**
* /moveable会抛出各种状态事件DragResizeHelper负责响应这些事件target和拖拽节点targetShadow进行修改
@ -282,6 +282,44 @@ export default class DragResizeHelper {
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代替
*/
public getAddContainerHighlightClassNameTimeout(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout {
public getAddContainerHighlightClassNameTimeout(
event: MouseEvent,
excludeElList: Element[] = [],
): NodeJS.Timeout | undefined {
return this.delayedMarkContainer(event, excludeElList);
}
@ -200,7 +203,7 @@ export default class StageCore extends EventEmitter {
* @param excludeElList
* @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);
}
@ -331,8 +334,8 @@ export default class StageCore extends EventEmitter {
// 先保证画布内完成渲染,再通知外部更新
setTimeout(() => this.emit('select', el));
})
.on('multi-update', (data: UpdateEventData, parentEl: HTMLElement | null) => {
this.emit('update', { data, parentEl });
.on('multi-update', (data: UpdateEventData) => {
this.emit('update', data);
});
}

View File

@ -24,7 +24,7 @@ import DragResizeHelper from './DragResizeHelper';
import MoveableOptionsManager from './MoveableOptionsManager';
import type { DelayedMarkContainer, GetRenderDocument, MarkContainerEnd, StageDragResizeConfig } from './types';
import { StageDragStatus } from './types';
import { calcValueByFontsize, down, getMode, getOffset, up } from './util';
import { down, getMode, up } from './util';
/**
* moveableOption参数并初始化moveablemoveable回调事件对组件进行更新
@ -51,10 +51,7 @@ export default class StageDragResize extends MoveableOptionsManager {
this.delayedMarkContainer = config.delayedMarkContainer;
this.disabledDragStart = config.disabledDragStart;
this.dragResizeHelper = new DragResizeHelper({
container: config.container,
updateDragEl: config.updateDragEl,
});
this.dragResizeHelper = config.dragResizeHelper;
this.on('update-moveable', () => {
if (this.moveable) {
@ -315,40 +312,13 @@ export default class StageDragResize extends MoveableOptionsManager {
if (!doc) return;
const offset =
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);
}
const rect = this.dragResizeHelper.getUpdatedElRect(this.target, parentEl, doc);
this.emit('update', {
data: [
{
el: this.target,
style: isResize ? { left, top, width, height } : { left, top },
style: isResize ? rect : { left: rect.left, top: rect.top },
},
],
parentEl,

View File

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