mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-05-08 03:16:39 +08:00
支持通过按住shift键进行组件多选的能力 (#193)
* feat(stage): 支持绝对定位,固定定位,组内元素按住shift键进行多选拖拽能力 * refactor: 更新pnpm-lock.yaml * feat(stage): 使用moveable.helper接管moveable target的更新,针对弹窗场景引入业务方方法进行校准 * feat(stage): 将多选逻辑封装到StageMultiDragResize * fix(stage): cr意见修改 Co-authored-by: parisma <parisma@tencent.com>
This commit is contained in:
parent
1750467d5b
commit
fe520bf600
@ -40,7 +40,6 @@ import {
|
|||||||
isFixed,
|
isFixed,
|
||||||
setLayout,
|
setLayout,
|
||||||
} from '@editor/utils/editor';
|
} from '@editor/utils/editor';
|
||||||
import { log } from '@editor/utils/logger';
|
|
||||||
|
|
||||||
import BaseService from './BaseService';
|
import BaseService from './BaseService';
|
||||||
|
|
||||||
@ -90,7 +89,6 @@ class Editor extends BaseService {
|
|||||||
*/
|
*/
|
||||||
public set<T = MNode>(name: keyof StoreState, value: T) {
|
public set<T = MNode>(name: keyof StoreState, value: T) {
|
||||||
this.state[name] = value as any;
|
this.state[name] = value as any;
|
||||||
log('store set ', name, ' ', value);
|
|
||||||
|
|
||||||
if (name === 'root') {
|
if (name === 'root') {
|
||||||
this.state.pageLength = (value as unknown as MApp)?.items?.length || 0;
|
this.state.pageLength = (value as unknown as MApp)?.items?.length || 0;
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"@tmagic/schema": "1.1.0-beta.2",
|
"@tmagic/schema": "1.1.0-beta.2",
|
||||||
"@tmagic/utils": "1.1.0-beta.2",
|
"@tmagic/utils": "1.1.0-beta.2",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
|
"keycon": "^1.1.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moveable": "^0.30.0",
|
"moveable": "^0.30.0",
|
||||||
"moveable-helper": "^0.4.0"
|
"moveable-helper": "^0.4.0"
|
||||||
|
@ -20,10 +20,11 @@ import { EventEmitter } from 'events';
|
|||||||
|
|
||||||
import type { Id } from '@tmagic/schema';
|
import type { Id } from '@tmagic/schema';
|
||||||
|
|
||||||
import { DEFAULT_ZOOM, GHOST_EL_ID_PREFIX } from './const';
|
import { DEFAULT_ZOOM, GHOST_EL_ID_PREFIX, PAGE_CLASS } from './const';
|
||||||
import StageDragResize from './StageDragResize';
|
import StageDragResize from './StageDragResize';
|
||||||
import StageHighlight from './StageHighlight';
|
import StageHighlight from './StageHighlight';
|
||||||
import StageMask from './StageMask';
|
import StageMask from './StageMask';
|
||||||
|
import StageMultiDragResize from './StageMultiDragResize';
|
||||||
import StageRender from './StageRender';
|
import StageRender from './StageRender';
|
||||||
import {
|
import {
|
||||||
CanSelect,
|
CanSelect,
|
||||||
@ -40,12 +41,15 @@ import { addSelectedClassName, removeSelectedClassName } from './util';
|
|||||||
|
|
||||||
export default class StageCore extends EventEmitter {
|
export default class StageCore extends EventEmitter {
|
||||||
public container?: HTMLDivElement;
|
public container?: HTMLDivElement;
|
||||||
|
// 当前选中的节点
|
||||||
public selectedDom: Element | undefined;
|
public selectedDom: HTMLElement | undefined;
|
||||||
|
// 多选选中的节点组
|
||||||
|
public selectedDomList: HTMLElement[] = [];
|
||||||
public highlightedDom: Element | undefined;
|
public highlightedDom: Element | undefined;
|
||||||
public renderer: StageRender;
|
public renderer: StageRender;
|
||||||
public mask: StageMask;
|
public mask: StageMask;
|
||||||
public dr: StageDragResize;
|
public dr: StageDragResize;
|
||||||
|
public multiDr: StageMultiDragResize;
|
||||||
public highlightLayer: StageHighlight;
|
public highlightLayer: StageHighlight;
|
||||||
public config: StageCoreConfig;
|
public config: StageCoreConfig;
|
||||||
public zoom = DEFAULT_ZOOM;
|
public zoom = DEFAULT_ZOOM;
|
||||||
@ -68,7 +72,8 @@ export default class StageCore extends EventEmitter {
|
|||||||
|
|
||||||
this.renderer = new StageRender({ core: this });
|
this.renderer = new StageRender({ core: this });
|
||||||
this.mask = new StageMask({ core: this });
|
this.mask = new StageMask({ core: this });
|
||||||
this.dr = new StageDragResize({ core: this, container: this.mask.content });
|
this.dr = new StageDragResize({ core: this, container: this.mask.content, mask: this.mask });
|
||||||
|
this.multiDr = new StageMultiDragResize({ core: this, container: this.mask.content, mask: this.mask });
|
||||||
this.highlightLayer = new StageHighlight({ core: this, container: this.mask.wrapper });
|
this.highlightLayer = new StageHighlight({ core: this, container: this.mask.wrapper });
|
||||||
|
|
||||||
this.renderer.on('runtime-ready', (runtime: Runtime) => {
|
this.renderer.on('runtime-ready', (runtime: Runtime) => {
|
||||||
@ -79,8 +84,11 @@ export default class StageCore extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.mask
|
this.mask
|
||||||
.on('beforeSelect', (event: MouseEvent) => {
|
.on('beforeSelect', async (event: MouseEvent) => {
|
||||||
this.setElementFromPoint(event);
|
this.clearSelectStatus('multiSelect');
|
||||||
|
const el = await this.setElementFromPoint(event);
|
||||||
|
if (!el) return;
|
||||||
|
this.select(el, event);
|
||||||
})
|
})
|
||||||
.on('select', () => {
|
.on('select', () => {
|
||||||
this.emit('select', this.selectedDom);
|
this.emit('select', this.selectedDom);
|
||||||
@ -90,16 +98,39 @@ export default class StageCore extends EventEmitter {
|
|||||||
this.emit('changeGuides', data);
|
this.emit('changeGuides', data);
|
||||||
})
|
})
|
||||||
.on('highlight', async (event: MouseEvent) => {
|
.on('highlight', async (event: MouseEvent) => {
|
||||||
await this.setElementFromPoint(event);
|
const el = await this.setElementFromPoint(event, 'mousemove');
|
||||||
|
if (!el) return;
|
||||||
|
await this.highlight(el);
|
||||||
if (this.highlightedDom === this.selectedDom) {
|
if (this.highlightedDom === this.selectedDom) {
|
||||||
this.highlightLayer.clearHighlight();
|
this.highlightLayer.clearHighlight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.highlightLayer.highlight(this.highlightedDom as HTMLElement);
|
|
||||||
this.emit('highlight', this.highlightedDom);
|
this.emit('highlight', this.highlightedDom);
|
||||||
})
|
})
|
||||||
.on('clearHighlight', async () => {
|
.on('clearHighlight', async () => {
|
||||||
this.highlightLayer.clearHighlight();
|
this.highlightLayer.clearHighlight();
|
||||||
|
})
|
||||||
|
.on('beforeMultiSelect', async (event: MouseEvent) => {
|
||||||
|
const el = await this.setElementFromPoint(event);
|
||||||
|
if (!el) return;
|
||||||
|
// 多选不可以选中magic-ui-page
|
||||||
|
if (el.className.includes(PAGE_CLASS)) return;
|
||||||
|
this.clearSelectStatus('select');
|
||||||
|
// 如果已有单选选中元素,不是magic-ui-page就可以加入多选列表
|
||||||
|
if (this.selectedDom && !this.selectedDom.className.includes(PAGE_CLASS)) {
|
||||||
|
this.selectedDomList.push(this.selectedDom as HTMLElement);
|
||||||
|
this.selectedDom = undefined;
|
||||||
|
}
|
||||||
|
// 判断元素是否已在多选列表
|
||||||
|
const existIndex = this.selectedDomList.findIndex((selectedDom) => selectedDom.id === el.id);
|
||||||
|
if (existIndex !== -1) {
|
||||||
|
// 再次点击取消选中
|
||||||
|
this.selectedDomList.splice(existIndex, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedDomList.push(el);
|
||||||
|
}
|
||||||
|
this.multiDr.multiSelect(this.selectedDomList);
|
||||||
|
this.emit('multiSelect', this.selectedDomList);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 要先触发select,在触发update
|
// 要先触发select,在触发update
|
||||||
@ -130,20 +161,17 @@ export default class StageCore extends EventEmitter {
|
|||||||
return doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
|
return doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setElementFromPoint(event: MouseEvent) {
|
public async setElementFromPoint(event: MouseEvent, type?: String) {
|
||||||
const els = this.getElementsFromPoint(event);
|
const els = this.getElementsFromPoint(event);
|
||||||
|
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
const stop = () => (stopped = true);
|
const stop = () => (stopped = true);
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, event, stop))) {
|
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, event, stop))) {
|
||||||
if (stopped) break;
|
if (stopped) break;
|
||||||
if (event.type === 'mousemove') {
|
if (event.type === type) {
|
||||||
this.highlight(el);
|
return el;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
this.select(el, event);
|
return el;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,6 +194,7 @@ export default class StageCore extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.mask.setLayout(el);
|
this.mask.setLayout(el);
|
||||||
|
this.multiDr.destroyDragElList();
|
||||||
this.dr.select(el, event);
|
this.dr.select(el, event);
|
||||||
|
|
||||||
if (this.config.autoScrollIntoView || el.dataset.autoScrollIntoView) {
|
if (this.config.autoScrollIntoView || el.dataset.autoScrollIntoView) {
|
||||||
@ -217,7 +246,7 @@ export default class StageCore extends EventEmitter {
|
|||||||
this.highlightLayer.clearHighlight();
|
this.highlightLayer.clearHighlight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (el === this.highlightedDom) return;
|
if (el === this.highlightedDom || !el) return;
|
||||||
this.highlightLayer.highlight(el);
|
this.highlightLayer.highlight(el);
|
||||||
this.highlightedDom = el;
|
this.highlightedDom = el;
|
||||||
}
|
}
|
||||||
@ -238,6 +267,19 @@ export default class StageCore extends EventEmitter {
|
|||||||
this.zoom = zoom;
|
this.zoom = zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于在切换选择模式时清除上一次的状态
|
||||||
|
* @param selectType 需要清理的选择模式 多选:multiSelect,单选:select
|
||||||
|
*/
|
||||||
|
public clearSelectStatus(selectType: String) {
|
||||||
|
if (selectType === 'multiSelect') {
|
||||||
|
this.multiDr.clearSelectStatus();
|
||||||
|
this.selectedDomList = [];
|
||||||
|
} else {
|
||||||
|
this.dr.clearSelectStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 挂载Dom节点
|
* 挂载Dom节点
|
||||||
* @param el 将stage挂载到该Dom节点上
|
* @param el 将stage挂载到该Dom节点上
|
||||||
|
@ -27,8 +27,9 @@ import { addClassName, removeClassNameByClassName } from '@tmagic/utils';
|
|||||||
|
|
||||||
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode, ZIndex } from './const';
|
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode, ZIndex } from './const';
|
||||||
import StageCore from './StageCore';
|
import StageCore from './StageCore';
|
||||||
|
import StageMask from './StageMask';
|
||||||
import type { SortEventData, StageDragResizeConfig } from './types';
|
import type { SortEventData, StageDragResizeConfig } from './types';
|
||||||
import { calcValueByFontsize, getAbsolutePosition, getMode, getOffset } from './util';
|
import { calcValueByFontsize, getAbsolutePosition, getMode, getOffset, getTargetElStyle } from './util';
|
||||||
|
|
||||||
/** 拖动状态 */
|
/** 拖动状态 */
|
||||||
enum ActionStatus {
|
enum ActionStatus {
|
||||||
@ -45,14 +46,21 @@ enum ActionStatus {
|
|||||||
*/
|
*/
|
||||||
export default class StageDragResize extends EventEmitter {
|
export default class StageDragResize extends EventEmitter {
|
||||||
public core: StageCore;
|
public core: StageCore;
|
||||||
|
public mask: StageMask;
|
||||||
/** 画布容器 */
|
/** 画布容器 */
|
||||||
public container: HTMLElement;
|
public container: HTMLElement;
|
||||||
/** 目标节点 */
|
/** 目标节点 */
|
||||||
public target?: HTMLElement;
|
public target?: HTMLElement;
|
||||||
/** 目标节点在蒙层中的占位节点 */
|
/** 目标节点在蒙层中的占位节点 */
|
||||||
public dragEl: HTMLDivElement;
|
public dragEl?: HTMLDivElement;
|
||||||
|
/** 多选:目标节点组 */
|
||||||
|
public targetList: HTMLElement[] = [];
|
||||||
|
/** 多选:目标节点在蒙层中的占位节点组 */
|
||||||
|
public dragElList: HTMLDivElement[] = [];
|
||||||
/** Moveable拖拽类实例 */
|
/** Moveable拖拽类实例 */
|
||||||
public moveable?: Moveable;
|
public moveable?: Moveable;
|
||||||
|
/** Moveable多选拖拽类实例 */
|
||||||
|
public moveableForMulti?: Moveable;
|
||||||
/** 水平参考线 */
|
/** 水平参考线 */
|
||||||
public horizontalGuidelines: number[] = [];
|
public horizontalGuidelines: number[] = [];
|
||||||
/** 垂直参考线 */
|
/** 垂直参考线 */
|
||||||
@ -74,9 +82,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
|
|
||||||
this.core = config.core;
|
this.core = config.core;
|
||||||
this.container = config.container;
|
this.container = config.container;
|
||||||
|
this.mask = config.mask;
|
||||||
this.dragEl = globalThis.document.createElement('div');
|
|
||||||
this.container.append(this.dragEl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,6 +153,17 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.updateMoveable();
|
this.updateMoveable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearSelectStatus(): void {
|
||||||
|
if (!this.moveable) return;
|
||||||
|
this.destroyDragEl();
|
||||||
|
this.moveable.target = null;
|
||||||
|
this.moveable.updateTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroyDragEl(): void {
|
||||||
|
this.dragEl?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
@ -166,9 +183,15 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.mode = getMode(el);
|
this.mode = getMode(el);
|
||||||
|
|
||||||
this.destroyGhostEl();
|
this.destroyGhostEl();
|
||||||
|
this.destroyDragEl();
|
||||||
|
this.dragEl = globalThis.document.createElement('div');
|
||||||
|
this.container.append(this.dragEl);
|
||||||
|
this.dragEl.style.cssText = getTargetElStyle(el);
|
||||||
|
this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
|
||||||
|
|
||||||
this.updateDragEl(el);
|
if (typeof this.core.config.updateDragEl === 'function') {
|
||||||
|
this.core.config.updateDragEl(this.dragEl, el);
|
||||||
|
}
|
||||||
this.moveableOptions = this.getOptions({
|
this.moveableOptions = this.getOptions({
|
||||||
target: this.dragEl,
|
target: this.dragEl,
|
||||||
});
|
});
|
||||||
@ -282,7 +305,6 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
if (this.mode === Mode.SORTABLE) {
|
if (this.mode === Mode.SORTABLE) {
|
||||||
this.ghostEl = this.generateGhostEl(this.target);
|
this.ghostEl = this.generateGhostEl(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.top = this.target.offsetTop;
|
frame.top = this.target.offsetTop;
|
||||||
frame.left = this.target.offsetLeft;
|
frame.left = this.target.offsetLeft;
|
||||||
})
|
})
|
||||||
@ -483,31 +505,6 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.ghostEl = undefined;
|
this.ghostEl = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDragEl(el: HTMLElement) {
|
|
||||||
const offset = getOffset(el);
|
|
||||||
const { transform } = getComputedStyle(el);
|
|
||||||
|
|
||||||
this.dragEl.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
transform: ${transform};
|
|
||||||
left: ${offset.left}px;
|
|
||||||
top: ${offset.top}px;
|
|
||||||
width: ${el.clientWidth}px;
|
|
||||||
height: ${el.clientHeight}px;
|
|
||||||
z-index: ${ZIndex.DRAG_EL};
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
|
|
||||||
|
|
||||||
if (typeof this.core.config.updateDragEl === 'function') {
|
|
||||||
this.core.config.updateDragEl(this.dragEl, el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroyDragEl(): void {
|
|
||||||
this.dragEl?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
||||||
if (!this.target) return {};
|
if (!this.target) return {};
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import KeyController from 'keycon';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
import { createDiv, injectStyle } from '@tmagic/utils';
|
import { createDiv, injectStyle } from '@tmagic/utils';
|
||||||
@ -82,6 +83,7 @@ export default class StageMask extends Rule {
|
|||||||
public maxScrollTop = 0;
|
public maxScrollTop = 0;
|
||||||
public maxScrollLeft = 0;
|
public maxScrollLeft = 0;
|
||||||
public intersectionObserver: IntersectionObserver | null = null;
|
public intersectionObserver: IntersectionObserver | null = null;
|
||||||
|
public shiftKeyDown: Boolean = false;
|
||||||
|
|
||||||
private mode: Mode = Mode.ABSOLUTE;
|
private mode: Mode = Mode.ABSOLUTE;
|
||||||
private pageResizeObserver: ResizeObserver | null = null;
|
private pageResizeObserver: ResizeObserver | null = null;
|
||||||
@ -106,6 +108,14 @@ export default class StageMask extends Rule {
|
|||||||
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
||||||
this.content.addEventListener('mousemove', this.highlightHandler);
|
this.content.addEventListener('mousemove', this.highlightHandler);
|
||||||
this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
|
this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
|
||||||
|
KeyController.global.keydown('shift', (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.shiftKeyDown = true;
|
||||||
|
});
|
||||||
|
KeyController.global.keyup('shift', (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.shiftKeyDown = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMode(mode: Mode) {
|
public setMode(mode: Mode) {
|
||||||
@ -292,23 +302,30 @@ export default class StageMask extends Rule {
|
|||||||
*/
|
*/
|
||||||
private mouseDownHandler = (event: MouseEvent): void => {
|
private mouseDownHandler = (event: MouseEvent): void => {
|
||||||
this.emit('clearHighlight');
|
this.emit('clearHighlight');
|
||||||
|
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
|
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
|
||||||
|
|
||||||
// 点击的对象如果是选中框,则不需要再触发选中了,而可能是拖动行为
|
// 如果单击多选选中区域,则不需要再触发选中了,而可能是拖动行为
|
||||||
|
if (!this.shiftKeyDown && (event.target as HTMLDivElement).className.indexOf('moveable-area') !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 点击对象如果是边框锚点,则可能是resize
|
||||||
if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
|
if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.content.removeEventListener('mousemove', this.highlightHandler);
|
this.content.removeEventListener('mousemove', this.highlightHandler);
|
||||||
|
|
||||||
this.emit('beforeSelect', event);
|
// 判断触发多选还是单选
|
||||||
|
if (this.shiftKeyDown) {
|
||||||
// 如果是右键点击,这里的mouseup事件监听没有效果
|
this.emit('beforeMultiSelect', event);
|
||||||
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
} else {
|
||||||
|
this.emit('beforeSelect', event);
|
||||||
|
// 如果是右键点击,这里的mouseup事件监听没有效果
|
||||||
|
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private mouseUpHandler = (): void => {
|
private mouseUpHandler = (): void => {
|
||||||
|
156
packages/stage/src/StageMultiDragResize.ts
Normal file
156
packages/stage/src/StageMultiDragResize.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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 { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
import Moveable from 'moveable';
|
||||||
|
import MoveableHelper from 'moveable-helper';
|
||||||
|
|
||||||
|
import { DRAG_EL_ID_PREFIX } from './const';
|
||||||
|
import StageCore from './StageCore';
|
||||||
|
import StageMask from './StageMask';
|
||||||
|
import { StageDragResizeConfig } from './types';
|
||||||
|
import { getTargetElStyle } from './util';
|
||||||
|
export default class StageMultiDragResize extends EventEmitter {
|
||||||
|
public core: StageCore;
|
||||||
|
public mask: StageMask;
|
||||||
|
/** 画布容器 */
|
||||||
|
public container: HTMLElement;
|
||||||
|
/** 多选:目标节点组 */
|
||||||
|
public targetList: HTMLElement[] = [];
|
||||||
|
/** 多选:目标节点在蒙层中的占位节点组 */
|
||||||
|
public dragElList: HTMLDivElement[] = [];
|
||||||
|
/** Moveable多选拖拽类实例 */
|
||||||
|
public moveableForMulti?: Moveable;
|
||||||
|
private multiMoveableHelper?: MoveableHelper;
|
||||||
|
|
||||||
|
constructor(config: StageDragResizeConfig) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.core = config.core;
|
||||||
|
this.container = config.container;
|
||||||
|
this.mask = config.mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多选
|
||||||
|
* @param els
|
||||||
|
*/
|
||||||
|
public multiSelect(els: HTMLElement[]): void {
|
||||||
|
this.targetList = els;
|
||||||
|
this.core.dr.destroyDragEl();
|
||||||
|
this.destroyDragElList();
|
||||||
|
// 生成虚拟多选节点
|
||||||
|
this.dragElList = els.map((elItem) => {
|
||||||
|
const dragElDiv = globalThis.document.createElement('div');
|
||||||
|
this.container.append(dragElDiv);
|
||||||
|
dragElDiv.style.cssText = getTargetElStyle(elItem);
|
||||||
|
dragElDiv.id = `${DRAG_EL_ID_PREFIX}${elItem.id}`;
|
||||||
|
// 业务方校准
|
||||||
|
if (typeof this.core.config.updateDragEl === 'function') {
|
||||||
|
this.core.config.updateDragEl(dragElDiv, elItem);
|
||||||
|
}
|
||||||
|
return dragElDiv;
|
||||||
|
});
|
||||||
|
this.moveableForMulti?.destroy();
|
||||||
|
this.multiMoveableHelper?.clear();
|
||||||
|
|
||||||
|
this.moveableForMulti = new Moveable(this.container, {
|
||||||
|
target: this.dragElList,
|
||||||
|
defaultGroupRotate: 0,
|
||||||
|
defaultGroupOrigin: '50% 50%',
|
||||||
|
draggable: true,
|
||||||
|
resizable: true,
|
||||||
|
throttleDrag: 0,
|
||||||
|
startDragRotate: 0,
|
||||||
|
throttleDragRotate: 0,
|
||||||
|
zoom: 1,
|
||||||
|
origin: true,
|
||||||
|
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
||||||
|
});
|
||||||
|
this.multiMoveableHelper = MoveableHelper.create({
|
||||||
|
useBeforeRender: true,
|
||||||
|
useRender: false,
|
||||||
|
createAuto: true,
|
||||||
|
});
|
||||||
|
const frames: { left: number; top: number; dragLeft: number; dragTop: number; id: string }[] = [];
|
||||||
|
this.moveableForMulti
|
||||||
|
.on('dragGroupStart', (params) => {
|
||||||
|
const { events } = params;
|
||||||
|
this.multiMoveableHelper?.onDragGroupStart(params);
|
||||||
|
// 记录拖动前快照
|
||||||
|
events.forEach((ev) => {
|
||||||
|
// 实际目标元素
|
||||||
|
const matchEventTarget = this.targetList.find((targetItem) => targetItem.id === ev.target.id.split('_')[2]);
|
||||||
|
// 蒙层虚拟元素(对于在组内的元素拖动时的相对位置不同,因此需要分别记录)
|
||||||
|
const dragEventTarget = ev.target as HTMLDivElement;
|
||||||
|
if (!matchEventTarget || !dragEventTarget) return;
|
||||||
|
frames.push({
|
||||||
|
left: matchEventTarget.offsetLeft,
|
||||||
|
top: matchEventTarget.offsetTop,
|
||||||
|
dragLeft: dragEventTarget.offsetLeft,
|
||||||
|
dragTop: dragEventTarget.offsetTop,
|
||||||
|
id: matchEventTarget.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('dragGroup', (params) => {
|
||||||
|
const { events } = params;
|
||||||
|
// 拖动过程更新
|
||||||
|
events.forEach((ev) => {
|
||||||
|
const frameSnapShot = frames.find((frameItem) => frameItem.id === ev.target.id.split('_')[2]);
|
||||||
|
if (!frameSnapShot) return;
|
||||||
|
const targeEl = this.targetList.find((targetItem) => targetItem.id === ev.target.id.split('_')[2]);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除多选状态
|
||||||
|
*/
|
||||||
|
public clearSelectStatus(): void {
|
||||||
|
if (!this.moveableForMulti) return;
|
||||||
|
this.destroyDragElList();
|
||||||
|
this.moveableForMulti.target = null;
|
||||||
|
this.moveableForMulti.updateTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁实例
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.moveableForMulti?.destroy();
|
||||||
|
this.destroyDragElList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除蒙层占位节点
|
||||||
|
*/
|
||||||
|
public destroyDragElList(): void {
|
||||||
|
this.dragElList.forEach((dragElItem) => dragElItem?.remove());
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@ export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
|
|||||||
|
|
||||||
export const CONTAINER_HIGHLIGHT_CLASS = 'tmagic-stage-container-highlight';
|
export const CONTAINER_HIGHLIGHT_CLASS = 'tmagic-stage-container-highlight';
|
||||||
|
|
||||||
|
export const PAGE_CLASS = 'magic-ui-page';
|
||||||
|
|
||||||
/** 默认放到缩小倍数 */
|
/** 默认放到缩小倍数 */
|
||||||
export const DEFAULT_ZOOM = 1;
|
export const DEFAULT_ZOOM = 1;
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ export interface StageMaskConfig {
|
|||||||
export interface StageDragResizeConfig {
|
export interface StageDragResizeConfig {
|
||||||
core: StageCore;
|
core: StageCore;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
|
mask: StageMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Rect = {
|
export type Rect = {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
import { removeClassName } from '@tmagic/utils';
|
import { removeClassName } from '@tmagic/utils';
|
||||||
|
|
||||||
import { Mode, SELECTED_CLASS } from './const';
|
import { Mode, SELECTED_CLASS, ZIndex } from './const';
|
||||||
import type { Offset } from './types';
|
import type { Offset } from './types';
|
||||||
|
|
||||||
const getParents = (el: Element, relative: Element) => {
|
const getParents = (el: Element, relative: Element) => {
|
||||||
@ -50,6 +50,21 @@ export const getOffset = (el: HTMLElement): Offset => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 将蒙层占位节点覆盖在原节点上方
|
||||||
|
export const getTargetElStyle = (el: HTMLElement) => {
|
||||||
|
const offset = getOffset(el);
|
||||||
|
const { transform } = getComputedStyle(el);
|
||||||
|
return `
|
||||||
|
position: absolute;
|
||||||
|
transform: ${transform};
|
||||||
|
left: ${offset.left}px;
|
||||||
|
top: ${offset.top}px;
|
||||||
|
width: ${el.clientWidth}px;
|
||||||
|
height: ${el.clientHeight}px;
|
||||||
|
z-index: ${ZIndex.DRAG_EL};
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
|
export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
|
||||||
const { offsetParent } = el;
|
const { offsetParent } = el;
|
||||||
|
|
||||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -254,6 +254,7 @@ importers:
|
|||||||
'@types/lodash-es': ^4.17.4
|
'@types/lodash-es': ^4.17.4
|
||||||
'@types/node': ^15.12.4
|
'@types/node': ^15.12.4
|
||||||
events: ^3.3.0
|
events: ^3.3.0
|
||||||
|
keycon: ^1.1.2
|
||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
moveable: ^0.30.0
|
moveable: ^0.30.0
|
||||||
moveable-helper: ^0.4.0
|
moveable-helper: ^0.4.0
|
||||||
@ -267,6 +268,7 @@ importers:
|
|||||||
'@tmagic/schema': link:../schema
|
'@tmagic/schema': link:../schema
|
||||||
'@tmagic/utils': link:../utils
|
'@tmagic/utils': link:../utils
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
|
keycon: 1.1.2
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
moveable: 0.30.0
|
moveable: 0.30.0
|
||||||
moveable-helper: 0.4.0
|
moveable-helper: 0.4.0
|
||||||
@ -3075,7 +3077,7 @@ packages:
|
|||||||
lodash: ^4.17.20
|
lodash: ^4.17.20
|
||||||
marko: ^3.14.4
|
marko: ^3.14.4
|
||||||
mote: ^0.2.0
|
mote: ^0.2.0
|
||||||
mustache: ^3.0.0
|
mustache: ^4.0.1
|
||||||
nunjucks: ^3.2.2
|
nunjucks: ^3.2.2
|
||||||
plates: ~0.4.11
|
plates: ~0.4.11
|
||||||
pug: ^3.0.0
|
pug: ^3.0.0
|
||||||
@ -5519,7 +5521,7 @@ packages:
|
|||||||
/keycon/1.1.2:
|
/keycon/1.1.2:
|
||||||
resolution: {integrity: sha512-yCoUAfwqmQUWrtOFuZhicxasF/4ae+M0aH8yV1wEKKZCZql8v6jWhlVF9dT5i1TfuHSmgt/GNuCaWIHT8wk6eQ==}
|
resolution: {integrity: sha512-yCoUAfwqmQUWrtOFuZhicxasF/4ae+M0aH8yV1wEKKZCZql8v6jWhlVF9dT5i1TfuHSmgt/GNuCaWIHT8wk6eQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@daybrush/utils': 1.6.0
|
'@daybrush/utils': 1.7.1
|
||||||
'@scena/event-emitter': 1.0.5
|
'@scena/event-emitter': 1.0.5
|
||||||
keycode: 2.2.1
|
keycode: 2.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user