From fe520bf600b9bed1f674655d27f1bf2312f886df Mon Sep 17 00:00:00 2001 From: khuntoriia <120667115@qq.com> Date: Thu, 21 Jul 2022 15:15:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87=E6=8C=89?= =?UTF-8?q?=E4=BD=8Fshift=E9=94=AE=E8=BF=9B=E8=A1=8C=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E7=9A=84=E8=83=BD=E5=8A=9B=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(stage): 支持绝对定位,固定定位,组内元素按住shift键进行多选拖拽能力 * refactor: 更新pnpm-lock.yaml * feat(stage): 使用moveable.helper接管moveable target的更新,针对弹窗场景引入业务方方法进行校准 * feat(stage): 将多选逻辑封装到StageMultiDragResize * fix(stage): cr意见修改 Co-authored-by: parisma --- packages/editor/src/services/editor.ts | 2 - packages/stage/package.json | 1 + packages/stage/src/StageCore.ts | 74 +++++++--- packages/stage/src/StageDragResize.ts | 63 ++++----- packages/stage/src/StageMask.ts | 29 +++- packages/stage/src/StageMultiDragResize.ts | 156 +++++++++++++++++++++ packages/stage/src/const.ts | 2 + packages/stage/src/types.ts | 1 + packages/stage/src/util.ts | 17 ++- pnpm-lock.yaml | 6 +- 10 files changed, 291 insertions(+), 60 deletions(-) create mode 100644 packages/stage/src/StageMultiDragResize.ts diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index 5814fd0f..0273716b 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -40,7 +40,6 @@ import { isFixed, setLayout, } from '@editor/utils/editor'; -import { log } from '@editor/utils/logger'; import BaseService from './BaseService'; @@ -90,7 +89,6 @@ class Editor extends BaseService { */ public set(name: keyof StoreState, value: T) { this.state[name] = value as any; - log('store set ', name, ' ', value); if (name === 'root') { this.state.pageLength = (value as unknown as MApp)?.items?.length || 0; diff --git a/packages/stage/package.json b/packages/stage/package.json index 5e07fd6f..13652f8f 100644 --- a/packages/stage/package.json +++ b/packages/stage/package.json @@ -31,6 +31,7 @@ "@tmagic/schema": "1.1.0-beta.2", "@tmagic/utils": "1.1.0-beta.2", "events": "^3.3.0", + "keycon": "^1.1.2", "lodash-es": "^4.17.21", "moveable": "^0.30.0", "moveable-helper": "^0.4.0" diff --git a/packages/stage/src/StageCore.ts b/packages/stage/src/StageCore.ts index 75315098..20cee786 100644 --- a/packages/stage/src/StageCore.ts +++ b/packages/stage/src/StageCore.ts @@ -20,10 +20,11 @@ import { EventEmitter } from 'events'; 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 StageHighlight from './StageHighlight'; import StageMask from './StageMask'; +import StageMultiDragResize from './StageMultiDragResize'; import StageRender from './StageRender'; import { CanSelect, @@ -40,12 +41,15 @@ import { addSelectedClassName, removeSelectedClassName } from './util'; export default class StageCore extends EventEmitter { public container?: HTMLDivElement; - - public selectedDom: Element | undefined; + // 当前选中的节点 + public selectedDom: HTMLElement | undefined; + // 多选选中的节点组 + public selectedDomList: HTMLElement[] = []; public highlightedDom: Element | undefined; public renderer: StageRender; public mask: StageMask; public dr: StageDragResize; + public multiDr: StageMultiDragResize; public highlightLayer: StageHighlight; public config: StageCoreConfig; public zoom = DEFAULT_ZOOM; @@ -68,7 +72,8 @@ export default class StageCore extends EventEmitter { this.renderer = new StageRender({ 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.renderer.on('runtime-ready', (runtime: Runtime) => { @@ -79,8 +84,11 @@ export default class StageCore extends EventEmitter { }); this.mask - .on('beforeSelect', (event: MouseEvent) => { - this.setElementFromPoint(event); + .on('beforeSelect', async (event: MouseEvent) => { + this.clearSelectStatus('multiSelect'); + const el = await this.setElementFromPoint(event); + if (!el) return; + this.select(el, event); }) .on('select', () => { this.emit('select', this.selectedDom); @@ -90,16 +98,39 @@ export default class StageCore extends EventEmitter { this.emit('changeGuides', data); }) .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) { this.highlightLayer.clearHighlight(); return; } - this.highlightLayer.highlight(this.highlightedDom as HTMLElement); this.emit('highlight', this.highlightedDom); }) .on('clearHighlight', async () => { 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 @@ -130,20 +161,17 @@ export default class StageCore extends EventEmitter { 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); - let stopped = false; const stop = () => (stopped = true); for (const el of els) { if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, event, stop))) { if (stopped) break; - if (event.type === 'mousemove') { - this.highlight(el); - break; + if (event.type === type) { + return el; } - this.select(el, event); - break; + return el; } } } @@ -166,6 +194,7 @@ export default class StageCore extends EventEmitter { } this.mask.setLayout(el); + this.multiDr.destroyDragElList(); this.dr.select(el, event); if (this.config.autoScrollIntoView || el.dataset.autoScrollIntoView) { @@ -217,7 +246,7 @@ export default class StageCore extends EventEmitter { this.highlightLayer.clearHighlight(); return; } - if (el === this.highlightedDom) return; + if (el === this.highlightedDom || !el) return; this.highlightLayer.highlight(el); this.highlightedDom = el; } @@ -238,6 +267,19 @@ export default class StageCore extends EventEmitter { this.zoom = zoom; } + /** + * 用于在切换选择模式时清除上一次的状态 + * @param selectType 需要清理的选择模式 多选:multiSelect,单选:select + */ + public clearSelectStatus(selectType: String) { + if (selectType === 'multiSelect') { + this.multiDr.clearSelectStatus(); + this.selectedDomList = []; + } else { + this.dr.clearSelectStatus(); + } + } + /** * 挂载Dom节点 * @param el 将stage挂载到该Dom节点上 diff --git a/packages/stage/src/StageDragResize.ts b/packages/stage/src/StageDragResize.ts index d54a3d2b..4bcb02e0 100644 --- a/packages/stage/src/StageDragResize.ts +++ b/packages/stage/src/StageDragResize.ts @@ -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 StageCore from './StageCore'; +import StageMask from './StageMask'; import type { SortEventData, StageDragResizeConfig } from './types'; -import { calcValueByFontsize, getAbsolutePosition, getMode, getOffset } from './util'; +import { calcValueByFontsize, getAbsolutePosition, getMode, getOffset, getTargetElStyle } from './util'; /** 拖动状态 */ enum ActionStatus { @@ -45,14 +46,21 @@ enum ActionStatus { */ export default class StageDragResize extends EventEmitter { public core: StageCore; + public mask: StageMask; /** 画布容器 */ public container: HTMLElement; /** 目标节点 */ public target?: HTMLElement; /** 目标节点在蒙层中的占位节点 */ - public dragEl: HTMLDivElement; + public dragEl?: HTMLDivElement; + /** 多选:目标节点组 */ + public targetList: HTMLElement[] = []; + /** 多选:目标节点在蒙层中的占位节点组 */ + public dragElList: HTMLDivElement[] = []; /** Moveable拖拽类实例 */ public moveable?: Moveable; + /** Moveable多选拖拽类实例 */ + public moveableForMulti?: Moveable; /** 水平参考线 */ public horizontalGuidelines: number[] = []; /** 垂直参考线 */ @@ -74,9 +82,7 @@ export default class StageDragResize extends EventEmitter { this.core = config.core; this.container = config.container; - - this.dragEl = globalThis.document.createElement('div'); - this.container.append(this.dragEl); + this.mask = config.mask; } /** @@ -147,6 +153,17 @@ export default class StageDragResize extends EventEmitter { 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.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({ target: this.dragEl, }); @@ -282,7 +305,6 @@ export default class StageDragResize extends EventEmitter { if (this.mode === Mode.SORTABLE) { this.ghostEl = this.generateGhostEl(this.target); } - frame.top = this.target.offsetTop; frame.left = this.target.offsetLeft; }) @@ -483,31 +505,6 @@ export default class StageDragResize extends EventEmitter { 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 { if (!this.target) return {}; diff --git a/packages/stage/src/StageMask.ts b/packages/stage/src/StageMask.ts index 74add247..88b7dfee 100644 --- a/packages/stage/src/StageMask.ts +++ b/packages/stage/src/StageMask.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +import KeyController from 'keycon'; import { throttle } from 'lodash-es'; import { createDiv, injectStyle } from '@tmagic/utils'; @@ -82,6 +83,7 @@ export default class StageMask extends Rule { public maxScrollTop = 0; public maxScrollLeft = 0; public intersectionObserver: IntersectionObserver | null = null; + public shiftKeyDown: Boolean = false; private mode: Mode = Mode.ABSOLUTE; private pageResizeObserver: ResizeObserver | null = null; @@ -106,6 +108,14 @@ export default class StageMask extends Rule { this.content.addEventListener('wheel', this.mouseWheelHandler); this.content.addEventListener('mousemove', this.highlightHandler); 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) { @@ -292,23 +302,30 @@ export default class StageMask extends Rule { */ private mouseDownHandler = (event: MouseEvent): void => { this.emit('clearHighlight'); - event.stopImmediatePropagation(); event.stopPropagation(); 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) { return; } this.content.removeEventListener('mousemove', this.highlightHandler); - this.emit('beforeSelect', event); - - // 如果是右键点击,这里的mouseup事件监听没有效果 - globalThis.document.addEventListener('mouseup', this.mouseUpHandler); + // 判断触发多选还是单选 + if (this.shiftKeyDown) { + this.emit('beforeMultiSelect', event); + } else { + this.emit('beforeSelect', event); + // 如果是右键点击,这里的mouseup事件监听没有效果 + globalThis.document.addEventListener('mouseup', this.mouseUpHandler); + } }; private mouseUpHandler = (): void => { diff --git a/packages/stage/src/StageMultiDragResize.ts b/packages/stage/src/StageMultiDragResize.ts new file mode 100644 index 00000000..ae93ab94 --- /dev/null +++ b/packages/stage/src/StageMultiDragResize.ts @@ -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()); + } +} diff --git a/packages/stage/src/const.ts b/packages/stage/src/const.ts index 027e7fb4..fc9c08c8 100644 --- a/packages/stage/src/const.ts +++ b/packages/stage/src/const.ts @@ -27,6 +27,8 @@ export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_'; export const CONTAINER_HIGHLIGHT_CLASS = 'tmagic-stage-container-highlight'; +export const PAGE_CLASS = 'magic-ui-page'; + /** 默认放到缩小倍数 */ export const DEFAULT_ZOOM = 1; diff --git a/packages/stage/src/types.ts b/packages/stage/src/types.ts index a8d21890..a73891e7 100644 --- a/packages/stage/src/types.ts +++ b/packages/stage/src/types.ts @@ -57,6 +57,7 @@ export interface StageMaskConfig { export interface StageDragResizeConfig { core: StageCore; container: HTMLElement; + mask: StageMask; } export type Rect = { diff --git a/packages/stage/src/util.ts b/packages/stage/src/util.ts index f617550c..3f9005e2 100644 --- a/packages/stage/src/util.ts +++ b/packages/stage/src/util.ts @@ -17,7 +17,7 @@ */ import { removeClassName } from '@tmagic/utils'; -import { Mode, SELECTED_CLASS } from './const'; +import { Mode, SELECTED_CLASS, ZIndex } from './const'; import type { Offset } from './types'; 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) => { const { offsetParent } = el; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a6cf9ab..c9339f1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,6 +254,7 @@ importers: '@types/lodash-es': ^4.17.4 '@types/node': ^15.12.4 events: ^3.3.0 + keycon: ^1.1.2 lodash-es: ^4.17.21 moveable: ^0.30.0 moveable-helper: ^0.4.0 @@ -267,6 +268,7 @@ importers: '@tmagic/schema': link:../schema '@tmagic/utils': link:../utils events: 3.3.0 + keycon: 1.1.2 lodash-es: 4.17.21 moveable: 0.30.0 moveable-helper: 0.4.0 @@ -3075,7 +3077,7 @@ packages: lodash: ^4.17.20 marko: ^3.14.4 mote: ^0.2.0 - mustache: ^3.0.0 + mustache: ^4.0.1 nunjucks: ^3.2.2 plates: ~0.4.11 pug: ^3.0.0 @@ -5519,7 +5521,7 @@ packages: /keycon/1.1.2: resolution: {integrity: sha512-yCoUAfwqmQUWrtOFuZhicxasF/4ae+M0aH8yV1wEKKZCZql8v6jWhlVF9dT5i1TfuHSmgt/GNuCaWIHT8wk6eQ==} dependencies: - '@daybrush/utils': 1.6.0 + '@daybrush/utils': 1.7.1 '@scena/event-emitter': 1.0.5 keycode: 2.2.1 dev: false