mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat: 优化拖拽体验
This commit is contained in:
parent
be4df0fc9b
commit
a842c5b0ce
@ -59,6 +59,7 @@ import { throttle } from 'lodash-es';
|
||||
|
||||
import type { MNode, MPage } from '@tmagic/schema';
|
||||
import { NodeType } from '@tmagic/schema';
|
||||
import StageCore from '@tmagic/stage';
|
||||
|
||||
import type { EditorService } from '@editor/services/editor';
|
||||
import type { Services } from '@editor/type';
|
||||
@ -73,6 +74,7 @@ const select = (data: MNode, editorService?: EditorService) => {
|
||||
}
|
||||
|
||||
editorService?.select(data);
|
||||
editorService?.get<StageCore>('stage')?.select(data.id);
|
||||
};
|
||||
|
||||
const highlight = (data: MNode, editorService?: EditorService) => {
|
||||
@ -80,6 +82,7 @@ const highlight = (data: MNode, editorService?: EditorService) => {
|
||||
throw new Error('没有id');
|
||||
}
|
||||
editorService?.highlight(data);
|
||||
editorService?.get<StageCore>('stage')?.highlight(data.id);
|
||||
};
|
||||
|
||||
const useDrop = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => ({
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
nextTick,
|
||||
markRaw,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
@ -124,7 +124,6 @@ export default defineComponent({
|
||||
const page = computed(() => services?.editorService.get<MPage>('page'));
|
||||
const zoom = computed(() => services?.uiService.get<number>('zoom'));
|
||||
const node = computed(() => services?.editorService.get<MNode>('node'));
|
||||
const highlightNode = computed(() => services?.editorService.get<MNode>('highlightNode'));
|
||||
|
||||
let stage: StageCore | null = null;
|
||||
let runtime: Runtime | null = null;
|
||||
@ -152,7 +151,7 @@ export default defineComponent({
|
||||
moveableOptions: props.moveableOptions,
|
||||
});
|
||||
|
||||
services?.editorService.set('stage', stage);
|
||||
services?.editorService.set('stage', markRaw(stage));
|
||||
|
||||
stage?.mount(stageContainer.value);
|
||||
|
||||
@ -199,25 +198,6 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => node.value?.id,
|
||||
(id) => {
|
||||
nextTick(() => {
|
||||
// 等待相关dom变更完成后,再select,适用大多数场景
|
||||
id && stage?.select(id);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => highlightNode.value?.id,
|
||||
(id) => {
|
||||
nextTick(() => {
|
||||
id && stage?.highlight(id);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const { contentRect } of entries) {
|
||||
services?.uiService.set('stageContainerRect', {
|
||||
|
@ -63,8 +63,12 @@ export default class StageCore extends EventEmitter {
|
||||
this.dr = new StageDragResize({ core: this, container: this.mask.content });
|
||||
this.highlightLayer = new StageHighlight({ core: this, container: this.mask.wrapper });
|
||||
|
||||
this.renderer.on('runtime-ready', (runtime: Runtime) => this.emit('runtime-ready', runtime));
|
||||
this.renderer.on('page-el-update', (el: HTMLElement) => this.mask?.observe(el));
|
||||
this.renderer.on('runtime-ready', (runtime: Runtime) => {
|
||||
this.emit('runtime-ready', runtime);
|
||||
});
|
||||
this.renderer.on('page-el-update', (el: HTMLElement) => {
|
||||
this.mask?.observe(el);
|
||||
});
|
||||
|
||||
this.mask
|
||||
.on('beforeSelect', (event: MouseEvent) => {
|
||||
@ -139,12 +143,14 @@ export default class StageCore extends EventEmitter {
|
||||
|
||||
const runtime = await this.renderer.getRuntime();
|
||||
|
||||
await runtime?.select?.(el.id);
|
||||
|
||||
if (runtime?.beforeSelect) {
|
||||
await runtime.beforeSelect(el);
|
||||
}
|
||||
|
||||
this.mask.setLayout(el);
|
||||
this.dr?.select(el, event);
|
||||
this.dr.select(el, event);
|
||||
this.selectedDom = el;
|
||||
|
||||
if (this.renderer.contentWindow) {
|
||||
@ -170,7 +176,7 @@ export default class StageCore extends EventEmitter {
|
||||
if (el) {
|
||||
// 更新了组件的布局,需要重新设置mask是否可以滚动
|
||||
this.mask.setLayout(el);
|
||||
this.dr?.select(el);
|
||||
this.dr.select(el);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
@ -242,14 +248,11 @@ export default class StageCore extends EventEmitter {
|
||||
}
|
||||
|
||||
private async getTargetElement(idOrEl: Id | HTMLElement): Promise<HTMLElement> {
|
||||
let el;
|
||||
if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
|
||||
const runtime = await this.renderer?.getRuntime();
|
||||
el = await runtime?.select?.(`${idOrEl}`);
|
||||
const el = this.renderer.contentWindow?.document.getElementById(`${idOrEl}`);
|
||||
if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
|
||||
} else {
|
||||
el = idOrEl;
|
||||
return el;
|
||||
}
|
||||
return el;
|
||||
return idOrEl;
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import type { MoveableOptions } from 'moveable';
|
||||
import Moveable from 'moveable';
|
||||
import MoveableHelper from 'moveable-helper';
|
||||
|
||||
import { GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
|
||||
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
|
||||
import StageCore from './StageCore';
|
||||
import type { SortEventData, StageDragResizeConfig } from './types';
|
||||
import type { Offset, Runtime, SortEventData, StageDragResizeConfig } from './types';
|
||||
import { getAbsolutePosition, getMode, getOffset } from './util';
|
||||
|
||||
enum ActionStatus {
|
||||
@ -45,6 +45,7 @@ export default class StageDragResize extends EventEmitter {
|
||||
public moveable?: Moveable;
|
||||
public horizontalGuidelines: number[] = [];
|
||||
public verticalGuidelines: number[] = [];
|
||||
public elementGuidelines: HTMLElement[] = [];
|
||||
|
||||
private moveableOptions: MoveableOptions = {};
|
||||
private dragStatus: ActionStatus = ActionStatus.END;
|
||||
@ -65,29 +66,36 @@ export default class StageDragResize extends EventEmitter {
|
||||
* @param el 选中组件的Dom节点元素
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
public async select(el: HTMLElement, event?: MouseEvent): Promise<void> {
|
||||
public select(el: HTMLElement, event?: MouseEvent): void {
|
||||
this.target = el;
|
||||
// 如果有滚动条会导致resize时获取到width,height不准确
|
||||
if (/(auto|scroll)/.test(this.target.style.overflow)) {
|
||||
this.target.style.overflow = 'hidden';
|
||||
}
|
||||
this.mode = getMode(el);
|
||||
this.destroyDragEl();
|
||||
|
||||
this.destroyGhostEl();
|
||||
|
||||
this.dragEl = this.generateDragEl(el);
|
||||
this.generateDragEl(el);
|
||||
|
||||
this.moveableOptions = await this.getOptions({
|
||||
target: this.dragEl || this.target,
|
||||
const originDraggable = this.moveableOptions.draggable;
|
||||
|
||||
this.moveableOptions = this.getOptions({
|
||||
target: this.dragEl,
|
||||
});
|
||||
|
||||
this.moveableHelper = MoveableHelper.create({
|
||||
useBeforeRender: true,
|
||||
useRender: false,
|
||||
createAuto: true,
|
||||
});
|
||||
// 从不能拖动到能拖动的节点之间切换,要重新创建moveable,不然dragStart不生效
|
||||
if (!this.moveable || originDraggable !== this.moveableOptions.draggable) {
|
||||
this.moveableHelper = MoveableHelper.create({
|
||||
useBeforeRender: true,
|
||||
useRender: false,
|
||||
createAuto: true,
|
||||
});
|
||||
|
||||
this.initMoveable();
|
||||
this.initMoveable();
|
||||
} else {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
if (event) {
|
||||
this.moveable?.dragStart(event);
|
||||
@ -97,11 +105,10 @@ export default class StageDragResize extends EventEmitter {
|
||||
/**
|
||||
* 初始化选中框并渲染出来
|
||||
*/
|
||||
public async refresh() {
|
||||
public refresh() {
|
||||
if (!this.moveable) throw new Error('未初始化moveable');
|
||||
|
||||
const options = await this.getOptions();
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
Object.entries(this.moveableOptions).forEach(([key, value]) => {
|
||||
(this.moveable as any)[key] = value;
|
||||
});
|
||||
this.moveable.updateTarget();
|
||||
@ -110,16 +117,20 @@ export default class StageDragResize extends EventEmitter {
|
||||
public setGuidelines(type: GuidesType, guidelines: number[]): void {
|
||||
if (type === GuidesType.HORIZONTAL) {
|
||||
this.horizontalGuidelines = guidelines;
|
||||
this.moveableOptions.horizontalGuidelines = guidelines;
|
||||
} else if (type === GuidesType.VERTICAL) {
|
||||
this.verticalGuidelines = guidelines;
|
||||
this.moveableOptions.verticalGuidelines = guidelines;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public clearGuides() {
|
||||
this.verticalGuidelines = [];
|
||||
this.horizontalGuidelines = [];
|
||||
this.verticalGuidelines = [];
|
||||
this.moveableOptions.horizontalGuidelines = [];
|
||||
this.moveableOptions.verticalGuidelines = [];
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@ -196,10 +207,7 @@ export default class StageDragResize extends EventEmitter {
|
||||
private bindDragEvent(): void {
|
||||
if (!this.moveable) throw new Error('moveable 为初始化');
|
||||
|
||||
let offset = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
let offset: Offset | null = null;
|
||||
|
||||
this.moveable
|
||||
.on('dragStart', (e) => {
|
||||
@ -209,14 +217,17 @@ export default class StageDragResize extends EventEmitter {
|
||||
|
||||
this.moveableHelper?.onDragStart(e);
|
||||
|
||||
offset = getAbsolutePosition(this.target, { left: 0, top: 0 });
|
||||
|
||||
if (this.mode === Mode.SORTABLE) {
|
||||
this.ghostEl = this.generateGhostEl(this.target);
|
||||
}
|
||||
})
|
||||
.on('drag', (e) => {
|
||||
if (!this.target || !this.dragEl) return;
|
||||
|
||||
if (!offset) {
|
||||
offset = getAbsolutePosition(this.target, { left: 0, top: 0 });
|
||||
}
|
||||
|
||||
this.dragStatus = ActionStatus.ING;
|
||||
|
||||
const { left, top } = e;
|
||||
@ -244,24 +255,23 @@ export default class StageDragResize extends EventEmitter {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
offset = null;
|
||||
|
||||
this.dragStatus = ActionStatus.END;
|
||||
this.destroyGhostEl();
|
||||
});
|
||||
}
|
||||
|
||||
private async getSnapElements(el: HTMLElement): Promise<HTMLElement[]> {
|
||||
const { renderer } = this.core;
|
||||
private getSnapElements(runtime: Runtime, el?: HTMLElement): HTMLElement[] {
|
||||
const { renderer, mask } = this.core;
|
||||
const getSnapElements =
|
||||
(await renderer.getRuntime())?.getSnapElements ||
|
||||
runtime?.getSnapElements ||
|
||||
(() => {
|
||||
const doc = renderer.contentWindow?.document;
|
||||
return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
|
||||
});
|
||||
return (
|
||||
getSnapElements(el)
|
||||
// 排除掉当前组件本身
|
||||
.filter((element) => element !== this.target && !this.target?.contains(element))
|
||||
return getSnapElements(el).filter(
|
||||
(element) => element !== this.target && !this.target?.contains(element) && element !== mask.page,
|
||||
);
|
||||
}
|
||||
|
||||
@ -307,7 +317,7 @@ export default class StageDragResize extends EventEmitter {
|
||||
|
||||
const ghostEl = el.cloneNode(true) as HTMLElement;
|
||||
const { top, left } = getAbsolutePosition(el, getOffset(el));
|
||||
ghostEl.id = `${GHOST_EL_ID_PREFIX}${ghostEl.id}`;
|
||||
ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
|
||||
ghostEl.style.zIndex = '5';
|
||||
ghostEl.style.opacity = '.5';
|
||||
ghostEl.style.position = 'absolute';
|
||||
@ -322,23 +332,24 @@ export default class StageDragResize extends EventEmitter {
|
||||
this.ghostEl = undefined;
|
||||
}
|
||||
|
||||
private generateDragEl(el: HTMLElement): HTMLElement {
|
||||
if (this.dragEl) {
|
||||
this.destroyDragEl();
|
||||
}
|
||||
|
||||
private generateDragEl(el: HTMLElement) {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
const offset = getOffset(el);
|
||||
const dragEl = globalThis.document.createElement('div');
|
||||
dragEl.style.cssText = `
|
||||
|
||||
if (!this.dragEl) {
|
||||
this.dragEl = globalThis.document.createElement('div');
|
||||
this.container.append(this.dragEl);
|
||||
}
|
||||
|
||||
this.dragEl.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${offset.left}px;
|
||||
top: ${offset.top}px;
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
`;
|
||||
this.container.append(dragEl);
|
||||
return dragEl;
|
||||
|
||||
this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
|
||||
}
|
||||
|
||||
private destroyDragEl(): void {
|
||||
@ -346,7 +357,7 @@ export default class StageDragResize extends EventEmitter {
|
||||
this.dragEl = undefined;
|
||||
}
|
||||
|
||||
private async getOptions(options: MoveableOptions = {}): Promise<MoveableOptions> {
|
||||
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
||||
if (!this.target) return {};
|
||||
|
||||
const isAbsolute = this.mode === Mode.ABSOLUTE;
|
||||
@ -378,8 +389,16 @@ export default class StageDragResize extends EventEmitter {
|
||||
center: isAbsolute,
|
||||
middle: isAbsolute,
|
||||
},
|
||||
elementSnapDirections: {
|
||||
top: isAbsolute,
|
||||
right: isAbsolute,
|
||||
bottom: isAbsolute,
|
||||
left: isAbsolute,
|
||||
},
|
||||
isDisplayInnerSnapDigit: true,
|
||||
horizontalGuidelines: this.horizontalGuidelines,
|
||||
verticalGuidelines: this.verticalGuidelines,
|
||||
elementGuidelines: this.elementGuidelines,
|
||||
|
||||
bounds: {
|
||||
top: 0,
|
||||
|
@ -225,12 +225,13 @@ export default class StageMask extends Rule {
|
||||
return;
|
||||
}
|
||||
|
||||
this.content.removeEventListener('mousemove', this.highlightHandler);
|
||||
this.emit('clearHighlight');
|
||||
|
||||
this.emit('beforeSelect', event);
|
||||
|
||||
// 如果是右键点击,这里的mouseup事件监听没有效果
|
||||
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
||||
this.content.removeEventListener('mousemove', this.highlightHandler);
|
||||
this.emit('clearHighlight');
|
||||
};
|
||||
|
||||
private mouseUpHandler = (): void => {
|
||||
|
@ -19,6 +19,8 @@
|
||||
// 流式布局下拖动时需要clone一个镜像节点,镜像节点的id前缀
|
||||
export const GHOST_EL_ID_PREFIX = 'ghost_el_';
|
||||
|
||||
export const DRAG_EL_ID_PREFIX = 'drag_el_';
|
||||
|
||||
// 默认放到缩小倍数
|
||||
export const DEFAULT_ZOOM = 1;
|
||||
|
||||
|
@ -94,7 +94,7 @@ export interface RemoveData {
|
||||
|
||||
export interface Runtime {
|
||||
beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
|
||||
getSnapElements?: (el: HTMLElement) => HTMLElement[];
|
||||
getSnapElements?: (el?: HTMLElement) => HTMLElement[];
|
||||
updateRootConfig: (config: MApp) => void;
|
||||
updatePageId?: (id: Id) => void;
|
||||
select?: (id: Id) => Promise<HTMLElement> | HTMLElement;
|
||||
|
Loading…
x
Reference in New Issue
Block a user