diff --git a/packages/editor/src/layouts/sidebar/LayerPanel.vue b/packages/editor/src/layouts/sidebar/LayerPanel.vue index a7c0ec8f..4f294b90 100644 --- a/packages/editor/src/layouts/sidebar/LayerPanel.vue +++ b/packages/editor/src/layouts/sidebar/LayerPanel.vue @@ -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('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('stage')?.highlight(data.id); }; const useDrop = (tree: Ref | undefined>, editorService?: EditorService) => ({ diff --git a/packages/editor/src/layouts/workspace/Stage.vue b/packages/editor/src/layouts/workspace/Stage.vue index 23b37b8a..c4c0b3a4 100644 --- a/packages/editor/src/layouts/workspace/Stage.vue +++ b/packages/editor/src/layouts/workspace/Stage.vue @@ -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('page')); const zoom = computed(() => services?.uiService.get('zoom')); const node = computed(() => services?.editorService.get('node')); - const highlightNode = computed(() => services?.editorService.get('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', { diff --git a/packages/stage/src/StageCore.ts b/packages/stage/src/StageCore.ts index f7b5f30c..646b8a50 100644 --- a/packages/stage/src/StageCore.ts +++ b/packages/stage/src/StageCore.ts @@ -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 { - 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; } } diff --git a/packages/stage/src/StageDragResize.ts b/packages/stage/src/StageDragResize.ts index e327f2b5..19e3a584 100644 --- a/packages/stage/src/StageDragResize.ts +++ b/packages/stage/src/StageDragResize.ts @@ -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 { + 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 { - 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 { + 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, diff --git a/packages/stage/src/StageMask.ts b/packages/stage/src/StageMask.ts index 17f47311..3ab4fb3c 100644 --- a/packages/stage/src/StageMask.ts +++ b/packages/stage/src/StageMask.ts @@ -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 => { diff --git a/packages/stage/src/const.ts b/packages/stage/src/const.ts index 40d742a5..727580cb 100644 --- a/packages/stage/src/const.ts +++ b/packages/stage/src/const.ts @@ -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; diff --git a/packages/stage/src/types.ts b/packages/stage/src/types.ts index 252641b6..9a871d04 100644 --- a/packages/stage/src/types.ts +++ b/packages/stage/src/types.ts @@ -94,7 +94,7 @@ export interface RemoveData { export interface Runtime { beforeSelect?: (el: HTMLElement) => Promise | boolean; - getSnapElements?: (el: HTMLElement) => HTMLElement[]; + getSnapElements?: (el?: HTMLElement) => HTMLElement[]; updateRootConfig: (config: MApp) => void; updatePageId?: (id: Id) => void; select?: (id: Id) => Promise | HTMLElement;