feat: 优化拖拽体验

This commit is contained in:
roymondchen 2022-04-01 21:19:40 +08:00 committed by khuntoriia
parent be4df0fc9b
commit a842c5b0ce
7 changed files with 84 additions and 76 deletions

View File

@ -59,6 +59,7 @@ import { throttle } from 'lodash-es';
import type { MNode, MPage } from '@tmagic/schema'; import type { MNode, MPage } from '@tmagic/schema';
import { NodeType } from '@tmagic/schema'; import { NodeType } from '@tmagic/schema';
import StageCore from '@tmagic/stage';
import type { EditorService } from '@editor/services/editor'; import type { EditorService } from '@editor/services/editor';
import type { Services } from '@editor/type'; import type { Services } from '@editor/type';
@ -73,6 +74,7 @@ const select = (data: MNode, editorService?: EditorService) => {
} }
editorService?.select(data); editorService?.select(data);
editorService?.get<StageCore>('stage')?.select(data.id);
}; };
const highlight = (data: MNode, editorService?: EditorService) => { const highlight = (data: MNode, editorService?: EditorService) => {
@ -80,6 +82,7 @@ const highlight = (data: MNode, editorService?: EditorService) => {
throw new Error('没有id'); throw new Error('没有id');
} }
editorService?.highlight(data); editorService?.highlight(data);
editorService?.get<StageCore>('stage')?.highlight(data.id);
}; };
const useDrop = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => ({ const useDrop = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => ({

View File

@ -23,7 +23,7 @@ import {
computed, computed,
defineComponent, defineComponent,
inject, inject,
nextTick, markRaw,
onMounted, onMounted,
onUnmounted, onUnmounted,
PropType, PropType,
@ -124,7 +124,6 @@ export default defineComponent({
const page = computed(() => services?.editorService.get<MPage>('page')); const page = computed(() => services?.editorService.get<MPage>('page'));
const zoom = computed(() => services?.uiService.get<number>('zoom')); const zoom = computed(() => services?.uiService.get<number>('zoom'));
const node = computed(() => services?.editorService.get<MNode>('node')); const node = computed(() => services?.editorService.get<MNode>('node'));
const highlightNode = computed(() => services?.editorService.get<MNode>('highlightNode'));
let stage: StageCore | null = null; let stage: StageCore | null = null;
let runtime: Runtime | null = null; let runtime: Runtime | null = null;
@ -152,7 +151,7 @@ export default defineComponent({
moveableOptions: props.moveableOptions, moveableOptions: props.moveableOptions,
}); });
services?.editorService.set('stage', stage); services?.editorService.set('stage', markRaw(stage));
stage?.mount(stageContainer.value); stage?.mount(stageContainer.value);
@ -199,25 +198,6 @@ export default defineComponent({
} }
}); });
watch(
() => node.value?.id,
(id) => {
nextTick(() => {
// domselect
id && stage?.select(id);
});
},
);
watch(
() => highlightNode.value?.id,
(id) => {
nextTick(() => {
id && stage?.highlight(id);
});
},
);
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (const { contentRect } of entries) { for (const { contentRect } of entries) {
services?.uiService.set('stageContainerRect', { services?.uiService.set('stageContainerRect', {

View File

@ -63,8 +63,12 @@ export default class StageCore extends EventEmitter {
this.dr = new StageDragResize({ core: this, container: this.mask.content }); this.dr = new StageDragResize({ core: this, container: this.mask.content });
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.emit('runtime-ready', runtime)); this.renderer.on('runtime-ready', (runtime: Runtime) => {
this.renderer.on('page-el-update', (el: HTMLElement) => this.mask?.observe(el)); this.emit('runtime-ready', runtime);
});
this.renderer.on('page-el-update', (el: HTMLElement) => {
this.mask?.observe(el);
});
this.mask this.mask
.on('beforeSelect', (event: MouseEvent) => { .on('beforeSelect', (event: MouseEvent) => {
@ -139,12 +143,14 @@ export default class StageCore extends EventEmitter {
const runtime = await this.renderer.getRuntime(); const runtime = await this.renderer.getRuntime();
await runtime?.select?.(el.id);
if (runtime?.beforeSelect) { if (runtime?.beforeSelect) {
await runtime.beforeSelect(el); await runtime.beforeSelect(el);
} }
this.mask.setLayout(el); this.mask.setLayout(el);
this.dr?.select(el, event); this.dr.select(el, event);
this.selectedDom = el; this.selectedDom = el;
if (this.renderer.contentWindow) { if (this.renderer.contentWindow) {
@ -170,7 +176,7 @@ export default class StageCore extends EventEmitter {
if (el) { if (el) {
// 更新了组件的布局需要重新设置mask是否可以滚动 // 更新了组件的布局需要重新设置mask是否可以滚动
this.mask.setLayout(el); this.mask.setLayout(el);
this.dr?.select(el); this.dr.select(el);
} }
}, 0); }, 0);
}); });
@ -242,14 +248,11 @@ export default class StageCore extends EventEmitter {
} }
private async getTargetElement(idOrEl: Id | HTMLElement): Promise<HTMLElement> { private async getTargetElement(idOrEl: Id | HTMLElement): Promise<HTMLElement> {
let el;
if (typeof idOrEl === 'string' || typeof idOrEl === 'number') { if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
const runtime = await this.renderer?.getRuntime(); const el = this.renderer.contentWindow?.document.getElementById(`${idOrEl}`);
el = await runtime?.select?.(`${idOrEl}`);
if (!el) throw new Error(`不存在ID为${idOrEl}的元素`); if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
} else { return el;
el = idOrEl;
} }
return el; return idOrEl;
} }
} }

View File

@ -23,9 +23,9 @@ import type { MoveableOptions } from 'moveable';
import Moveable from 'moveable'; import Moveable from 'moveable';
import MoveableHelper from 'moveable-helper'; 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 StageCore from './StageCore';
import type { SortEventData, StageDragResizeConfig } from './types'; import type { Offset, Runtime, SortEventData, StageDragResizeConfig } from './types';
import { getAbsolutePosition, getMode, getOffset } from './util'; import { getAbsolutePosition, getMode, getOffset } from './util';
enum ActionStatus { enum ActionStatus {
@ -45,6 +45,7 @@ export default class StageDragResize extends EventEmitter {
public moveable?: Moveable; public moveable?: Moveable;
public horizontalGuidelines: number[] = []; public horizontalGuidelines: number[] = [];
public verticalGuidelines: number[] = []; public verticalGuidelines: number[] = [];
public elementGuidelines: HTMLElement[] = [];
private moveableOptions: MoveableOptions = {}; private moveableOptions: MoveableOptions = {};
private dragStatus: ActionStatus = ActionStatus.END; private dragStatus: ActionStatus = ActionStatus.END;
@ -65,29 +66,36 @@ export default class StageDragResize extends EventEmitter {
* @param el Dom节点元素 * @param el Dom节点元素
* @param event * @param event
*/ */
public async select(el: HTMLElement, event?: MouseEvent): Promise<void> { public select(el: HTMLElement, event?: MouseEvent): void {
this.target = el; this.target = el;
// 如果有滚动条会导致resize时获取到widthheight不准确 // 如果有滚动条会导致resize时获取到widthheight不准确
if (/(auto|scroll)/.test(this.target.style.overflow)) { if (/(auto|scroll)/.test(this.target.style.overflow)) {
this.target.style.overflow = 'hidden'; this.target.style.overflow = 'hidden';
} }
this.mode = getMode(el); this.mode = getMode(el);
this.destroyDragEl();
this.destroyGhostEl(); this.destroyGhostEl();
this.dragEl = this.generateDragEl(el); this.generateDragEl(el);
this.moveableOptions = await this.getOptions({ const originDraggable = this.moveableOptions.draggable;
target: this.dragEl || this.target,
this.moveableOptions = this.getOptions({
target: this.dragEl,
}); });
this.moveableHelper = MoveableHelper.create({ // 从不能拖动到能拖动的节点之间切换要重新创建moveable不然dragStart不生效
useBeforeRender: true, if (!this.moveable || originDraggable !== this.moveableOptions.draggable) {
useRender: false, this.moveableHelper = MoveableHelper.create({
createAuto: true, useBeforeRender: true,
}); useRender: false,
createAuto: true,
});
this.initMoveable(); this.initMoveable();
} else {
this.refresh();
}
if (event) { if (event) {
this.moveable?.dragStart(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'); if (!this.moveable) throw new Error('未初始化moveable');
const options = await this.getOptions(); Object.entries(this.moveableOptions).forEach(([key, value]) => {
Object.entries(options).forEach(([key, value]) => {
(this.moveable as any)[key] = value; (this.moveable as any)[key] = value;
}); });
this.moveable.updateTarget(); this.moveable.updateTarget();
@ -110,16 +117,20 @@ export default class StageDragResize extends EventEmitter {
public setGuidelines(type: GuidesType, guidelines: number[]): void { public setGuidelines(type: GuidesType, guidelines: number[]): void {
if (type === GuidesType.HORIZONTAL) { if (type === GuidesType.HORIZONTAL) {
this.horizontalGuidelines = guidelines; this.horizontalGuidelines = guidelines;
this.moveableOptions.horizontalGuidelines = guidelines;
} else if (type === GuidesType.VERTICAL) { } else if (type === GuidesType.VERTICAL) {
this.verticalGuidelines = guidelines; this.verticalGuidelines = guidelines;
this.moveableOptions.verticalGuidelines = guidelines;
} }
this.refresh(); this.refresh();
} }
public clearGuides() { public clearGuides() {
this.verticalGuidelines = [];
this.horizontalGuidelines = []; this.horizontalGuidelines = [];
this.verticalGuidelines = [];
this.moveableOptions.horizontalGuidelines = [];
this.moveableOptions.verticalGuidelines = [];
this.refresh(); this.refresh();
} }
@ -196,10 +207,7 @@ export default class StageDragResize extends EventEmitter {
private bindDragEvent(): void { private bindDragEvent(): void {
if (!this.moveable) throw new Error('moveable 为初始化'); if (!this.moveable) throw new Error('moveable 为初始化');
let offset = { let offset: Offset | null = null;
left: 0,
top: 0,
};
this.moveable this.moveable
.on('dragStart', (e) => { .on('dragStart', (e) => {
@ -209,14 +217,17 @@ export default class StageDragResize extends EventEmitter {
this.moveableHelper?.onDragStart(e); this.moveableHelper?.onDragStart(e);
offset = getAbsolutePosition(this.target, { left: 0, top: 0 });
if (this.mode === Mode.SORTABLE) { if (this.mode === Mode.SORTABLE) {
this.ghostEl = this.generateGhostEl(this.target); this.ghostEl = this.generateGhostEl(this.target);
} }
}) })
.on('drag', (e) => { .on('drag', (e) => {
if (!this.target || !this.dragEl) return; if (!this.target || !this.dragEl) return;
if (!offset) {
offset = getAbsolutePosition(this.target, { left: 0, top: 0 });
}
this.dragStatus = ActionStatus.ING; this.dragStatus = ActionStatus.ING;
const { left, top } = e; const { left, top } = e;
@ -244,24 +255,23 @@ export default class StageDragResize extends EventEmitter {
this.update(); this.update();
} }
} }
offset = null;
this.dragStatus = ActionStatus.END; this.dragStatus = ActionStatus.END;
this.destroyGhostEl(); this.destroyGhostEl();
}); });
} }
private async getSnapElements(el: HTMLElement): Promise<HTMLElement[]> { private getSnapElements(runtime: Runtime, el?: HTMLElement): HTMLElement[] {
const { renderer } = this.core; const { renderer, mask } = this.core;
const getSnapElements = const getSnapElements =
(await renderer.getRuntime())?.getSnapElements || runtime?.getSnapElements ||
(() => { (() => {
const doc = renderer.contentWindow?.document; const doc = renderer.contentWindow?.document;
return doc ? Array.from(doc.querySelectorAll('[id]')) : []; return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
}); });
return ( return getSnapElements(el).filter(
getSnapElements(el) (element) => element !== this.target && !this.target?.contains(element) && element !== mask.page,
// 排除掉当前组件本身
.filter((element) => element !== this.target && !this.target?.contains(element))
); );
} }
@ -307,7 +317,7 @@ export default class StageDragResize extends EventEmitter {
const ghostEl = el.cloneNode(true) as HTMLElement; const ghostEl = el.cloneNode(true) as HTMLElement;
const { top, left } = getAbsolutePosition(el, getOffset(el)); 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.zIndex = '5';
ghostEl.style.opacity = '.5'; ghostEl.style.opacity = '.5';
ghostEl.style.position = 'absolute'; ghostEl.style.position = 'absolute';
@ -322,23 +332,24 @@ export default class StageDragResize extends EventEmitter {
this.ghostEl = undefined; this.ghostEl = undefined;
} }
private generateDragEl(el: HTMLElement): HTMLElement { private generateDragEl(el: HTMLElement) {
if (this.dragEl) {
this.destroyDragEl();
}
const { width, height } = el.getBoundingClientRect(); const { width, height } = el.getBoundingClientRect();
const offset = getOffset(el); 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; position: absolute;
left: ${offset.left}px; left: ${offset.left}px;
top: ${offset.top}px; top: ${offset.top}px;
width: ${width}px; width: ${width}px;
height: ${height}px; height: ${height}px;
`; `;
this.container.append(dragEl);
return dragEl; this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
} }
private destroyDragEl(): void { private destroyDragEl(): void {
@ -346,7 +357,7 @@ export default class StageDragResize extends EventEmitter {
this.dragEl = undefined; this.dragEl = undefined;
} }
private async getOptions(options: MoveableOptions = {}): Promise<MoveableOptions> { private getOptions(options: MoveableOptions = {}): MoveableOptions {
if (!this.target) return {}; if (!this.target) return {};
const isAbsolute = this.mode === Mode.ABSOLUTE; const isAbsolute = this.mode === Mode.ABSOLUTE;
@ -378,8 +389,16 @@ export default class StageDragResize extends EventEmitter {
center: isAbsolute, center: isAbsolute,
middle: isAbsolute, middle: isAbsolute,
}, },
elementSnapDirections: {
top: isAbsolute,
right: isAbsolute,
bottom: isAbsolute,
left: isAbsolute,
},
isDisplayInnerSnapDigit: true,
horizontalGuidelines: this.horizontalGuidelines, horizontalGuidelines: this.horizontalGuidelines,
verticalGuidelines: this.verticalGuidelines, verticalGuidelines: this.verticalGuidelines,
elementGuidelines: this.elementGuidelines,
bounds: { bounds: {
top: 0, top: 0,

View File

@ -225,12 +225,13 @@ export default class StageMask extends Rule {
return; return;
} }
this.content.removeEventListener('mousemove', this.highlightHandler);
this.emit('clearHighlight');
this.emit('beforeSelect', event); this.emit('beforeSelect', event);
// 如果是右键点击这里的mouseup事件监听没有效果 // 如果是右键点击这里的mouseup事件监听没有效果
globalThis.document.addEventListener('mouseup', this.mouseUpHandler); globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
this.content.removeEventListener('mousemove', this.highlightHandler);
this.emit('clearHighlight');
}; };
private mouseUpHandler = (): void => { private mouseUpHandler = (): void => {

View File

@ -19,6 +19,8 @@
// 流式布局下拖动时需要clone一个镜像节点镜像节点的id前缀 // 流式布局下拖动时需要clone一个镜像节点镜像节点的id前缀
export const GHOST_EL_ID_PREFIX = 'ghost_el_'; export const GHOST_EL_ID_PREFIX = 'ghost_el_';
export const DRAG_EL_ID_PREFIX = 'drag_el_';
// 默认放到缩小倍数 // 默认放到缩小倍数
export const DEFAULT_ZOOM = 1; export const DEFAULT_ZOOM = 1;

View File

@ -94,7 +94,7 @@ export interface RemoveData {
export interface Runtime { export interface Runtime {
beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean; beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
getSnapElements?: (el: HTMLElement) => HTMLElement[]; getSnapElements?: (el?: HTMLElement) => HTMLElement[];
updateRootConfig: (config: MApp) => void; updateRootConfig: (config: MApp) => void;
updatePageId?: (id: Id) => void; updatePageId?: (id: Id) => void;
select?: (id: Id) => Promise<HTMLElement> | HTMLElement; select?: (id: Id) => Promise<HTMLElement> | HTMLElement;