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 { 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) => ({

View File

@ -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(() => {
// domselect
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', {

View File

@ -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;
}
}

View File

@ -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时获取到widthheight不准确
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,

View File

@ -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 => {

View File

@ -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;

View File

@ -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;