mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-08-29 18:23:29 +08:00
refactor(stage):重构魔方编辑器stage代码
Squash merge branch 'feature/oc_actionbox_879360293' into 'master' 对魔方编辑器核心代码stage进行重构,这部分代码主要是负责编辑器中间画布区域的处理,包括渲染一个所见即所得的画布,支持组件的增删改查和拖拽、高亮操作。 旧代码存在的问题以及解决方案: 1、过多暴露属性和循环引用,导致stageCore、stageDragResize、StageMultiDragResize、StageRender、StageMask、StageHighlight之间形成复杂的网状依赖,非常不可控。StageCore负责创建后面5个类的实例,并把这些实例作为自己的公共属性,同时core把自己的this传给这些实例,这些实例就会通过core传进来的this,通过core间接的访问其它实例的方法和属性,比如在stageDragResize中可能存在这样的一个访问:this.core.stageRender.contentWindow.document 解决方案: 1)、属性尽量设置为私有,对外暴露方法,不暴露属性; 2)、core避免向其它类传递this,改为传递接口,需要什么就传什么 2、事件传递较多,跳来跳去,定位问题较为困难 解决方案: 重新梳理各个类的职责,尽量在类中闭环,减少事件传递。 新增了actionManager类,core负责管理render、mask、actionManager三个类; actionManager负责管理单选、多选、高亮三个类,同时将mask中的事件监听,转移到actionManager监听,actionManager形成单选、多选、高亮行为后,直接调动单选、多选、高亮完成功能。 3、存在一些重复代码 主要是拖拽框的代码在单选、多选、高亮中各自维护,改为统一维护 4、多选不支持辅助线对齐 将单选中的moveableOption管理逻辑抽取出来成为单选和多选的父类,使多选支持辅助线对齐 本次改动取消了一些对外暴露的属性,moveableOption回调函数签名也有变化,详细情况如下: 删除stageCore公共属性: public selectedDom: HTMLElement | undefined; public selectedDomList: HTMLElement[] = []; public highlightedDom: Element | undefined; public dr: StageDragResize; public multiDr: StageMultiDragResize; public highlightLayer: StageHighlight; public config: StageCoreConfig; public zoom = DEFAULT_ZOOM; public containerHighlightClassName: string; public containerHighlightDuration: number; public containerHighlightType?: ContainerHighlightType; public isContainer: IsContainer; stageCore入参改动: 这两个参数原本定义: moveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions; multiMoveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions; 修改后定义: moveableOptions?: CustomizeMoveableOptions; multiMoveableOptions?: CustomizeMoveableOptions; CustomizeMoveableOptions = | ((config?: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions) | MoveableOptions | undefined; export interface CustomizeMoveableOptionsCallbackConfig { targetElId?: string; }
This commit is contained in:
parent
deeb55cd0b
commit
3fb880d09b
@ -292,7 +292,7 @@ icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/ico
|
|||||||
|
|
||||||
### moveableOptions
|
### moveableOptions
|
||||||
|
|
||||||
- **类型:** ((core: StageCore) => MoveableOptions) | [MoveableOptions](https://daybrush.com/moveable/release/latest/doc/)
|
- **类型:** ((config?: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions) | [MoveableOptions](https://daybrush.com/moveable/release/latest/doc/)
|
||||||
|
|
||||||
- **默认值:** {}
|
- **默认值:** {}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import { Edit, FolderOpened, SwitchButton, Tickets } from '@element-plus/icons-v
|
|||||||
import type { MoveableOptions } from '@tmagic/editor';
|
import type { MoveableOptions } from '@tmagic/editor';
|
||||||
import { ComponentGroup } from '@tmagic/editor';
|
import { ComponentGroup } from '@tmagic/editor';
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
import StageCore from '@tmagic/stage';
|
import { CustomizeMoveableOptionsCallbackConfig } from '@tmagic/stage';
|
||||||
import { asyncLoadJs } from '@tmagic/utils';
|
import { asyncLoadJs } from '@tmagic/utils';
|
||||||
|
|
||||||
import editorApi from '@src/api/editor';
|
import editorApi from '@src/api/editor';
|
||||||
@ -89,10 +89,10 @@ export default defineComponent({
|
|||||||
magicPresetConfigs,
|
magicPresetConfigs,
|
||||||
magicPresetEvents,
|
magicPresetEvents,
|
||||||
editorDefaultSelected,
|
editorDefaultSelected,
|
||||||
moveableOptions: (core?: StageCore): MoveableOptions => {
|
moveableOptions: (config?: CustomizeMoveableOptionsCallbackConfig): MoveableOptions => {
|
||||||
const options: MoveableOptions = {};
|
const options: MoveableOptions = {};
|
||||||
const id = core?.dr?.target?.id;
|
|
||||||
|
|
||||||
|
const id = config?.targetElId;
|
||||||
if (!id || !editor.value) return options;
|
if (!id || !editor.value) return options;
|
||||||
|
|
||||||
const node = editor.value.editorService.getNodeById(id);
|
const node = editor.value.editorService.getNodeById(id);
|
||||||
|
@ -22,4 +22,6 @@ import './resetcss.css';
|
|||||||
|
|
||||||
export * from './events';
|
export * from './events';
|
||||||
|
|
||||||
|
export { default as Env } from './Env';
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -69,8 +69,13 @@ import { defineComponent, onUnmounted, PropType, provide, reactive, toRaw, watch
|
|||||||
import { EventOption } from '@tmagic/core';
|
import { EventOption } from '@tmagic/core';
|
||||||
import type { FormConfig } from '@tmagic/form';
|
import type { FormConfig } from '@tmagic/form';
|
||||||
import type { MApp, MNode } from '@tmagic/schema';
|
import type { MApp, MNode } from '@tmagic/schema';
|
||||||
import type StageCore from '@tmagic/stage';
|
import {
|
||||||
import { CONTAINER_HIGHLIGHT_CLASS, ContainerHighlightType, MoveableOptions } from '@tmagic/stage';
|
CONTAINER_HIGHLIGHT_CLASS_NAME,
|
||||||
|
ContainerHighlightType,
|
||||||
|
CustomizeMoveableOptionsCallbackConfig,
|
||||||
|
MoveableOptions,
|
||||||
|
UpdateDragEl,
|
||||||
|
} from '@tmagic/stage';
|
||||||
|
|
||||||
import Framework from './layouts/Framework.vue';
|
import Framework from './layouts/Framework.vue';
|
||||||
import NavMenu from './layouts/NavMenu.vue';
|
import NavMenu from './layouts/NavMenu.vue';
|
||||||
@ -164,7 +169,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
/** 画布中组件选中框的移动范围 */
|
/** 画布中组件选中框的移动范围 */
|
||||||
moveableOptions: {
|
moveableOptions: {
|
||||||
type: [Object, Function] as PropType<MoveableOptions | ((core?: StageCore) => MoveableOptions)>,
|
type: [Object, Function] as PropType<
|
||||||
|
MoveableOptions | ((config?: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions)
|
||||||
|
>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 编辑器初始化时默认选中的组件ID */
|
/** 编辑器初始化时默认选中的组件ID */
|
||||||
@ -184,7 +191,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
containerHighlightClassName: {
|
containerHighlightClassName: {
|
||||||
type: String,
|
type: String,
|
||||||
default: CONTAINER_HIGHLIGHT_CLASS,
|
default: CONTAINER_HIGHLIGHT_CLASS_NAME,
|
||||||
},
|
},
|
||||||
|
|
||||||
containerHighlightDuration: {
|
containerHighlightDuration: {
|
||||||
@ -207,7 +214,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateDragEl: {
|
updateDragEl: {
|
||||||
type: Function as PropType<(el: HTMLDivElement, target: HTMLElement) => void>,
|
type: Function as PropType<UpdateDragEl>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -21,7 +21,12 @@ import type { Component } from 'vue';
|
|||||||
import type { FormConfig } from '@tmagic/form';
|
import type { FormConfig } from '@tmagic/form';
|
||||||
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
||||||
import type StageCore from '@tmagic/stage';
|
import type StageCore from '@tmagic/stage';
|
||||||
import type { ContainerHighlightType, MoveableOptions } from '@tmagic/stage';
|
import type {
|
||||||
|
ContainerHighlightType,
|
||||||
|
CustomizeMoveableOptionsCallbackConfig,
|
||||||
|
MoveableOptions,
|
||||||
|
UpdateDragEl,
|
||||||
|
} from '@tmagic/stage';
|
||||||
|
|
||||||
import type { CodeBlockService } from './services/codeBlock';
|
import type { CodeBlockService } from './services/codeBlock';
|
||||||
import type { ComponentListService } from './services/componentList';
|
import type { ComponentListService } from './services/componentList';
|
||||||
@ -57,10 +62,10 @@ export interface StageOptions {
|
|||||||
containerHighlightDuration: number;
|
containerHighlightDuration: number;
|
||||||
containerHighlightType: ContainerHighlightType;
|
containerHighlightType: ContainerHighlightType;
|
||||||
render: () => HTMLDivElement;
|
render: () => HTMLDivElement;
|
||||||
moveableOptions: MoveableOptions | ((core?: StageCore) => MoveableOptions);
|
moveableOptions: MoveableOptions | ((config?: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions);
|
||||||
canSelect: (el: HTMLElement) => boolean | Promise<boolean>;
|
canSelect: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
isContainer: (el: HTMLElement) => boolean | Promise<boolean>;
|
isContainer: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
updateDragEl: (el: HTMLDivElement) => void;
|
updateDragEl: UpdateDragEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
|
@ -54,7 +54,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
|||||||
editorService.highlight(el.id);
|
editorService.highlight(el.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
stage.on('multiSelect', (els: HTMLElement[]) => {
|
stage.on('multi-select', (els: HTMLElement[]) => {
|
||||||
editorService.multiSelect(els.map((el) => el.id));
|
editorService.multiSelect(els.map((el) => el.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
|||||||
editorService.get<StageCore>('stage').select(parent.id);
|
editorService.get<StageCore>('stage').select(parent.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
stage.on('changeGuides', (e) => {
|
stage.on('change-guides', (e) => {
|
||||||
uiService.set('showGuides', true);
|
uiService.set('showGuides', true);
|
||||||
|
|
||||||
if (!root.value || !page.value) return;
|
if (!root.value || !page.value) return;
|
||||||
|
@ -1 +1,57 @@
|
|||||||
# [文档](https://tencent.github.io/tmagic-editor/docs/)
|
# 画布功能介绍
|
||||||
|
画布是编辑器中最核心的功能,处理组件拖拽和所见即所得的展示。
|
||||||
|
## 画布整体功能示意图
|
||||||
|

|
||||||
|
如上图所示,中间粉色区域及其周边的标尺,是画布区域,就是这个模块代码要处理的内容。<br/><br/>
|
||||||
|
## 已选组件的组件树
|
||||||
|

|
||||||
|
已选组件列表,组件列表也可以单选、多选、高亮、删除、拖拽组件到容器内<br/><br/>
|
||||||
|
|
||||||
|
## 画布支持的功能
|
||||||
|
- 渲染runtime
|
||||||
|
- 从编辑器增加组件,可以在左侧组件列表中通过单击/拖拽往画布中加入组件
|
||||||
|
- 删除组件,在画布中右键单击组件,在弹出菜单中删除;或者在左侧已选组件的组件树中右键删除组件
|
||||||
|
- 单选拖拽组件,可以在画布中选中组件,也可以在左侧目录中
|
||||||
|
- 多选拖拽组件,通过按住ctrl健选中多个组件
|
||||||
|
- 拖拽改变组件大小
|
||||||
|
- 旋转组件
|
||||||
|
- 高亮组件,在画布中mousemove经过组件的时候,或者在组件树中mousemove经过组件的时候,高亮组件
|
||||||
|
- 配置组件,单选选中组件之后,右侧表单区域对组件进行配置,并更新组件的渲染
|
||||||
|
- 添加/删除/隐藏/显示参考线,通过在标尺中往画布中拖拽,给画布添加参考线,图中两条竖向和一天横向的红色线条就是参考线
|
||||||
|
- 辅助对齐,单选和多选都支持拖拽过程中会辅助对齐其它组件,并在靠近参考线时吸附到参考线
|
||||||
|
- 拖拽组件进入容器,支持通过在画布中单选,或者在组件树中单选,将组件拖拽进入容器
|
||||||
|
<br/><br/>
|
||||||
|
# 核心类介绍
|
||||||
|
## StageCore
|
||||||
|
- 负责统一对外接口,编辑器通过StageCore传入runtime、添加/删除组件、缩放画布、更新参考线和标尺等;同时StageCore也会对外抛出事件,比如组件选中、多选、高亮、更新,runtimeReady等。
|
||||||
|
- 管理三个核心类:StageRender、StageMask、ActionManager
|
||||||
|
<br/><br/>
|
||||||
|
## StageRender
|
||||||
|
基于iframe加载传入进来的runtimeUrl,并支持增删改查组件。还提供了一个核心API:getElementsFromPoint,该API负责获取指定坐标下所有dom节点。
|
||||||
|
<br/><br/>
|
||||||
|
## StageMask
|
||||||
|
mask是一个盖在画布区域的一个蒙层,主要作用是隔离鼠标事件,避免鼠标事件直接作用于runtime中的组件,从而避免触发组件本身的点击事件(比如链接组件会跳走)。mask在滚动画布时,需要保证同步大小。
|
||||||
|
<br/><br/>
|
||||||
|
## ActionManager
|
||||||
|
- 负责监听鼠标和键盘事件,基于这些事件,形成单选、多选、高亮行为。主要监听的是蒙层上的鼠标事件,通过StageRender.getElementsFromPoint计算获得鼠标下方的组件,实现事件监听和实际组件的解构。
|
||||||
|
- 向上负责跟StageCore双向通信,提供接口供core调用,并向core抛出事件
|
||||||
|
- 向下管理StageDragResize、StageMultiDragResize、StageHightlight这三个单选、多选、高亮类,让它们协同工作
|
||||||
|
<br/><br/>
|
||||||
|
## StageDragResize
|
||||||
|
负责单选相关逻辑,拖拽、改变大小、旋转等行为是依赖于开源库Moveable实现的,这些行为并不是直接作用于组件本身,而是在蒙层上创建了一个跟组件同等大小的边框div,实际拖拽的是边框div,在拖拽过程中同步更新组件。
|
||||||
|
这个类的主要工作包括:
|
||||||
|
- 初始化组件操作边框,初始化moveable参数
|
||||||
|
- 更新moveable参数,比如增加了参考线、缩放了大小、表单改变了组件,都需要更新
|
||||||
|
- 接收moveable的回调函数,同步去更新实际组件的渲染
|
||||||
|
<br/><br/>
|
||||||
|
## StageMultiDragResize
|
||||||
|
功能跟StageDragResize类似,只是这个类是负责多选操作的,通过ctrl健选中多个组件,多选状态下不支持通过表单配置组件。
|
||||||
|
<br/><br/>
|
||||||
|
## StageHightlight
|
||||||
|
在鼠标经过画布中的组件、或者鼠标经过组件目录树中的组件时,会触发组件高亮,高亮也是通过moveable实现的,这个类主要负责初始化moveable并管理高亮状态。
|
||||||
|
<br/><br/>
|
||||||
|
## MoveableOptionsManager
|
||||||
|
StageDragResize、StageMultiDragResize的父类,负责管理Moveable的配置
|
||||||
|
<br/><br/>
|
||||||
|
## TargetShadow
|
||||||
|
统一管理拖拽和高亮框,包括创建、更新、销毁。
|
||||||
|
534
packages/stage/src/ActionManager.ts
Normal file
534
packages/stage/src/ActionManager.ts
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
/*
|
||||||
|
* 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 KeyController from 'keycon';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
|
|
||||||
|
import { Env } from '@tmagic/core';
|
||||||
|
import { Id } from '@tmagic/schema';
|
||||||
|
import { addClassName, getDocument, removeClassNameByClassName } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import { CONTAINER_HIGHLIGHT_CLASS_NAME, GHOST_EL_ID_PREFIX, GuidesType, MouseButton, PAGE_CLASS } from './const';
|
||||||
|
import StageDragResize from './StageDragResize';
|
||||||
|
import StageHighlight from './StageHighlight';
|
||||||
|
import StageMultiDragResize from './StageMultiDragResize';
|
||||||
|
import {
|
||||||
|
ActionManagerConfig,
|
||||||
|
CanSelect,
|
||||||
|
ContainerHighlightType,
|
||||||
|
CustomizeMoveableOptions,
|
||||||
|
CustomizeMoveableOptionsCallbackConfig,
|
||||||
|
GetElementsFromPoint,
|
||||||
|
GetRenderDocument,
|
||||||
|
GetTargetElement,
|
||||||
|
IsContainer,
|
||||||
|
Point,
|
||||||
|
SelectStatus,
|
||||||
|
StageDragStatus,
|
||||||
|
UpdateEventData,
|
||||||
|
} from './types';
|
||||||
|
import { isMoveableButton } from './util';
|
||||||
|
|
||||||
|
const throttleTime = 100;
|
||||||
|
const defaultContainerHighlightDuration = 800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理蒙层mask之上的操作:1、监听键盘鼠标事件,判断形成单选、多选、高亮操作;2、管理单选、多选、高亮三个类协同工作。
|
||||||
|
* @extends EventEmitter
|
||||||
|
*/
|
||||||
|
export default class ActionManager extends EventEmitter {
|
||||||
|
private dr: StageDragResize;
|
||||||
|
private multiDr: StageMultiDragResize;
|
||||||
|
private highlightLayer: StageHighlight;
|
||||||
|
/** 单选、多选、高亮的容器(蒙层的content) */
|
||||||
|
private container: HTMLElement;
|
||||||
|
/** 当前选中的节点 */
|
||||||
|
private selectedEl: HTMLElement | undefined;
|
||||||
|
/** 多选选中的节点组 */
|
||||||
|
private selectedElList: HTMLElement[] = [];
|
||||||
|
/** 当前高亮的节点 */
|
||||||
|
private highlightedEl: HTMLElement | undefined;
|
||||||
|
/** 当前是否处于多选状态 */
|
||||||
|
private isMultiSelectStatus = false;
|
||||||
|
/** 当拖拽组件到容器上方进入可加入容器状态时,给容器添加的一个class名称 */
|
||||||
|
private containerHighlightClassName: string;
|
||||||
|
/** 当拖拽组件到容器上方时,需要悬停多久才能将组件加入容器 */
|
||||||
|
private containerHighlightDuration: number;
|
||||||
|
/** 将组件加入容器的操作方式 */
|
||||||
|
private containerHighlightType?: ContainerHighlightType;
|
||||||
|
private isAltKeydown = false;
|
||||||
|
private getTargetElement: GetTargetElement;
|
||||||
|
private getElementsFromPoint: GetElementsFromPoint;
|
||||||
|
private canSelect: CanSelect;
|
||||||
|
private isContainer: IsContainer;
|
||||||
|
private getRenderDocument: GetRenderDocument;
|
||||||
|
|
||||||
|
private mouseMoveHandler = throttle(async (event: MouseEvent): Promise<void> => {
|
||||||
|
const el = await this.getElementFromPoint(event);
|
||||||
|
if (!el) {
|
||||||
|
this.clearHighlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.highlight(el);
|
||||||
|
}, throttleTime);
|
||||||
|
|
||||||
|
constructor(config: ActionManagerConfig) {
|
||||||
|
super();
|
||||||
|
this.container = config.container;
|
||||||
|
this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME;
|
||||||
|
this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration;
|
||||||
|
this.containerHighlightType = config.containerHighlightType;
|
||||||
|
this.getTargetElement = config.getTargetElement;
|
||||||
|
this.getElementsFromPoint = config.getElementsFromPoint;
|
||||||
|
this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id);
|
||||||
|
this.getRenderDocument = config.getRenderDocument;
|
||||||
|
this.isContainer = config.isContainer;
|
||||||
|
|
||||||
|
this.dr = new StageDragResize({
|
||||||
|
container: config.container,
|
||||||
|
getRootContainer: config.getRootContainer,
|
||||||
|
getRenderDocument: config.getRenderDocument,
|
||||||
|
updateDragEl: config.updateDragEl,
|
||||||
|
markContainerEnd: () => this.markContainerEnd(),
|
||||||
|
delayedMarkContainer: (event: MouseEvent, exclude: Element[]) => {
|
||||||
|
if (this.canAddToContainer()) {
|
||||||
|
return this.delayedMarkContainer(event, exclude);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
moveableOptions: this.changeCallback(config.moveableOptions),
|
||||||
|
});
|
||||||
|
this.multiDr = new StageMultiDragResize({
|
||||||
|
container: config.container,
|
||||||
|
multiMoveableOptions: config.multiMoveableOptions,
|
||||||
|
getRootContainer: config.getRootContainer,
|
||||||
|
getRenderDocument: config.getRenderDocument,
|
||||||
|
updateDragEl: config.updateDragEl,
|
||||||
|
});
|
||||||
|
this.highlightLayer = new StageHighlight({
|
||||||
|
container: config.container,
|
||||||
|
updateDragEl: config.updateDragEl,
|
||||||
|
getRootContainer: config.getRootContainer,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initMouseEvent();
|
||||||
|
this.initKeyEvent();
|
||||||
|
this.initActionEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置水平/垂直参考线
|
||||||
|
* @param type 参考线类型
|
||||||
|
* @param guidelines 参考线坐标数组
|
||||||
|
*/
|
||||||
|
public setGuidelines(type: GuidesType, guidelines: number[]): void {
|
||||||
|
this.dr.setGuidelines(type, guidelines);
|
||||||
|
this.multiDr.setGuidelines(type, guidelines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有参考线
|
||||||
|
*/
|
||||||
|
public clearGuides(): void {
|
||||||
|
this.dr.clearGuides();
|
||||||
|
this.multiDr.clearGuides();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新moveable,外部主要调用场景是元素配置变更、页面大小变更
|
||||||
|
* @param el 变更的元素
|
||||||
|
*/
|
||||||
|
public updateMoveable(el?: HTMLElement): void {
|
||||||
|
this.dr.updateMoveable(el);
|
||||||
|
// 多选时不可配置元素,因此不存在多选元素变更,不需要传el
|
||||||
|
this.multiDr.updateMoveable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否单选选中的元素
|
||||||
|
*/
|
||||||
|
public isSelectedEl(el: HTMLElement): boolean {
|
||||||
|
// 有可能dom已经重新渲染,不再是原来的dom了,所以这里判断id,而不是判断el === this.selectedDom
|
||||||
|
return el.id === this.selectedEl?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSelectedEl(el: HTMLElement): void {
|
||||||
|
this.selectedEl = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSelectedEl(): HTMLElement | undefined {
|
||||||
|
return this.selectedEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSelectedElList(): HTMLElement[] {
|
||||||
|
return this.selectedElList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取鼠标下方第一个可选中元素,如果元素层叠,返回到是最上层元素
|
||||||
|
* @param event 鼠标事件
|
||||||
|
* @returns 鼠标下方第一个可选中元素
|
||||||
|
*/
|
||||||
|
public async getElementFromPoint(event: MouseEvent): Promise<HTMLElement | undefined> {
|
||||||
|
const els = this.getElementsFromPoint(event as Point);
|
||||||
|
|
||||||
|
let stopped = false;
|
||||||
|
const stop = () => (stopped = true);
|
||||||
|
for (const el of els) {
|
||||||
|
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.isElCanSelect(el, event, stop))) {
|
||||||
|
if (stopped) break;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个元素能否在当前场景被选中
|
||||||
|
* @param el 被判断的元素
|
||||||
|
* @param event 鼠标事件
|
||||||
|
* @param stop 通过该元素如果得知剩下的元素都不可被选中,通知调用方终止对剩下元素的判断
|
||||||
|
* @returns 能否选中
|
||||||
|
*/
|
||||||
|
public async isElCanSelect(el: HTMLElement, event: MouseEvent, stop: () => boolean): Promise<boolean> {
|
||||||
|
// 执行业务方传入的判断逻辑
|
||||||
|
const canSelectByProp = await this.canSelect(el, event, stop);
|
||||||
|
if (!canSelectByProp) return false;
|
||||||
|
// 多选规则
|
||||||
|
if (this.isMultiSelectStatus) {
|
||||||
|
return this.canMultiSelect(el, stop);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个元素是否可以被多选,如果当前元素是page,则调stop函数告诉调用方不必继续判断其它元素了
|
||||||
|
*/
|
||||||
|
public canMultiSelect(el: HTMLElement, stop: () => boolean): boolean {
|
||||||
|
// 多选状态下不可以选中magic-ui-page,并停止继续向上层选中
|
||||||
|
if (el.className.includes(PAGE_CLASS)) {
|
||||||
|
stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const selectedEl = this.getSelectedEl();
|
||||||
|
// 先单击选中了页面(magic-ui-page),再按住多选键多选时,任一元素均可选中
|
||||||
|
if (selectedEl?.className.includes(PAGE_CLASS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.multiDr.canSelect(el, selectedEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public select(el: HTMLElement, event: MouseEvent | undefined): void {
|
||||||
|
this.selectedEl = el;
|
||||||
|
this.clearSelectStatus(SelectStatus.MULTI_SELECT);
|
||||||
|
this.dr.select(el, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public multiSelect(idOrElList: HTMLElement[] | Id[]): void {
|
||||||
|
this.selectedElList = idOrElList.map((idOrEl) => this.getTargetElement(idOrEl));
|
||||||
|
this.clearSelectStatus(SelectStatus.SELECT);
|
||||||
|
this.multiDr.multiSelect(this.selectedElList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHighlightEl(): HTMLElement | undefined {
|
||||||
|
return this.highlightedEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setHighlightEl(el: HTMLElement | undefined): void {
|
||||||
|
this.highlightedEl = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
public highlight(idOrEl: Id | HTMLElement): void {
|
||||||
|
let el;
|
||||||
|
try {
|
||||||
|
el = this.getTargetElement(idOrEl);
|
||||||
|
} catch (error) {
|
||||||
|
this.clearHighlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中组件不高亮、多选拖拽状态不高亮
|
||||||
|
if (el === this.getSelectedEl() || this.multiDr.dragStatus === StageDragStatus.ING) {
|
||||||
|
this.clearHighlight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el === this.highlightedEl || !el) return;
|
||||||
|
|
||||||
|
this.highlightLayer.highlight(el);
|
||||||
|
this.highlightedEl = el;
|
||||||
|
this.emit('highlight', el);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearHighlight(): void {
|
||||||
|
this.setHighlightEl(undefined);
|
||||||
|
this.highlightLayer.clearHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于在切换选择模式时清除上一次的状态
|
||||||
|
* @param selectType 需要清理的选择模式
|
||||||
|
*/
|
||||||
|
public clearSelectStatus(selectType: SelectStatus): void {
|
||||||
|
if (selectType === SelectStatus.MULTI_SELECT) {
|
||||||
|
this.multiDr.clearSelectStatus();
|
||||||
|
this.selectedElList = [];
|
||||||
|
} else {
|
||||||
|
this.dr.clearSelectStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找到鼠标下方的容器,通过添加className对容器进行标记
|
||||||
|
* @param event 鼠标事件
|
||||||
|
* @param excludeElList 计算鼠标点所在容器时要排除的元素列表
|
||||||
|
*/
|
||||||
|
public async addContainerHighlightClassName(event: MouseEvent, excludeElList: Element[]): Promise<void> {
|
||||||
|
const doc = this.getRenderDocument();
|
||||||
|
if (!doc) return;
|
||||||
|
|
||||||
|
const els = this.getElementsFromPoint(event);
|
||||||
|
|
||||||
|
for (const el of els) {
|
||||||
|
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.isContainer(el)) && !excludeElList.includes(el)) {
|
||||||
|
addClassName(el, doc, this.containerHighlightClassName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标拖拽着元素,在容器上方悬停,延迟一段时间后,对容器进行标记,如果悬停时间够长将标记成功,悬停时间短,调用方通过返回的timeoutId取消标记
|
||||||
|
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
||||||
|
* @param event 鼠标事件
|
||||||
|
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
||||||
|
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
||||||
|
*/
|
||||||
|
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout {
|
||||||
|
return globalThis.setTimeout(() => {
|
||||||
|
this.addContainerHighlightClassName(event, excludeElList);
|
||||||
|
}, this.containerHighlightDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.container.removeEventListener('mousedown', this.mouseDownHandler);
|
||||||
|
this.container.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||||
|
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler);
|
||||||
|
this.container.removeEventListener('wheel', this.mouseWheelHandler);
|
||||||
|
this.dr.destroy();
|
||||||
|
this.multiDr.destroy();
|
||||||
|
this.highlightLayer.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private changeCallback(options: CustomizeMoveableOptions): CustomizeMoveableOptions {
|
||||||
|
// 在actionManager才能获取到各种参数,在这里传好参数有比较好的扩展性
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
return () => {
|
||||||
|
// 要再判断一次,不然过不了ts检查
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
const cfg: CustomizeMoveableOptionsCallbackConfig = {
|
||||||
|
targetElId: this.selectedEl?.id,
|
||||||
|
};
|
||||||
|
return options(cfg);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在执行多选逻辑前,先准备好多选选中元素
|
||||||
|
* @param el 新选中的元素
|
||||||
|
* @returns 多选选中的元素列表
|
||||||
|
*/
|
||||||
|
private async beforeMultiSelect(event: MouseEvent): Promise<void> {
|
||||||
|
const el = await this.getElementFromPoint(event);
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
// 如果已有单选选中元素,不是magic-ui-page就可以加入多选列表
|
||||||
|
if (this.selectedEl && !this.selectedEl.className.includes(PAGE_CLASS)) {
|
||||||
|
this.selectedElList.push(this.selectedEl as HTMLElement);
|
||||||
|
this.selectedEl = undefined;
|
||||||
|
}
|
||||||
|
// 判断元素是否已在多选列表
|
||||||
|
const existIndex = this.selectedElList.findIndex((selectedDom) => selectedDom.id === el.id);
|
||||||
|
if (existIndex !== -1) {
|
||||||
|
// 再次点击取消选中
|
||||||
|
this.selectedElList.splice(existIndex, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedElList.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前状态下能否将组件加入容器,默认是鼠标悬停一段时间加入,alt模式则是按住alt+鼠标悬停一段时间加入
|
||||||
|
*/
|
||||||
|
private canAddToContainer(): boolean {
|
||||||
|
return (
|
||||||
|
this.containerHighlightType === ContainerHighlightType.DEFAULT ||
|
||||||
|
(this.containerHighlightType === ContainerHighlightType.ALT && this.isAltKeydown)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束对container的标记状态
|
||||||
|
* @returns 标记的容器元素,没有标记的容器时返回null
|
||||||
|
*/
|
||||||
|
private markContainerEnd(): HTMLElement | null {
|
||||||
|
const doc = this.getRenderDocument();
|
||||||
|
if (doc && this.canAddToContainer()) {
|
||||||
|
return removeClassNameByClassName(doc, this.containerHighlightClassName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initMouseEvent(): void {
|
||||||
|
this.container.addEventListener('mousedown', this.mouseDownHandler);
|
||||||
|
this.container.addEventListener('mousemove', this.mouseMoveHandler);
|
||||||
|
this.container.addEventListener('mouseleave', this.mouseLeaveHandler);
|
||||||
|
this.container.addEventListener('wheel', this.mouseWheelHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化键盘事件监听
|
||||||
|
*/
|
||||||
|
private initKeyEvent(): void {
|
||||||
|
const { isMac } = new Env();
|
||||||
|
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||||
|
|
||||||
|
// 多选启用状态监听
|
||||||
|
KeyController.global.keydown(ctrl, (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.isMultiSelectStatus = true;
|
||||||
|
});
|
||||||
|
// ctrl+tab切到其他窗口,需要将多选状态置为false
|
||||||
|
KeyController.global.on('blur', () => {
|
||||||
|
this.isMultiSelectStatus = false;
|
||||||
|
});
|
||||||
|
KeyController.global.keyup(ctrl, (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.isMultiSelectStatus = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// alt健监听,用于启用拖拽组件加入容器状态
|
||||||
|
KeyController.global.keydown('alt', (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.isAltKeydown = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
KeyController.global.keyup('alt', (e) => {
|
||||||
|
e.inputEvent.preventDefault();
|
||||||
|
this.markContainerEnd();
|
||||||
|
this.isAltKeydown = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单选、多选抛出来的事件
|
||||||
|
*/
|
||||||
|
private initActionEvent(): void {
|
||||||
|
this.dr
|
||||||
|
.on('update', (data: UpdateEventData) => {
|
||||||
|
// 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知
|
||||||
|
setTimeout(() => this.emit('update', data));
|
||||||
|
})
|
||||||
|
.on('sort', (data: UpdateEventData) => {
|
||||||
|
// 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知
|
||||||
|
setTimeout(() => this.emit('sort', data));
|
||||||
|
})
|
||||||
|
.on('select-parent', () => {
|
||||||
|
this.emit('select-parent');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.multiDr
|
||||||
|
.on('update', (data: UpdateEventData, parentEl: HTMLElement | null) => {
|
||||||
|
this.emit('multi-update', data, parentEl);
|
||||||
|
})
|
||||||
|
.on('change-to-select', async (id: Id) => {
|
||||||
|
// 如果还在多选状态,不触发切换到单选
|
||||||
|
if (this.isMultiSelectStatus) return false;
|
||||||
|
const el = this.getTargetElement(id);
|
||||||
|
this.emit('change-to-select', el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在down事件中集中cpu处理画布中选中操作渲染,在up事件中再通知外面的编辑器更新
|
||||||
|
*/
|
||||||
|
private mouseDownHandler = async (event: MouseEvent): Promise<void> => {
|
||||||
|
this.clearHighlight();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (this.isStopTriggerSelect(event)) return;
|
||||||
|
|
||||||
|
// 点击状态下不触发高亮事件
|
||||||
|
this.container.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||||
|
|
||||||
|
// 判断触发多选还是单选
|
||||||
|
if (this.isMultiSelectStatus) {
|
||||||
|
await this.beforeMultiSelect(event);
|
||||||
|
if (this.selectedElList.length > 0) {
|
||||||
|
this.emit('before-multi-select', this.selectedElList);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const el = await this.getElementFromPoint(event);
|
||||||
|
if (!el) return;
|
||||||
|
this.emit('before-select', el, event);
|
||||||
|
}
|
||||||
|
getDocument().addEventListener('mouseup', this.mouseUpHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
private isStopTriggerSelect(event: MouseEvent): boolean {
|
||||||
|
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return true;
|
||||||
|
if (!event.target) return true;
|
||||||
|
|
||||||
|
const targetClassList = (event.target as HTMLDivElement).classList;
|
||||||
|
|
||||||
|
// 如果单击多选选中区域,则不需要再触发选中了,要支持此处单击后进行拖动
|
||||||
|
if (!this.isMultiSelectStatus && targetClassList.contains('moveable-area')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 点击对象如果是边框锚点,则可能是resize; 点击对象是功能按钮
|
||||||
|
if (targetClassList.contains('moveable-control') || isMoveableButton(event.target as Element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在up事件中负责对外通知选中事件,通知画布之外的编辑器更新
|
||||||
|
*/
|
||||||
|
private mouseUpHandler = (): void => {
|
||||||
|
getDocument().removeEventListener('mouseup', this.mouseUpHandler);
|
||||||
|
this.container.addEventListener('mousemove', this.mouseMoveHandler);
|
||||||
|
if (this.isMultiSelectStatus) {
|
||||||
|
this.emit('multi-select', this.selectedElList);
|
||||||
|
} else {
|
||||||
|
this.emit('select', this.selectedEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private mouseLeaveHandler = () => {
|
||||||
|
setTimeout(() => this.clearHighlight(), throttleTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
private mouseWheelHandler = () => {
|
||||||
|
this.clearHighlight();
|
||||||
|
};
|
||||||
|
}
|
249
packages/stage/src/MoveableOptionsManager.ts
Normal file
249
packages/stage/src/MoveableOptionsManager.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* 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 { merge } from 'lodash-es';
|
||||||
|
import { MoveableOptions } from 'moveable';
|
||||||
|
|
||||||
|
import { GuidesType, Mode } from './const';
|
||||||
|
import selectParentAbles from './MoveableSelectParentAble';
|
||||||
|
import { GetRootContainer, MoveableOptionsManagerConfig } from './types';
|
||||||
|
import { getOffset } from './util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选和多选的父类,用于管理moveableOptions
|
||||||
|
* @extends EventEmitter
|
||||||
|
*/
|
||||||
|
export default class MoveableOptionsManager extends EventEmitter {
|
||||||
|
/** 布局方式:流式布局、绝对定位、固定定位 */
|
||||||
|
public mode: Mode = Mode.ABSOLUTE;
|
||||||
|
|
||||||
|
/** 画布容器 */
|
||||||
|
protected container: HTMLElement;
|
||||||
|
|
||||||
|
/** 水平参考线 */
|
||||||
|
private horizontalGuidelines: number[] = [];
|
||||||
|
/** 垂直参考线 */
|
||||||
|
private verticalGuidelines: number[] = [];
|
||||||
|
/** 对齐元素集合 */
|
||||||
|
private elementGuidelines: HTMLElement[] = [];
|
||||||
|
/** 由外部调用方(编辑器)传入进来的moveable默认参数,可以为空,也可以是一个回调函数 */
|
||||||
|
private customizedOptions?: (() => MoveableOptions) | MoveableOptions;
|
||||||
|
/** 获取整个画布的根元素(在StageCore的mount函数中挂载的container) */
|
||||||
|
private getRootContainer: GetRootContainer;
|
||||||
|
|
||||||
|
constructor(config: MoveableOptionsManagerConfig) {
|
||||||
|
super();
|
||||||
|
this.customizedOptions = config.moveableOptions;
|
||||||
|
this.container = config.container;
|
||||||
|
this.getRootContainer = config.getRootContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置水平/垂直参考线
|
||||||
|
* @param type 参考线类型
|
||||||
|
* @param guidelines 参考线坐标数组
|
||||||
|
*/
|
||||||
|
public setGuidelines(type: GuidesType, guidelines: number[]): void {
|
||||||
|
if (type === GuidesType.HORIZONTAL) {
|
||||||
|
this.horizontalGuidelines = guidelines;
|
||||||
|
} else if (type === GuidesType.VERTICAL) {
|
||||||
|
this.verticalGuidelines = guidelines;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('update-moveable');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除横向和纵向的参考线
|
||||||
|
*/
|
||||||
|
public clearGuides(): void {
|
||||||
|
this.horizontalGuidelines = [];
|
||||||
|
this.verticalGuidelines = [];
|
||||||
|
|
||||||
|
this.emit('update-moveable');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置有哪些元素要辅助对齐
|
||||||
|
* @param selectedElList 选中的元素列表,需要排除在对齐元素之外
|
||||||
|
* @param allElList 全部元素列表
|
||||||
|
*/
|
||||||
|
protected setElementGuidelines(selectedElList: HTMLElement[], allElList: HTMLElement[]): void {
|
||||||
|
this.elementGuidelines.forEach((node) => {
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
this.elementGuidelines = [];
|
||||||
|
|
||||||
|
if (this.mode === Mode.ABSOLUTE) {
|
||||||
|
this.container.append(this.createGuidelineElements(selectedElList, allElList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取moveable参数
|
||||||
|
* @param isMultiSelect 是否多选模式
|
||||||
|
* @param runtimeOptions 调用时实时传进来的的moveable参数
|
||||||
|
* @returns moveable所需参数
|
||||||
|
*/
|
||||||
|
protected getOptions(isMultiSelect: boolean, runtimeOptions: MoveableOptions = {}): MoveableOptions {
|
||||||
|
const defaultOptions = this.getDefaultOptions(isMultiSelect);
|
||||||
|
const customizedOptions = this.getCustomizeOptions();
|
||||||
|
|
||||||
|
return merge(defaultOptions, customizedOptions, runtimeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单选和多选的moveable公共参数
|
||||||
|
* @returns moveable公共参数
|
||||||
|
*/
|
||||||
|
private getDefaultOptions(isMultiSelect: boolean): MoveableOptions {
|
||||||
|
const isSortable = this.mode === Mode.SORTABLE;
|
||||||
|
|
||||||
|
const commonOptions = {
|
||||||
|
draggable: true,
|
||||||
|
resizable: true,
|
||||||
|
rootContainer: this.getRootContainer(),
|
||||||
|
zoom: 1,
|
||||||
|
throttleDrag: 0,
|
||||||
|
snappable: true,
|
||||||
|
horizontalGuidelines: this.horizontalGuidelines,
|
||||||
|
verticalGuidelines: this.verticalGuidelines,
|
||||||
|
elementGuidelines: this.elementGuidelines,
|
||||||
|
bounds: {
|
||||||
|
top: 0,
|
||||||
|
// 设置0的话无法移动到left为0,所以只能设置为-1
|
||||||
|
left: -1,
|
||||||
|
right: this.container.clientWidth - 1,
|
||||||
|
bottom: isSortable ? undefined : this.container.clientHeight,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const differenceOptions = isMultiSelect ? this.getMultiOptions() : this.getSingleOptions();
|
||||||
|
|
||||||
|
return merge(commonOptions, differenceOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单选下的差异化参数
|
||||||
|
* @returns {MoveableOptions} moveable options参数
|
||||||
|
*/
|
||||||
|
private getSingleOptions(): MoveableOptions {
|
||||||
|
const isAbsolute = this.mode === Mode.ABSOLUTE;
|
||||||
|
const isFixed = this.mode === Mode.FIXED;
|
||||||
|
|
||||||
|
return {
|
||||||
|
origin: false,
|
||||||
|
dragArea: false,
|
||||||
|
scalable: false,
|
||||||
|
rotatable: false,
|
||||||
|
snapGap: isAbsolute || isFixed,
|
||||||
|
snapThreshold: 5,
|
||||||
|
snapDigit: 0,
|
||||||
|
isDisplaySnapDigit: isAbsolute,
|
||||||
|
snapDirections: {
|
||||||
|
top: isAbsolute,
|
||||||
|
right: isAbsolute,
|
||||||
|
bottom: isAbsolute,
|
||||||
|
left: isAbsolute,
|
||||||
|
center: isAbsolute,
|
||||||
|
middle: isAbsolute,
|
||||||
|
},
|
||||||
|
elementSnapDirections: {
|
||||||
|
top: isAbsolute,
|
||||||
|
right: isAbsolute,
|
||||||
|
bottom: isAbsolute,
|
||||||
|
left: isAbsolute,
|
||||||
|
},
|
||||||
|
isDisplayInnerSnapDigit: true,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
selectParent: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
ables: [selectParentAbles(this.selectParentHandler.bind(this))],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取多选下的差异化参数
|
||||||
|
* @returns {MoveableOptions} moveable options参数
|
||||||
|
*/
|
||||||
|
private getMultiOptions(): MoveableOptions {
|
||||||
|
return {
|
||||||
|
defaultGroupRotate: 0,
|
||||||
|
defaultGroupOrigin: '50% 50%',
|
||||||
|
startDragRotate: 0,
|
||||||
|
throttleDragRotate: 0,
|
||||||
|
origin: true,
|
||||||
|
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务方自定义的moveable参数
|
||||||
|
*/
|
||||||
|
private getCustomizeOptions(): MoveableOptions | undefined {
|
||||||
|
if (typeof this.customizedOptions === 'function') {
|
||||||
|
return this.customizedOptions();
|
||||||
|
}
|
||||||
|
return this.customizedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这是给selectParentAbles的回调函数,用于触发选中父元素事件
|
||||||
|
*/
|
||||||
|
private selectParentHandler(): void {
|
||||||
|
this.emit('select-parent');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为需要辅助对齐的元素创建div
|
||||||
|
* @param selectedElList 选中的元素列表,需要排除在对齐元素之外
|
||||||
|
* @param allElList 全部元素列表
|
||||||
|
* @returns frame 辅助对齐元素集合的页面片
|
||||||
|
*/
|
||||||
|
private createGuidelineElements(selectedElList: HTMLElement[], allElList: HTMLElement[]): DocumentFragment {
|
||||||
|
const frame = globalThis.document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (const node of allElList) {
|
||||||
|
const { width, height } = node.getBoundingClientRect();
|
||||||
|
if (this.isInElementList(node, selectedElList)) continue;
|
||||||
|
const { left, top } = getOffset(node);
|
||||||
|
const elementGuideline = globalThis.document.createElement('div');
|
||||||
|
elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
|
||||||
|
this.elementGuidelines.push(elementGuideline);
|
||||||
|
frame.append(elementGuideline);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个元素是否在元素列表里面
|
||||||
|
* @param ele 元素
|
||||||
|
* @param eleList 元素列表
|
||||||
|
* @returns 是否在元素列表里面
|
||||||
|
*/
|
||||||
|
private isInElementList(ele: HTMLElement, eleList: HTMLElement[]): boolean {
|
||||||
|
for (const eleItem of eleList) {
|
||||||
|
if (ele === eleItem) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
import { MoveableManagerInterface, Renderer } from 'moveable';
|
import { MoveableManagerInterface, Renderer } from 'moveable';
|
||||||
|
|
||||||
import type StageDragResize from './StageDragResize';
|
export default (selectParentHandler: () => void) => ({
|
||||||
|
name: 'select-parent',
|
||||||
export default (dr: StageDragResize) => ({
|
|
||||||
name: 'selectParent',
|
|
||||||
props: {},
|
props: {},
|
||||||
events: {},
|
events: {},
|
||||||
render(moveable: MoveableManagerInterface<any, any>, React: Renderer) {
|
render(moveable: MoveableManagerInterface<any, any>, React: Renderer) {
|
||||||
@ -51,7 +49,7 @@ export default (dr: StageDragResize) => ({
|
|||||||
className: 'moveable-button',
|
className: 'moveable-button',
|
||||||
title: '选中父组件',
|
title: '选中父组件',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
dr.emit('select-parent');
|
selectParentHandler();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
React.createElement(
|
React.createElement(
|
||||||
|
@ -60,12 +60,12 @@ export default class Rule extends EventEmitter {
|
|||||||
defaultGuides: vLines,
|
defaultGuides: vLines,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit('changeGuides', {
|
this.emit('change-guides', {
|
||||||
type: GuidesType.HORIZONTAL,
|
type: GuidesType.HORIZONTAL,
|
||||||
guides: hLines,
|
guides: hLines,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit('changeGuides', {
|
this.emit('change-guides', {
|
||||||
type: GuidesType.VERTICAL,
|
type: GuidesType.VERTICAL,
|
||||||
guides: vLines,
|
guides: vLines,
|
||||||
});
|
});
|
||||||
@ -143,7 +143,7 @@ export default class Rule extends EventEmitter {
|
|||||||
|
|
||||||
private hGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
private hGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||||
this.horizontalGuidelines = e.guides;
|
this.horizontalGuidelines = e.guides;
|
||||||
this.emit('changeGuides', {
|
this.emit('change-guides', {
|
||||||
type: GuidesType.HORIZONTAL,
|
type: GuidesType.HORIZONTAL,
|
||||||
guides: this.horizontalGuidelines,
|
guides: this.horizontalGuidelines,
|
||||||
});
|
});
|
||||||
@ -151,7 +151,7 @@ export default class Rule extends EventEmitter {
|
|||||||
|
|
||||||
private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
|
||||||
this.verticalGuidelines = e.guides;
|
this.verticalGuidelines = e.guides;
|
||||||
this.emit('changeGuides', {
|
this.emit('change-guides', {
|
||||||
type: GuidesType.VERTICAL,
|
type: GuidesType.VERTICAL,
|
||||||
guides: this.verticalGuidelines,
|
guides: this.verticalGuidelines,
|
||||||
});
|
});
|
||||||
|
@ -18,303 +18,149 @@
|
|||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import type { Id } from '@tmagic/schema';
|
import { Id } from '@tmagic/schema';
|
||||||
import { addClassName } from '@tmagic/utils';
|
|
||||||
|
|
||||||
import { CONTAINER_HIGHLIGHT_CLASS, DEFAULT_ZOOM, GHOST_EL_ID_PREFIX, PAGE_CLASS } from './const';
|
import ActionManager from './ActionManager';
|
||||||
import StageDragResize from './StageDragResize';
|
import { DEFAULT_ZOOM } from './const';
|
||||||
import StageHighlight from './StageHighlight';
|
|
||||||
import StageMask from './StageMask';
|
import StageMask from './StageMask';
|
||||||
import StageMultiDragResize from './StageMultiDragResize';
|
|
||||||
import StageRender from './StageRender';
|
import StageRender from './StageRender';
|
||||||
import {
|
import {
|
||||||
CanSelect,
|
ActionManagerConfig,
|
||||||
ContainerHighlightType,
|
CustomizeRender,
|
||||||
GuidesEventData,
|
GuidesEventData,
|
||||||
IsContainer,
|
Point,
|
||||||
RemoveData,
|
RemoveData,
|
||||||
Runtime,
|
Runtime,
|
||||||
SortEventData,
|
|
||||||
StageCoreConfig,
|
StageCoreConfig,
|
||||||
StageDragStatus,
|
|
||||||
UpdateData,
|
UpdateData,
|
||||||
UpdateEventData,
|
UpdateEventData,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { addSelectedClassName, removeSelectedClassName } from './util';
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 负责管理画布,管理renderer、mask、actionManager三个核心类,并负责统一对外通信,包括提供接口和抛事件
|
||||||
|
*/
|
||||||
export default class StageCore extends EventEmitter {
|
export default class StageCore extends EventEmitter {
|
||||||
public container?: HTMLDivElement;
|
public container?: HTMLDivElement;
|
||||||
// 当前选中的节点
|
|
||||||
public selectedDom: HTMLElement | undefined;
|
|
||||||
// 多选选中的节点组
|
|
||||||
public selectedDomList: HTMLElement[] = [];
|
|
||||||
public highlightedDom: Element | undefined;
|
|
||||||
public renderer: StageRender;
|
public renderer: StageRender;
|
||||||
public mask: StageMask;
|
public mask: StageMask;
|
||||||
public dr: StageDragResize;
|
|
||||||
public multiDr: StageMultiDragResize;
|
|
||||||
public highlightLayer: StageHighlight;
|
|
||||||
public config: StageCoreConfig;
|
|
||||||
public zoom = DEFAULT_ZOOM;
|
|
||||||
public containerHighlightClassName: string;
|
|
||||||
public containerHighlightDuration: number;
|
|
||||||
public containerHighlightType?: ContainerHighlightType;
|
|
||||||
public isContainer: IsContainer;
|
|
||||||
|
|
||||||
private canSelect: CanSelect;
|
private actionManager: ActionManager;
|
||||||
private pageResizeObserver: ResizeObserver | null = null;
|
private pageResizeObserver: ResizeObserver | null = null;
|
||||||
|
private autoScrollIntoView: boolean | undefined;
|
||||||
|
private customizedRender?: CustomizeRender;
|
||||||
|
|
||||||
constructor(config: StageCoreConfig) {
|
constructor(config: StageCoreConfig) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.config = config;
|
this.autoScrollIntoView = config.autoScrollIntoView;
|
||||||
|
this.customizedRender = config.render;
|
||||||
|
|
||||||
this.setZoom(config.zoom);
|
this.renderer = new StageRender({
|
||||||
this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id);
|
runtimeUrl: config.runtimeUrl,
|
||||||
this.isContainer = config.isContainer;
|
zoom: config.zoom,
|
||||||
this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS;
|
customizedRender: async (): Promise<HTMLElement | null> => {
|
||||||
this.containerHighlightDuration = config.containerHighlightDuration || 800;
|
if (this?.customizedRender) {
|
||||||
this.containerHighlightType = config.containerHighlightType;
|
return await this.customizedRender(this);
|
||||||
|
}
|
||||||
this.renderer = new StageRender({ runtimeUrl: config.runtimeUrl, render: this.render.bind(this) });
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
this.mask = new StageMask();
|
this.mask = new StageMask();
|
||||||
this.dr = new StageDragResize({ core: this, container: this.mask.content, mask: this.mask });
|
this.actionManager = new ActionManager(this.getActionManagerConfig(config));
|
||||||
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) => {
|
this.initRenderEvent();
|
||||||
this.emit('runtime-ready', runtime);
|
this.initActionEvent();
|
||||||
});
|
this.initMaskEvent();
|
||||||
this.renderer.on('page-el-update', (el: HTMLElement) => {
|
|
||||||
this.mask?.observe(el);
|
|
||||||
this.observePageResize(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mask
|
|
||||||
.on('beforeSelect', async (event: MouseEvent) => {
|
|
||||||
this.clearSelectStatus('multiSelect');
|
|
||||||
const el = await this.getElementFromPoint(event);
|
|
||||||
if (!el) return;
|
|
||||||
this.select(el, event);
|
|
||||||
})
|
|
||||||
.on('select', () => {
|
|
||||||
this.emit('select', this.selectedDom);
|
|
||||||
})
|
|
||||||
.on('changeGuides', (data: GuidesEventData) => {
|
|
||||||
this.dr.setGuidelines(data.type, data.guides);
|
|
||||||
this.emit('changeGuides', data);
|
|
||||||
})
|
|
||||||
.on('highlight', async (event: MouseEvent) => {
|
|
||||||
const el = await this.getElementFromPoint(event);
|
|
||||||
if (!el) return;
|
|
||||||
// 如果多选组件处于拖拽状态时不进行组件高亮
|
|
||||||
if (this.multiDr.dragStatus === StageDragStatus.ING) return;
|
|
||||||
await this.highlight(el);
|
|
||||||
if (this.highlightedDom === this.selectedDom) {
|
|
||||||
this.highlightLayer.clearHighlight();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.emit('highlight', this.highlightedDom);
|
|
||||||
})
|
|
||||||
.on('clearHighlight', async () => {
|
|
||||||
this.highlightLayer.clearHighlight();
|
|
||||||
})
|
|
||||||
.on('beforeMultiSelect', async (event: MouseEvent) => {
|
|
||||||
const el = await this.getElementFromPoint(event);
|
|
||||||
if (!el) return;
|
|
||||||
// 如果已有单选选中元素,不是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.multiSelect(this.selectedDomList);
|
|
||||||
this.emit('multiSelect', this.selectedDomList);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 要先触发select,在触发update
|
|
||||||
this.dr
|
|
||||||
.on('update', (data: UpdateEventData) => {
|
|
||||||
setTimeout(() => this.emit('update', data));
|
|
||||||
})
|
|
||||||
.on('sort', (data: UpdateEventData) => {
|
|
||||||
setTimeout(() => this.emit('sort', data));
|
|
||||||
})
|
|
||||||
.on('select-parent', () => {
|
|
||||||
this.emit('select-parent');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.multiDr
|
|
||||||
.on('update', (data: UpdateEventData) => {
|
|
||||||
setTimeout(() => this.emit('update', data));
|
|
||||||
})
|
|
||||||
.on('select', async (id: Id) => {
|
|
||||||
const el = await this.getTargetElement(id);
|
|
||||||
this.select(el); // 选中
|
|
||||||
setTimeout(() => this.emit('select', el)); // set node
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getElementsFromPoint(event: MouseEvent) {
|
|
||||||
const { renderer, zoom } = this;
|
|
||||||
|
|
||||||
let x = event.clientX;
|
|
||||||
let y = event.clientY;
|
|
||||||
|
|
||||||
if (renderer.iframe) {
|
|
||||||
const rect = renderer.iframe.getClientRects()[0];
|
|
||||||
if (rect) {
|
|
||||||
x = x - rect.left;
|
|
||||||
y = y - rect.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderer.getDocument()?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getElementFromPoint(event: MouseEvent) {
|
|
||||||
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.isElCanSelect(el, event, stop))) {
|
|
||||||
if (stopped) break;
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isElCanSelect(el: HTMLElement, event: MouseEvent, stop: () => boolean): Promise<Boolean> {
|
|
||||||
// 执行业务方传入的判断逻辑
|
|
||||||
const canSelectByProp = await this.canSelect(el, event, stop);
|
|
||||||
if (!canSelectByProp) return false;
|
|
||||||
// 多选规则
|
|
||||||
if (this.mask.isMultiSelectStatus) {
|
|
||||||
return this.multiDr.canSelect(el, stop);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中组件
|
* 单选选中元素
|
||||||
* @param idOrEl 组件Dom节点的id属性,或者Dom节点
|
* @param idOrEl 选中的id或者元素
|
||||||
*/
|
*/
|
||||||
public async select(idOrEl: Id | HTMLElement, event?: MouseEvent): Promise<void> {
|
public async select(idOrEl: Id | HTMLElement, event?: MouseEvent): Promise<void> {
|
||||||
this.clearSelectStatus('multiSelect');
|
const el = this.renderer.getTargetElement(idOrEl);
|
||||||
const el = await this.getTargetElement(idOrEl);
|
if (el === this.actionManager.getSelectedEl()) return;
|
||||||
|
|
||||||
if (el === this.selectedDom) return;
|
await this.renderer.select([el]);
|
||||||
|
|
||||||
const runtime = await this.renderer.getRuntime();
|
|
||||||
|
|
||||||
await runtime?.select?.(el.id);
|
|
||||||
|
|
||||||
if (runtime?.beforeSelect) {
|
|
||||||
await runtime.beforeSelect(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mask.setLayout(el);
|
this.mask.setLayout(el);
|
||||||
this.dr.select(el, event);
|
|
||||||
|
|
||||||
if (this.config.autoScrollIntoView || el.dataset.autoScrollIntoView) {
|
this.actionManager.select(el, event);
|
||||||
|
|
||||||
|
if (this.autoScrollIntoView || el.dataset.autoScrollIntoView) {
|
||||||
this.mask.observerIntersection(el);
|
this.mask.observerIntersection(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedDom = el;
|
|
||||||
|
|
||||||
const doc = this.renderer.getDocument();
|
|
||||||
if (doc) {
|
|
||||||
removeSelectedClassName(doc);
|
|
||||||
if (this.selectedDom) {
|
|
||||||
addSelectedClassName(this.selectedDom, doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多选
|
* 多选选中多个元素
|
||||||
* @param domList 多选节点
|
* @param idOrElList 选中元素的id或元素列表
|
||||||
*/
|
*/
|
||||||
public async multiSelect(idOrElList: HTMLElement[] | Id[]): Promise<void> {
|
public async multiSelect(idOrElList: HTMLElement[] | Id[]): Promise<void> {
|
||||||
this.clearSelectStatus('select');
|
const els = idOrElList.map((idOrEl) => this.renderer.getTargetElement(idOrEl));
|
||||||
this.selectedDomList = await Promise.all(idOrElList.map(async (idOrEl) => await this.getTargetElement(idOrEl)));
|
if (els.length === 0) return;
|
||||||
this.multiDr.multiSelect(this.selectedDomList);
|
|
||||||
|
const lastEl = els[els.length - 1];
|
||||||
|
// 是否减少了组件选择
|
||||||
|
const isReduceSelect = els.length < this.actionManager.getSelectedElList().length;
|
||||||
|
await this.renderer.select(els);
|
||||||
|
|
||||||
|
this.mask.setLayout(lastEl);
|
||||||
|
|
||||||
|
this.actionManager.multiSelect(idOrElList);
|
||||||
|
|
||||||
|
if ((this.autoScrollIntoView || lastEl.dataset.autoScrollIntoView) && !isReduceSelect) {
|
||||||
|
this.mask.observerIntersection(lastEl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新选中的节点
|
* 高亮选中元素
|
||||||
* @param data 更新的数据
|
* @param el 要高亮的元素
|
||||||
*/
|
*/
|
||||||
public update(data: UpdateData): Promise<void> {
|
public highlight(idOrEl: Id | HTMLElement): void {
|
||||||
|
this.actionManager.highlight(idOrEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新组件
|
||||||
|
* @param data 更新组件的数据
|
||||||
|
*/
|
||||||
|
public async update(data: UpdateData): Promise<void> {
|
||||||
const { config } = data;
|
const { config } = data;
|
||||||
|
|
||||||
return this.renderer?.getRuntime().then((runtime) => {
|
await this.renderer.update(data);
|
||||||
runtime?.update?.(data);
|
// 通过setTimeout等画布中组件完成渲染更新
|
||||||
// 更新配置后,需要等组件渲染更新
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const el = this.renderer.getDocument()?.getElementById(`${config.id}`);
|
const el = this.renderer.getTargetElement(`${config.id}`);
|
||||||
// 有可能dom已经重新渲染,不再是原来的dom了,所以这里判断id,而不是判断el === this.selectedDom
|
if (el && this.actionManager.isSelectedEl(el)) {
|
||||||
if (el && el.id === this.selectedDom?.id) {
|
|
||||||
this.selectedDom = el;
|
|
||||||
// 更新了组件的布局,需要重新设置mask是否可以滚动
|
// 更新了组件的布局,需要重新设置mask是否可以滚动
|
||||||
this.mask.setLayout(el);
|
this.mask.setLayout(el);
|
||||||
this.dr.updateMoveable(el);
|
// 组件有更新,需要set
|
||||||
|
this.actionManager.setSelectedEl(el);
|
||||||
|
this.actionManager.updateMoveable(el);
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 高亮选中组件
|
* 往画布增加一个组件
|
||||||
* @param el 页面Dom节点
|
* @param data 组件信息数据
|
||||||
*/
|
*/
|
||||||
public async highlight(idOrEl: HTMLElement | Id): Promise<void> {
|
public async add(data: UpdateData): Promise<void> {
|
||||||
let el;
|
return await this.renderer.add(data);
|
||||||
try {
|
|
||||||
el = await this.getTargetElement(idOrEl);
|
|
||||||
} catch (error) {
|
|
||||||
this.highlightLayer.clearHighlight();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (el === this.highlightedDom || !el) return;
|
|
||||||
this.highlightLayer.highlight(el);
|
|
||||||
this.highlightedDom = el;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sortNode(data: SortEventData): Promise<void> {
|
/**
|
||||||
return this.renderer?.getRuntime().then((runtime) => runtime?.sortNode?.(data));
|
* 从画布删除一个组件
|
||||||
}
|
* @param data 组件信息数据
|
||||||
|
*/
|
||||||
public add(data: UpdateData): Promise<void> {
|
public async remove(data: RemoveData): Promise<void> {
|
||||||
return this.renderer?.getRuntime().then((runtime) => runtime?.add?.(data));
|
return await this.renderer.remove(data);
|
||||||
}
|
|
||||||
|
|
||||||
public remove(data: RemoveData): Promise<void> {
|
|
||||||
return this.renderer?.getRuntime().then((runtime) => runtime?.remove?.(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setZoom(zoom: number = DEFAULT_ZOOM): void {
|
public setZoom(zoom: number = DEFAULT_ZOOM): void {
|
||||||
this.zoom = zoom;
|
this.renderer.setZoom(zoom);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于在切换选择模式时清除上一次的状态
|
|
||||||
* @param selectType 需要清理的选择模式 多选:multiSelect,单选:select
|
|
||||||
*/
|
|
||||||
public clearSelectStatus(selectType: String) {
|
|
||||||
if (selectType === 'multiSelect') {
|
|
||||||
this.multiDr.clearSelectStatus();
|
|
||||||
this.selectedDomList = [];
|
|
||||||
} else {
|
|
||||||
this.dr.clearSelectStatus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -336,40 +182,36 @@ export default class StageCore extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public clearGuides() {
|
public clearGuides() {
|
||||||
this.mask.clearGuides();
|
this.mask.clearGuides();
|
||||||
this.dr.clearGuides();
|
this.actionManager.clearGuides();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addContainerHighlightClassName(event: MouseEvent, exclude: Element[]) {
|
/**
|
||||||
const els = this.getElementsFromPoint(event);
|
* @deprecated 废弃接口,建议用delayedMarkContainer代替
|
||||||
const { renderer } = this;
|
*/
|
||||||
const doc = renderer.getDocument();
|
public getAddContainerHighlightClassNameTimeout(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout {
|
||||||
|
return this.delayedMarkContainer(event, excludeElList);
|
||||||
if (!doc) return;
|
|
||||||
|
|
||||||
for (const el of els) {
|
|
||||||
if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.isContainer(el)) && !exclude.includes(el)) {
|
|
||||||
addClassName(el, doc, this.containerHighlightClassName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAddContainerHighlightClassNameTimeout(event: MouseEvent, exclude: Element[] = []): NodeJS.Timeout {
|
/**
|
||||||
return globalThis.setTimeout(() => {
|
* 鼠标拖拽着元素,在容器上方悬停,延迟一段时间后,对容器进行标记,如果悬停时间够长将标记成功,悬停时间短,调用方通过返回的timeoutId取消标记
|
||||||
this.addContainerHighlightClassName(event, exclude);
|
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
||||||
}, this.containerHighlightDuration);
|
* @param event 鼠标事件
|
||||||
|
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
||||||
|
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
||||||
|
*/
|
||||||
|
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout {
|
||||||
|
return this.actionManager.delayedMarkContainer(event, excludeElList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
const { mask, renderer, dr, highlightLayer, pageResizeObserver } = this;
|
const { mask, renderer, actionManager, pageResizeObserver } = this;
|
||||||
|
|
||||||
renderer.destroy();
|
renderer.destroy();
|
||||||
mask.destroy();
|
mask.destroy();
|
||||||
dr.destroy();
|
actionManager.destroy();
|
||||||
highlightLayer.destroy();
|
|
||||||
pageResizeObserver?.disconnect();
|
pageResizeObserver?.disconnect();
|
||||||
|
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
@ -384,32 +226,117 @@ export default class StageCore extends EventEmitter {
|
|||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
this.pageResizeObserver = new ResizeObserver((entries) => {
|
this.pageResizeObserver = new ResizeObserver((entries) => {
|
||||||
this.mask.pageResize(entries);
|
this.mask.pageResize(entries);
|
||||||
|
this.actionManager.updateMoveable();
|
||||||
if (this.dr.moveable) {
|
|
||||||
this.dr.updateMoveable();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pageResizeObserver.observe(page);
|
this.pageResizeObserver.observe(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getActionManagerConfig(config: StageCoreConfig): ActionManagerConfig {
|
||||||
* 传入stageRender供其回调,获取业务方自定义渲染画布页面渲染结果
|
const actionManagerConfig: ActionManagerConfig = {
|
||||||
*/
|
containerHighlightClassName: config.containerHighlightClassName,
|
||||||
private async render(): Promise<HTMLElement | null> {
|
containerHighlightDuration: config.containerHighlightDuration,
|
||||||
if (this.config?.render) {
|
containerHighlightType: config.containerHighlightType,
|
||||||
return await this.config.render(this);
|
moveableOptions: config.moveableOptions,
|
||||||
}
|
multiMoveableOptions: config.multiMoveableOptions,
|
||||||
return null;
|
container: this.mask.content,
|
||||||
|
canSelect: config.canSelect,
|
||||||
|
isContainer: config.isContainer,
|
||||||
|
updateDragEl: config.updateDragEl,
|
||||||
|
getRootContainer: () => this.container,
|
||||||
|
getRenderDocument: () => this.renderer.getDocument(),
|
||||||
|
getTargetElement: (idOrEl: Id | HTMLElement) => this.renderer.getTargetElement(idOrEl),
|
||||||
|
getElementsFromPoint: (point: Point) => this.renderer.getElementsFromPoint(point),
|
||||||
|
};
|
||||||
|
|
||||||
|
return actionManagerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTargetElement(idOrEl: Id | HTMLElement): Promise<HTMLElement> {
|
private initRenderEvent(): void {
|
||||||
if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
|
this.renderer.on('runtime-ready', (runtime: Runtime) => {
|
||||||
const el = this.renderer.getDocument()?.getElementById(`${idOrEl}`);
|
this.emit('runtime-ready', runtime);
|
||||||
if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
|
});
|
||||||
return el;
|
this.renderer.on('page-el-update', (el: HTMLElement) => {
|
||||||
|
this.mask?.observe(el);
|
||||||
|
this.observePageResize(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return idOrEl;
|
|
||||||
|
private initMaskEvent(): void {
|
||||||
|
this.mask.on('change-guides', (data: GuidesEventData) => {
|
||||||
|
this.actionManager.setGuidelines(data.type, data.guides);
|
||||||
|
this.emit('change-guides', data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化操作相关事件监听
|
||||||
|
*/
|
||||||
|
private initActionEvent(): void {
|
||||||
|
this.initActionManagerEvent();
|
||||||
|
this.initDrEvent();
|
||||||
|
this.initMulDrEvent();
|
||||||
|
this.initHighlightEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化ActionManager类本身抛出来的事件监听
|
||||||
|
*/
|
||||||
|
private initActionManagerEvent(): void {
|
||||||
|
this.actionManager
|
||||||
|
.on('before-select', (idOrEl: Id | HTMLElement, event?: MouseEvent) => {
|
||||||
|
this.select(idOrEl, event);
|
||||||
|
})
|
||||||
|
.on('select', (selectedEl: HTMLElement) => {
|
||||||
|
this.emit('select', selectedEl);
|
||||||
|
})
|
||||||
|
.on('before-multi-select', (idOrElList: HTMLElement[] | Id[]) => {
|
||||||
|
this.multiSelect(idOrElList);
|
||||||
|
})
|
||||||
|
.on('multi-select', (selectedElList: HTMLElement[]) => {
|
||||||
|
this.emit('multi-select', selectedElList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化DragResize类通过ActionManager抛出来的事件监听
|
||||||
|
*/
|
||||||
|
private initDrEvent(): void {
|
||||||
|
this.actionManager
|
||||||
|
.on('update', (data: UpdateEventData) => {
|
||||||
|
this.emit('update', data);
|
||||||
|
})
|
||||||
|
.on('sort', (data: UpdateEventData) => {
|
||||||
|
this.emit('sort', data);
|
||||||
|
})
|
||||||
|
.on('select-parent', () => {
|
||||||
|
this.emit('select-parent');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化MultiDragResize类通过ActionManager抛出来的事件监听
|
||||||
|
*/
|
||||||
|
private initMulDrEvent(): void {
|
||||||
|
this.actionManager
|
||||||
|
// 多选切换到单选
|
||||||
|
.on('change-to-select', (el: HTMLElement) => {
|
||||||
|
this.select(el);
|
||||||
|
// 先保证画布内完成渲染,再通知外部更新
|
||||||
|
setTimeout(() => this.emit('select', el));
|
||||||
|
})
|
||||||
|
.on('multi-update', (data: UpdateEventData, parentEl: HTMLElement | null) => {
|
||||||
|
this.emit('update', { data, parentEl });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化Highlight类通过ActionManager抛出来的事件监听
|
||||||
|
*/
|
||||||
|
private initHighlightEvent(): void {
|
||||||
|
this.actionManager.on('highlight', async (highlightEl: HTMLElement) => {
|
||||||
|
this.emit('highlight', highlightEl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,79 +17,60 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { EventEmitter } from 'events';
|
import Moveable, { MoveableOptions } from 'moveable';
|
||||||
|
|
||||||
import KeyController from 'keycon';
|
|
||||||
import type { MoveableOptions } from 'moveable';
|
|
||||||
import Moveable from 'moveable';
|
|
||||||
import MoveableHelper from 'moveable-helper';
|
import MoveableHelper from 'moveable-helper';
|
||||||
|
|
||||||
import { removeClassNameByClassName } from '@tmagic/utils';
|
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const';
|
||||||
|
import MoveableOptionsManager from './MoveableOptionsManager';
|
||||||
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode, ZIndex } from './const';
|
import TargetShadow from './TargetShadow';
|
||||||
import selectParentAbles from './MoveableSelectParentAble';
|
import type { DelayedMarkContainer, GetRenderDocument, MarkContainerEnd, StageDragResizeConfig } from './types';
|
||||||
import StageCore from './StageCore';
|
import { StageDragStatus } from './types';
|
||||||
import StageMask from './StageMask';
|
import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, up } from './util';
|
||||||
import type { StageDragResizeConfig } from './types';
|
|
||||||
import { ContainerHighlightType, StageDragStatus } from './types';
|
|
||||||
import { calcValueByFontsize, down, getAbsolutePosition, getMode, getOffset, getTargetElStyle, up } from './util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中框
|
* 管理单选操作,响应选中操作,初始化moveableOption参数并初始化moveable,处理moveable回调事件对组件进行更新
|
||||||
|
* @extends MoveableOptionsManager
|
||||||
*/
|
*/
|
||||||
export default class StageDragResize extends EventEmitter {
|
export default class StageDragResize extends MoveableOptionsManager {
|
||||||
public core: StageCore;
|
|
||||||
public mask: StageMask;
|
|
||||||
/** 画布容器 */
|
|
||||||
public container: HTMLElement;
|
|
||||||
/** 目标节点 */
|
/** 目标节点 */
|
||||||
public target?: HTMLElement;
|
private target?: HTMLElement;
|
||||||
/** 目标节点在蒙层中的占位节点 */
|
/** 目标节点在蒙层中的占位节点 */
|
||||||
public dragEl?: HTMLDivElement;
|
private targetShadow: TargetShadow;
|
||||||
/** Moveable拖拽类实例 */
|
/** Moveable拖拽类实例 */
|
||||||
public moveable?: Moveable;
|
private moveable?: Moveable;
|
||||||
/** 水平参考线 */
|
|
||||||
public horizontalGuidelines: number[] = [];
|
|
||||||
/** 垂直参考线 */
|
|
||||||
public verticalGuidelines: number[] = [];
|
|
||||||
/** 对齐元素集合 */
|
|
||||||
public elementGuidelines: HTMLElement[] = [];
|
|
||||||
/** 布局方式:流式布局、绝对定位、固定定位 */
|
|
||||||
public mode: Mode = Mode.ABSOLUTE;
|
|
||||||
|
|
||||||
private moveableOptions: MoveableOptions = {};
|
|
||||||
/** 拖动状态 */
|
/** 拖动状态 */
|
||||||
private dragStatus: StageDragStatus = StageDragStatus.END;
|
private dragStatus: StageDragStatus = StageDragStatus.END;
|
||||||
/** 流式布局下,目标节点的镜像节点 */
|
/** 流式布局下,目标节点的镜像节点 */
|
||||||
private ghostEl: HTMLElement | undefined;
|
private ghostEl: HTMLElement | undefined;
|
||||||
private moveableHelper?: MoveableHelper;
|
private moveableHelper?: MoveableHelper;
|
||||||
private isContainerHighlight: Boolean = false;
|
private getRenderDocument: GetRenderDocument;
|
||||||
|
private markContainerEnd: MarkContainerEnd;
|
||||||
|
private delayedMarkContainer: DelayedMarkContainer;
|
||||||
|
|
||||||
constructor(config: StageDragResizeConfig) {
|
constructor(config: StageDragResizeConfig) {
|
||||||
super();
|
super(config);
|
||||||
|
|
||||||
this.core = config.core;
|
this.getRenderDocument = config.getRenderDocument;
|
||||||
this.container = config.container;
|
this.markContainerEnd = config.markContainerEnd;
|
||||||
this.mask = config.mask;
|
this.delayedMarkContainer = config.delayedMarkContainer;
|
||||||
|
|
||||||
KeyController.global.keydown('alt', (e) => {
|
this.targetShadow = new TargetShadow({
|
||||||
e.inputEvent.preventDefault();
|
container: config.container,
|
||||||
this.isContainerHighlight = true;
|
updateDragEl: config.updateDragEl,
|
||||||
|
zIndex: ZIndex.DRAG_EL,
|
||||||
|
idPrefix: DRAG_EL_ID_PREFIX,
|
||||||
});
|
});
|
||||||
KeyController.global.keyup('alt', (e) => {
|
|
||||||
e.inputEvent.preventDefault();
|
|
||||||
|
|
||||||
const doc = this.core.renderer.contentWindow?.document;
|
this.on('update-moveable', () => {
|
||||||
if (doc && this.canContainerHighlight()) {
|
if (this.moveable) {
|
||||||
removeClassNameByClassName(doc, this.core.containerHighlightClassName);
|
this.updateMoveable();
|
||||||
}
|
}
|
||||||
this.isContainerHighlight = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将选中框渲染并覆盖到选中的组件Dom节点上方
|
* 将选中框渲染并覆盖到选中的组件Dom节点上方
|
||||||
* 当选中的节点是不是absolute时,会创建一个新的节点出来作为拖拽目标
|
* 当选中的节点不是absolute时,会创建一个新的节点出来作为拖拽目标
|
||||||
* @param el 选中组件的Dom节点元素
|
* @param el 选中组件的Dom节点元素
|
||||||
* @param event 鼠标事件
|
* @param event 鼠标事件
|
||||||
*/
|
*/
|
||||||
@ -97,23 +78,11 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
const oldTarget = this.target;
|
const oldTarget = this.target;
|
||||||
this.target = el;
|
this.target = el;
|
||||||
|
|
||||||
if (!this.dragEl) {
|
|
||||||
this.dragEl = globalThis.document.createElement('div');
|
|
||||||
this.container.append(this.dragEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从不能拖动到能拖动的节点之间切换,要重新创建moveable,不然dragStart不生效
|
// 从不能拖动到能拖动的节点之间切换,要重新创建moveable,不然dragStart不生效
|
||||||
if (!this.moveable || this.target !== oldTarget) {
|
if (!this.moveable || this.target !== oldTarget) {
|
||||||
this.init(el);
|
this.initMoveable(el);
|
||||||
this.moveableHelper = MoveableHelper.create({
|
|
||||||
useBeforeRender: true,
|
|
||||||
useRender: false,
|
|
||||||
createAuto: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initMoveable();
|
|
||||||
} else {
|
} else {
|
||||||
this.updateMoveable();
|
this.updateMoveable(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
@ -125,120 +94,70 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
* 初始化选中框并渲染出来
|
* 初始化选中框并渲染出来
|
||||||
*/
|
*/
|
||||||
public updateMoveable(el = this.target): void {
|
public updateMoveable(el = this.target): void {
|
||||||
if (!this.moveable) throw new Error('未初始化moveable');
|
if (!this.moveable) return;
|
||||||
if (!el) throw new Error('未选中任何节点');
|
if (!el) throw new Error('未选中任何节点');
|
||||||
|
|
||||||
this.target = el;
|
this.target = el;
|
||||||
|
|
||||||
this.init(el);
|
const options: MoveableOptions = this.init(el);
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.moveable) {
|
|
||||||
this.updateMoveable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearGuides() {
|
|
||||||
this.horizontalGuidelines = [];
|
|
||||||
this.verticalGuidelines = [];
|
|
||||||
this.moveableOptions.horizontalGuidelines = [];
|
|
||||||
this.moveableOptions.verticalGuidelines = [];
|
|
||||||
this.updateMoveable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearSelectStatus(): void {
|
public clearSelectStatus(): void {
|
||||||
if (!this.moveable) return;
|
if (!this.moveable) return;
|
||||||
this.destroyDragEl();
|
this.targetShadow.destroyEl();
|
||||||
this.dragEl = undefined;
|
|
||||||
this.moveable.target = null;
|
this.moveable.target = null;
|
||||||
this.moveable.updateTarget();
|
this.moveable.updateTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroyDragEl(): void {
|
|
||||||
this.dragEl?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.moveable?.destroy();
|
this.moveable?.destroy();
|
||||||
this.destroyGhostEl();
|
this.destroyGhostEl();
|
||||||
this.destroyDragEl();
|
this.targetShadow.destroy();
|
||||||
this.dragStatus = StageDragStatus.END;
|
this.dragStatus = StageDragStatus.END;
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(el: HTMLElement): void {
|
private init(el: HTMLElement): MoveableOptions {
|
||||||
// 如果有滚动条会导致resize时获取到width,height不准确
|
// 如果有滚动条会导致resize时获取到width,height不准确
|
||||||
if (/(auto|scroll)/.test(el.style.overflow)) {
|
if (/(auto|scroll)/.test(el.style.overflow)) {
|
||||||
el.style.overflow = 'hidden';
|
el.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mode = getMode(el);
|
this.mode = getMode(el);
|
||||||
|
|
||||||
this.destroyGhostEl();
|
this.destroyGhostEl();
|
||||||
|
|
||||||
if (!this.dragEl) {
|
this.targetShadow.update(el);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragEl.style.cssText = getTargetElStyle(el);
|
// 设置选中元素的周围元素,用于选中元素跟周围元素对齐辅助
|
||||||
this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
|
const elementGuidelines: any = this.target?.parentElement?.children || [];
|
||||||
|
this.setElementGuidelines([this.target as HTMLElement], elementGuidelines);
|
||||||
|
|
||||||
if (typeof this.core.config.updateDragEl === 'function') {
|
return this.getOptions(false, {
|
||||||
this.core.config.updateDragEl(this.dragEl, el);
|
target: this.targetShadow.el,
|
||||||
}
|
|
||||||
this.moveableOptions = this.getOptions({
|
|
||||||
target: this.dragEl,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setElementGuidelines(nodes: HTMLElement[]) {
|
private initMoveable(el: HTMLElement) {
|
||||||
this.elementGuidelines.forEach((node) => {
|
const options: MoveableOptions = this.init(el);
|
||||||
node.remove();
|
this.moveableHelper = MoveableHelper.create({
|
||||||
|
useBeforeRender: true,
|
||||||
|
useRender: false,
|
||||||
|
createAuto: true,
|
||||||
});
|
});
|
||||||
this.elementGuidelines = [];
|
|
||||||
|
|
||||||
if (this.mode === Mode.ABSOLUTE) {
|
|
||||||
this.container.append(this.createGuidelineElements(nodes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createGuidelineElements(nodes: HTMLElement[]) {
|
|
||||||
const frame = globalThis.document.createDocumentFragment();
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
const { width, height } = node.getBoundingClientRect();
|
|
||||||
if (node === this.target) continue;
|
|
||||||
const { left, top } = getOffset(node as HTMLElement);
|
|
||||||
const elementGuideline = globalThis.document.createElement('div');
|
|
||||||
elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
|
|
||||||
this.elementGuidelines.push(elementGuideline);
|
|
||||||
frame.append(elementGuideline);
|
|
||||||
}
|
|
||||||
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initMoveable() {
|
|
||||||
this.moveable?.destroy();
|
this.moveable?.destroy();
|
||||||
|
|
||||||
this.moveable = new Moveable(this.container, {
|
this.moveable = new Moveable(this.container, {
|
||||||
...this.moveableOptions,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bindResizeEvent();
|
this.bindResizeEvent();
|
||||||
@ -267,7 +186,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
})
|
})
|
||||||
.on('resize', (e) => {
|
.on('resize', (e) => {
|
||||||
const { width, height, drag } = e;
|
const { width, height, drag } = e;
|
||||||
if (!this.moveable || !this.target || !this.dragEl) return;
|
if (!this.moveable || !this.target || !this.targetShadow.el) return;
|
||||||
|
|
||||||
const { beforeTranslate } = drag;
|
const { beforeTranslate } = drag;
|
||||||
this.dragStatus = StageDragStatus.ING;
|
this.dragStatus = StageDragStatus.ING;
|
||||||
@ -275,8 +194,8 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
// 流式布局
|
// 流式布局
|
||||||
if (this.mode === Mode.SORTABLE) {
|
if (this.mode === Mode.SORTABLE) {
|
||||||
this.target.style.top = '0px';
|
this.target.style.top = '0px';
|
||||||
this.dragEl.style.width = `${width}px`;
|
this.targetShadow.el.style.width = `${width}px`;
|
||||||
this.dragEl.style.height = `${height}px`;
|
this.targetShadow.el.style.height = `${height}px`;
|
||||||
} else {
|
} else {
|
||||||
this.moveableHelper?.onResize(e);
|
this.moveableHelper?.onResize(e);
|
||||||
this.target.style.left = `${frame.left + beforeTranslate[0]}px`;
|
this.target.style.left = `${frame.left + beforeTranslate[0]}px`;
|
||||||
@ -302,9 +221,6 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
|
|
||||||
let timeout: NodeJS.Timeout | undefined;
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
const { contentWindow } = this.core.renderer;
|
|
||||||
const doc = contentWindow?.document;
|
|
||||||
|
|
||||||
this.moveable
|
this.moveable
|
||||||
.on('dragStart', (e) => {
|
.on('dragStart', (e) => {
|
||||||
if (!this.target) throw new Error('未选中组件');
|
if (!this.target) throw new Error('未选中组件');
|
||||||
@ -321,16 +237,13 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
frame.left = this.target.offsetLeft;
|
frame.left = this.target.offsetLeft;
|
||||||
})
|
})
|
||||||
.on('drag', (e) => {
|
.on('drag', (e) => {
|
||||||
if (!this.target || !this.dragEl) return;
|
if (!this.target || !this.targetShadow.el) return;
|
||||||
|
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
globalThis.clearTimeout(timeout);
|
globalThis.clearTimeout(timeout);
|
||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
}
|
}
|
||||||
|
timeout = this.delayedMarkContainer(e.inputEvent, [this.target]);
|
||||||
if (this.canContainerHighlight()) {
|
|
||||||
timeout = this.core.getAddContainerHighlightClassNameTimeout(e.inputEvent, [this.target]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragStatus = StageDragStatus.ING;
|
this.dragStatus = StageDragStatus.ING;
|
||||||
|
|
||||||
@ -351,12 +264,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
timeout = undefined;
|
timeout = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentEl: HTMLElement | null = null;
|
const parentEl = this.markContainerEnd();
|
||||||
|
|
||||||
if (doc && this.canContainerHighlight()) {
|
|
||||||
parentEl = removeClassNameByClassName(doc, this.core.containerHighlightClassName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
|
// 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
|
||||||
if (this.dragStatus === StageDragStatus.ING) {
|
if (this.dragStatus === StageDragStatus.ING) {
|
||||||
if (parentEl) {
|
if (parentEl) {
|
||||||
@ -386,7 +294,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.moveableHelper?.onRotateStart(e);
|
this.moveableHelper?.onRotateStart(e);
|
||||||
})
|
})
|
||||||
.on('rotate', (e) => {
|
.on('rotate', (e) => {
|
||||||
if (!this.target || !this.dragEl) return;
|
if (!this.target || !this.targetShadow.el) return;
|
||||||
this.dragStatus = StageDragStatus.ING;
|
this.dragStatus = StageDragStatus.ING;
|
||||||
this.moveableHelper?.onRotate(e);
|
this.moveableHelper?.onRotate(e);
|
||||||
const frame = this.moveableHelper?.getFrame(e.target);
|
const frame = this.moveableHelper?.getFrame(e.target);
|
||||||
@ -417,7 +325,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.moveableHelper?.onScaleStart(e);
|
this.moveableHelper?.onScaleStart(e);
|
||||||
})
|
})
|
||||||
.on('scale', (e) => {
|
.on('scale', (e) => {
|
||||||
if (!this.target || !this.dragEl) return;
|
if (!this.target || !this.targetShadow.el) return;
|
||||||
this.dragStatus = StageDragStatus.ING;
|
this.dragStatus = StageDragStatus.ING;
|
||||||
this.moveableHelper?.onScale(e);
|
this.moveableHelper?.onScale(e);
|
||||||
const frame = this.moveableHelper?.getFrame(e.target);
|
const frame = this.moveableHelper?.getFrame(e.target);
|
||||||
@ -461,8 +369,7 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
private update(isResize = false, parentEl: HTMLElement | null = null): void {
|
private update(isResize = false, parentEl: HTMLElement | null = null): void {
|
||||||
if (!this.target) return;
|
if (!this.target) return;
|
||||||
|
|
||||||
const { contentWindow } = this.core.renderer;
|
const doc = this.getRenderDocument();
|
||||||
const doc = contentWindow?.document;
|
|
||||||
|
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
@ -474,15 +381,24 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
const width = calcValueByFontsize(doc, this.target.clientWidth);
|
const width = calcValueByFontsize(doc, this.target.clientWidth);
|
||||||
const height = calcValueByFontsize(doc, this.target.clientHeight);
|
const height = calcValueByFontsize(doc, this.target.clientHeight);
|
||||||
|
|
||||||
if (parentEl && this.mode === Mode.ABSOLUTE && this.dragEl) {
|
if (parentEl && this.mode === Mode.ABSOLUTE && this.targetShadow.el) {
|
||||||
const [translateX, translateY] = this.moveableHelper?.getFrame(this.dragEl).properties.transform.translate.value;
|
const targetShadowHtmlEl = this.targetShadow.el as HTMLElement;
|
||||||
|
const targetShadowElOffsetLeft = targetShadowHtmlEl.offsetLeft || 0;
|
||||||
|
const targetShadowElOffsetTop = targetShadowHtmlEl.offsetTop || 0;
|
||||||
|
|
||||||
|
const frame = this.moveableHelper?.getFrame(this.targetShadow.el);
|
||||||
|
|
||||||
|
const [translateX, translateY] = frame?.properties.transform.translate.value;
|
||||||
const { left: parentLeft, top: parentTop } = getOffset(parentEl);
|
const { left: parentLeft, top: parentTop } = getOffset(parentEl);
|
||||||
|
|
||||||
left =
|
left =
|
||||||
calcValueByFontsize(doc, this.dragEl.offsetLeft) +
|
calcValueByFontsize(doc, targetShadowElOffsetLeft) +
|
||||||
parseFloat(translateX) -
|
parseFloat(translateX) -
|
||||||
calcValueByFontsize(doc, parentLeft);
|
calcValueByFontsize(doc, parentLeft);
|
||||||
top =
|
top =
|
||||||
calcValueByFontsize(doc, this.dragEl.offsetTop) + parseFloat(translateY) - calcValueByFontsize(doc, parentTop);
|
calcValueByFontsize(doc, targetShadowElOffsetTop) +
|
||||||
|
parseFloat(translateY) -
|
||||||
|
calcValueByFontsize(doc, parentTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('update', {
|
this.emit('update', {
|
||||||
@ -530,86 +446,4 @@ export default class StageDragResize extends EventEmitter {
|
|||||||
this.ghostEl?.remove();
|
this.ghostEl?.remove();
|
||||||
this.ghostEl = undefined;
|
this.ghostEl = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
|
||||||
if (!this.target) return {};
|
|
||||||
|
|
||||||
const isAbsolute = this.mode === Mode.ABSOLUTE;
|
|
||||||
const isFixed = this.mode === Mode.FIXED;
|
|
||||||
const isSortable = this.mode === Mode.SORTABLE;
|
|
||||||
|
|
||||||
let { moveableOptions = {} } = this.core.config;
|
|
||||||
|
|
||||||
if (typeof moveableOptions === 'function') {
|
|
||||||
moveableOptions = moveableOptions(this.core);
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementGuidelines: any = moveableOptions.elementGuidelines || this.target.parentElement?.children || [];
|
|
||||||
|
|
||||||
this.setElementGuidelines(elementGuidelines);
|
|
||||||
|
|
||||||
if (moveableOptions.elementGuidelines) {
|
|
||||||
delete moveableOptions.elementGuidelines;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
origin: false,
|
|
||||||
rootContainer: this.core.container,
|
|
||||||
zoom: 1,
|
|
||||||
dragArea: false,
|
|
||||||
draggable: true,
|
|
||||||
resizable: true,
|
|
||||||
scalable: false,
|
|
||||||
rotatable: false,
|
|
||||||
snappable: true,
|
|
||||||
snapGap: isAbsolute || isFixed,
|
|
||||||
snapThreshold: 5,
|
|
||||||
snapDigit: 0,
|
|
||||||
throttleDrag: 0,
|
|
||||||
isDisplaySnapDigit: isAbsolute,
|
|
||||||
snapDirections: {
|
|
||||||
top: isAbsolute,
|
|
||||||
right: isAbsolute,
|
|
||||||
bottom: isAbsolute,
|
|
||||||
left: isAbsolute,
|
|
||||||
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,
|
|
||||||
// 设置0的话无法移动到left为0,所以只能设置为-1
|
|
||||||
left: -1,
|
|
||||||
right: this.container.clientWidth - 1,
|
|
||||||
bottom: isSortable ? undefined : this.container.clientHeight,
|
|
||||||
...(moveableOptions.bounds || {}),
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
selectParent: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
ables: [selectParentAbles(this)],
|
|
||||||
|
|
||||||
...options,
|
|
||||||
...moveableOptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private canContainerHighlight() {
|
|
||||||
return (
|
|
||||||
this.core.containerHighlightType === ContainerHighlightType.DEFAULT ||
|
|
||||||
(this.core.containerHighlightType === ContainerHighlightType.ALT && this.isContainerHighlight)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,27 +20,28 @@ import { EventEmitter } from 'events';
|
|||||||
|
|
||||||
import Moveable from 'moveable';
|
import Moveable from 'moveable';
|
||||||
|
|
||||||
import { HIGHLIGHT_EL_ID_PREFIX } from './const';
|
import { HIGHLIGHT_EL_ID_PREFIX, ZIndex } from './const';
|
||||||
import StageCore from './StageCore';
|
import TargetShadow from './TargetShadow';
|
||||||
import TargetCalibrate from './TargetCalibrate';
|
import type { GetRootContainer, StageHighlightConfig } from './types';
|
||||||
import type { StageHighlightConfig } from './types';
|
|
||||||
export default class StageHighlight extends EventEmitter {
|
export default class StageHighlight extends EventEmitter {
|
||||||
public core: StageCore;
|
|
||||||
public container: HTMLElement;
|
public container: HTMLElement;
|
||||||
public target?: HTMLElement;
|
public target?: HTMLElement;
|
||||||
public moveable?: Moveable;
|
public moveable?: Moveable;
|
||||||
public calibrationTarget: TargetCalibrate;
|
public targetShadow: TargetShadow;
|
||||||
|
private getRootContainer: GetRootContainer;
|
||||||
|
|
||||||
constructor(config: StageHighlightConfig) {
|
constructor(config: StageHighlightConfig) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.core = config.core;
|
|
||||||
this.container = config.container;
|
this.container = config.container;
|
||||||
this.calibrationTarget = new TargetCalibrate({
|
this.getRootContainer = config.getRootContainer;
|
||||||
parent: this.core.mask.content,
|
|
||||||
mask: this.core.mask,
|
this.targetShadow = new TargetShadow({
|
||||||
dr: this.core.dr,
|
container: config.container,
|
||||||
core: this.core,
|
updateDragEl: config.updateDragEl,
|
||||||
|
zIndex: ZIndex.HIGHLIGHT_EL,
|
||||||
|
idPrefix: HIGHLIGHT_EL_ID_PREFIX,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +55,9 @@ export default class StageHighlight extends EventEmitter {
|
|||||||
this.moveable?.destroy();
|
this.moveable?.destroy();
|
||||||
|
|
||||||
this.moveable = new Moveable(this.container, {
|
this.moveable = new Moveable(this.container, {
|
||||||
target: this.calibrationTarget.update(el, HIGHLIGHT_EL_ID_PREFIX),
|
target: this.targetShadow.update(el),
|
||||||
origin: false,
|
origin: false,
|
||||||
rootContainer: this.core.container,
|
rootContainer: this.getRootContainer(),
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -65,7 +66,7 @@ export default class StageHighlight extends EventEmitter {
|
|||||||
* 清空高亮
|
* 清空高亮
|
||||||
*/
|
*/
|
||||||
public clearHighlight(): void {
|
public clearHighlight(): void {
|
||||||
if (!this.moveable) return;
|
if (!this.moveable || !this.target) return;
|
||||||
this.target = undefined;
|
this.target = undefined;
|
||||||
this.moveable.target = null;
|
this.moveable.target = null;
|
||||||
this.moveable.updateTarget();
|
this.moveable.updateTarget();
|
||||||
@ -76,6 +77,6 @@ export default class StageHighlight extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.moveable?.destroy();
|
this.moveable?.destroy();
|
||||||
this.calibrationTarget.destroy();
|
this.targetShadow.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,20 +16,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import KeyController from 'keycon';
|
import { createDiv, getDocument, injectStyle } from '@tmagic/utils';
|
||||||
import { throttle } from 'lodash-es';
|
|
||||||
|
|
||||||
import { createDiv, injectStyle } from '@tmagic/utils';
|
import { Mode, ZIndex } from './const';
|
||||||
|
|
||||||
import { Mode, MouseButton, ZIndex } from './const';
|
|
||||||
import Rule from './Rule';
|
import Rule from './Rule';
|
||||||
import { getScrollParent, isFixedParent, isMoveableButton } from './util';
|
import { getScrollParent, isFixedParent } from './util';
|
||||||
|
|
||||||
const wrapperClassName = 'editor-mask-wrapper';
|
const wrapperClassName = 'editor-mask-wrapper';
|
||||||
const throttleTime = 100;
|
|
||||||
|
|
||||||
const hideScrollbar = () => {
|
const hideScrollbar = () => {
|
||||||
injectStyle(globalThis.document, `.${wrapperClassName}::-webkit-scrollbar { width: 0 !important; display: none }`);
|
injectStyle(getDocument(), `.${wrapperClassName}::-webkit-scrollbar { width: 0 !important; display: none }`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createContent = (): HTMLDivElement =>
|
const createContent = (): HTMLDivElement =>
|
||||||
@ -78,36 +74,28 @@ export default class StageMask extends Rule {
|
|||||||
public wrapperWidth = 0;
|
public wrapperWidth = 0;
|
||||||
public maxScrollTop = 0;
|
public maxScrollTop = 0;
|
||||||
public maxScrollLeft = 0;
|
public maxScrollLeft = 0;
|
||||||
public isMultiSelectStatus: Boolean = false;
|
|
||||||
|
|
||||||
private mode: Mode = Mode.ABSOLUTE;
|
private mode: Mode = Mode.ABSOLUTE;
|
||||||
private pageScrollParent: HTMLElement | null = null;
|
private pageScrollParent: HTMLElement | null = null;
|
||||||
private intersectionObserver: IntersectionObserver | null = null;
|
private intersectionObserver: IntersectionObserver | null = null;
|
||||||
private wrapperResizeObserver: ResizeObserver | null = null;
|
private wrapperResizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 高亮事件处理函数
|
|
||||||
* @param event 事件对象
|
|
||||||
*/
|
|
||||||
private highlightHandler = throttle((event: MouseEvent): void => {
|
|
||||||
this.emit('highlight', event);
|
|
||||||
}, throttleTime);
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
super(wrapper);
|
super(wrapper);
|
||||||
|
|
||||||
this.wrapper = wrapper;
|
this.wrapper = wrapper;
|
||||||
|
|
||||||
this.initContentEventListener();
|
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
||||||
this.wrapper.appendChild(this.content);
|
this.wrapper.appendChild(this.content);
|
||||||
|
|
||||||
this.initMultiSelectEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMode(mode: Mode) {
|
public setMode(mode: Mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.scroll();
|
this.scroll();
|
||||||
|
|
||||||
|
this.content.dataset.mode = mode;
|
||||||
|
|
||||||
if (mode === Mode.FIXED) {
|
if (mode === Mode.FIXED) {
|
||||||
this.content.style.width = `${this.wrapperWidth}px`;
|
this.content.style.width = `${this.wrapperWidth}px`;
|
||||||
this.content.style.height = `${this.wrapperHeight}px`;
|
this.content.style.height = `${this.wrapperHeight}px`;
|
||||||
@ -182,42 +170,9 @@ export default class StageMask extends Rule {
|
|||||||
this.pageScrollParent = null;
|
this.pageScrollParent = null;
|
||||||
this.wrapperResizeObserver?.disconnect();
|
this.wrapperResizeObserver?.disconnect();
|
||||||
|
|
||||||
this.content.removeEventListener('mouseleave', this.mouseLeaveHandler);
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化content的事件监听
|
|
||||||
*/
|
|
||||||
private initContentEventListener(): void {
|
|
||||||
this.content.addEventListener('mousedown', this.mouseDownHandler);
|
|
||||||
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
|
||||||
this.content.addEventListener('mousemove', this.highlightHandler);
|
|
||||||
this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化多选事件监听
|
|
||||||
*/
|
|
||||||
private initMultiSelectEvent(): void {
|
|
||||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
|
||||||
|
|
||||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
|
||||||
|
|
||||||
KeyController.global.keydown(ctrl, (e) => {
|
|
||||||
e.inputEvent.preventDefault();
|
|
||||||
this.isMultiSelectStatus = true;
|
|
||||||
});
|
|
||||||
// ctrl+tab切到其他窗口,需要将多选状态置为false
|
|
||||||
KeyController.global.on('blur', () => {
|
|
||||||
this.isMultiSelectStatus = false;
|
|
||||||
});
|
|
||||||
KeyController.global.keyup(ctrl, (e) => {
|
|
||||||
e.inputEvent.preventDefault();
|
|
||||||
this.isMultiSelectStatus = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听选中元素是否在画布可视区域内,如果目标元素不在可视区域内,通过滚动使该元素出现在可视区域
|
* 监听选中元素是否在画布可视区域内,如果目标元素不在可视区域内,通过滚动使该元素出现在可视区域
|
||||||
*/
|
*/
|
||||||
@ -286,6 +241,17 @@ export default class StageMask extends Rule {
|
|||||||
|
|
||||||
private scrollTo(scrollLeft: number, scrollTop: number): void {
|
private scrollTo(scrollLeft: number, scrollTop: number): void {
|
||||||
this.content.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
|
this.content.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
|
||||||
|
|
||||||
|
const event = new CustomEvent<{
|
||||||
|
scrollLeft: number;
|
||||||
|
scrollTop: number;
|
||||||
|
}>('customScroll', {
|
||||||
|
detail: {
|
||||||
|
scrollLeft: this.scrollLeft,
|
||||||
|
scrollTop: this.scrollTop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.content.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -333,51 +299,7 @@ export default class StageMask extends Rule {
|
|||||||
if (this.maxScrollLeft < this.scrollLeft) this.scrollLeft = this.maxScrollLeft;
|
if (this.maxScrollLeft < this.scrollLeft) this.scrollLeft = this.maxScrollLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 点击事件处理函数
|
|
||||||
* @param event 事件对象
|
|
||||||
*/
|
|
||||||
private mouseDownHandler = (event: MouseEvent): void => {
|
|
||||||
this.emit('clearHighlight');
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
|
|
||||||
if (!event.target) return;
|
|
||||||
|
|
||||||
const targetClassList = (event.target as HTMLDivElement).classList;
|
|
||||||
|
|
||||||
// 如果单击多选选中区域,则不需要再触发选中了,而可能是拖动行为
|
|
||||||
if (!this.isMultiSelectStatus && targetClassList.contains('moveable-area')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 点击对象如果是边框锚点,则可能是resize; 点击对象是功能按钮
|
|
||||||
if (targetClassList.contains('moveable-control') || isMoveableButton(event.target as Element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.content.removeEventListener('mousemove', this.highlightHandler);
|
|
||||||
|
|
||||||
// 判断触发多选还是单选
|
|
||||||
if (this.isMultiSelectStatus) {
|
|
||||||
this.emit('beforeMultiSelect', event);
|
|
||||||
} else {
|
|
||||||
this.emit('beforeSelect', event);
|
|
||||||
}
|
|
||||||
// 如果是右键点击,这里的mouseup事件监听没有效果
|
|
||||||
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
private mouseUpHandler = (): void => {
|
|
||||||
globalThis.document.removeEventListener('mouseup', this.mouseUpHandler);
|
|
||||||
this.content.addEventListener('mousemove', this.highlightHandler);
|
|
||||||
if (!this.isMultiSelectStatus) {
|
|
||||||
this.emit('select');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private mouseWheelHandler = (event: WheelEvent) => {
|
private mouseWheelHandler = (event: WheelEvent) => {
|
||||||
this.emit('clearHighlight');
|
|
||||||
if (!this.page) throw new Error('page 未初始化');
|
if (!this.page) throw new Error('page 未初始化');
|
||||||
|
|
||||||
const { deltaY, deltaX } = event;
|
const { deltaY, deltaX } = event;
|
||||||
@ -394,11 +316,6 @@ export default class StageMask extends Rule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scroll();
|
this.scroll();
|
||||||
|
|
||||||
this.emit('scroll', event);
|
this.emit('scroll', event);
|
||||||
};
|
};
|
||||||
|
|
||||||
private mouseLeaveHandler = () => {
|
|
||||||
setTimeout(() => this.emit('clearHighlight'), throttleTime);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -16,39 +16,53 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import type { OnDragStart, OnResizeStart } from 'moveable';
|
||||||
|
|
||||||
import type { MoveableOptions, OnDragStart, OnResizeStart } from 'moveable';
|
|
||||||
import Moveable from 'moveable';
|
import Moveable from 'moveable';
|
||||||
import MoveableHelper from 'moveable-helper';
|
import MoveableHelper from 'moveable-helper';
|
||||||
|
|
||||||
import { DRAG_EL_ID_PREFIX, PAGE_CLASS } from './const';
|
import { DRAG_EL_ID_PREFIX, Mode, ZIndex } from './const';
|
||||||
import StageCore from './StageCore';
|
import MoveableOptionsManager from './MoveableOptionsManager';
|
||||||
import StageMask from './StageMask';
|
import TargetShadow from './TargetShadow';
|
||||||
import { StageDragResizeConfig, StageDragStatus } from './types';
|
import { GetRenderDocument, MoveableOptionsManagerConfig, StageDragStatus, StageMultiDragResizeConfig } from './types';
|
||||||
import { calcValueByFontsize, getMode, getTargetElStyle } from './util';
|
import { calcValueByFontsize, getMode } from './util';
|
||||||
|
|
||||||
export default class StageMultiDragResize extends EventEmitter {
|
export default class StageMultiDragResize extends MoveableOptionsManager {
|
||||||
public core: StageCore;
|
|
||||||
public mask: StageMask;
|
|
||||||
/** 画布容器 */
|
/** 画布容器 */
|
||||||
public container: HTMLElement;
|
public container: HTMLElement;
|
||||||
/** 多选:目标节点组 */
|
/** 多选:目标节点组 */
|
||||||
public targetList: HTMLElement[] = [];
|
public targetList: HTMLElement[] = [];
|
||||||
/** 多选:目标节点在蒙层中的占位节点组 */
|
/** 多选:目标节点在蒙层中的占位节点组 */
|
||||||
public dragElList: HTMLDivElement[] = [];
|
public targetShadow: TargetShadow;
|
||||||
/** Moveable多选拖拽类实例 */
|
/** Moveable多选拖拽类实例 */
|
||||||
public moveableForMulti?: Moveable;
|
public moveableForMulti?: Moveable;
|
||||||
/** 拖动状态 */
|
/** 拖动状态 */
|
||||||
public dragStatus: StageDragStatus = StageDragStatus.END;
|
public dragStatus: StageDragStatus = StageDragStatus.END;
|
||||||
private multiMoveableHelper?: MoveableHelper;
|
private multiMoveableHelper?: MoveableHelper;
|
||||||
|
private getRenderDocument: GetRenderDocument;
|
||||||
|
|
||||||
constructor(config: StageDragResizeConfig) {
|
constructor(config: StageMultiDragResizeConfig) {
|
||||||
super();
|
const moveableOptionsManagerConfig: MoveableOptionsManagerConfig = {
|
||||||
|
container: config.container,
|
||||||
|
moveableOptions: config.multiMoveableOptions,
|
||||||
|
getRootContainer: config.getRootContainer,
|
||||||
|
};
|
||||||
|
super(moveableOptionsManagerConfig);
|
||||||
|
|
||||||
this.core = config.core;
|
|
||||||
this.container = config.container;
|
this.container = config.container;
|
||||||
this.mask = config.mask;
|
this.getRenderDocument = config.getRenderDocument;
|
||||||
|
|
||||||
|
this.targetShadow = new TargetShadow({
|
||||||
|
container: config.container,
|
||||||
|
updateDragEl: config.updateDragEl,
|
||||||
|
zIndex: ZIndex.DRAG_EL,
|
||||||
|
idPrefix: DRAG_EL_ID_PREFIX,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on('update-moveable', () => {
|
||||||
|
if (this.moveableForMulti) {
|
||||||
|
this.updateMoveable();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,27 +70,24 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
* @param els
|
* @param els
|
||||||
*/
|
*/
|
||||||
public multiSelect(els: HTMLElement[]): void {
|
public multiSelect(els: HTMLElement[]): void {
|
||||||
this.targetList = els;
|
if (els.length === 0) {
|
||||||
this.core.dr.destroyDragEl();
|
return;
|
||||||
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.mode = getMode(els[0]);
|
||||||
});
|
this.targetList = els;
|
||||||
|
|
||||||
|
this.targetShadow.updateGroup(els);
|
||||||
|
|
||||||
|
// 设置周围元素,用于选中元素跟周围元素的对齐辅助
|
||||||
|
const elementGuidelines: any = this.targetList[0].parentElement?.children || [];
|
||||||
|
this.setElementGuidelines(this.targetList, elementGuidelines);
|
||||||
|
|
||||||
this.moveableForMulti?.destroy();
|
this.moveableForMulti?.destroy();
|
||||||
this.multiMoveableHelper?.clear();
|
this.multiMoveableHelper?.clear();
|
||||||
this.moveableForMulti = new Moveable(
|
this.moveableForMulti = new Moveable(
|
||||||
this.container,
|
this.container,
|
||||||
this.getOptions({
|
this.getOptions(true, {
|
||||||
target: this.dragElList,
|
target: this.targetShadow.els,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.multiMoveableHelper = MoveableHelper.create({
|
this.multiMoveableHelper = MoveableHelper.create({
|
||||||
@ -177,47 +188,58 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
})
|
})
|
||||||
.on('clickGroup', (params) => {
|
.on('clickGroup', (params) => {
|
||||||
const { inputTarget, targets } = params;
|
const { inputTarget, targets } = params;
|
||||||
// 如果此时mask不处于多选状态下,且有多个元素被选中,同时点击的元素在选中元素中的其中一项,代表多选态切换为该元素的单选态
|
// 如果有多个元素被选中,同时点击的元素在选中元素中的其中一项,可能是多选态切换为该元素的单选态,抛事件给上一层继续判断是否切换
|
||||||
if (!this.mask.isMultiSelectStatus && targets.length > 1 && targets.includes(inputTarget)) {
|
if (targets.length > 1 && targets.includes(inputTarget)) {
|
||||||
this.emit('select', inputTarget.id.replace(DRAG_EL_ID_PREFIX, ''));
|
this.emit('change-to-select', inputTarget.id.replace(DRAG_EL_ID_PREFIX, ''));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public canSelect(el: HTMLElement, stop: () => boolean): Boolean {
|
public canSelect(el: HTMLElement, selectedEl: HTMLElement | undefined): boolean {
|
||||||
// 多选状态下不可以选中magic-ui-page,并停止继续向上层选中
|
const currentTargetMode = getMode(el);
|
||||||
if (el.className.includes(PAGE_CLASS)) {
|
let selectedElMode = '';
|
||||||
this.core.highlightedDom = undefined;
|
|
||||||
this.core.highlightLayer.clearHighlight();
|
// 流式布局不支持多选
|
||||||
stop();
|
if (currentTargetMode === Mode.SORTABLE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const currentTargetMode = getMode(el);
|
|
||||||
let selectedDomMode = '';
|
if (this.targetList.length === 0 && selectedEl) {
|
||||||
if (this.core.selectedDom?.className.includes(PAGE_CLASS)) {
|
|
||||||
// 先单击选中了页面(magic-ui-page),再按住多选键多选时,任一元素均可选中
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.targetList.length === 0 && this.core.selectedDom) {
|
|
||||||
// 单选后添加到多选的情况
|
// 单选后添加到多选的情况
|
||||||
selectedDomMode = getMode(this.core.selectedDom);
|
selectedElMode = getMode(selectedEl);
|
||||||
} else if (this.targetList.length > 0) {
|
} else if (this.targetList.length > 0) {
|
||||||
// 已加入多选列表的布局模式是一样的,取第一个判断
|
// 已加入多选列表的布局模式是一样的,取第一个判断
|
||||||
selectedDomMode = getMode(this.targetList[0]);
|
selectedElMode = getMode(this.targetList[0]);
|
||||||
}
|
}
|
||||||
// 定位模式不同,不可混选
|
// 定位模式不同,不可混选
|
||||||
if (currentTargetMode !== selectedDomMode) {
|
if (currentTargetMode !== selectedElMode) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateMoveable(eleList = this.targetList) {
|
||||||
|
if (!this.moveableForMulti) return;
|
||||||
|
if (!eleList) throw new Error('未选中任何节点');
|
||||||
|
|
||||||
|
this.targetList = eleList;
|
||||||
|
|
||||||
|
const options = this.getOptions(true, {
|
||||||
|
target: this.targetShadow.els,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(options).forEach(([key, value]) => {
|
||||||
|
(this.moveableForMulti as any)[key] = value;
|
||||||
|
});
|
||||||
|
this.moveableForMulti.updateTarget();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除多选状态
|
* 清除多选状态
|
||||||
*/
|
*/
|
||||||
public clearSelectStatus(): void {
|
public clearSelectStatus(): void {
|
||||||
if (!this.moveableForMulti) return;
|
if (!this.moveableForMulti) return;
|
||||||
this.destroyDragElList();
|
this.targetShadow.destroyEls();
|
||||||
this.moveableForMulti.target = null;
|
this.moveableForMulti.target = null;
|
||||||
this.moveableForMulti.updateTarget();
|
this.moveableForMulti.updateTarget();
|
||||||
this.targetList = [];
|
this.targetList = [];
|
||||||
@ -228,14 +250,7 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.moveableForMulti?.destroy();
|
this.moveableForMulti?.destroy();
|
||||||
this.destroyDragElList();
|
this.targetShadow.destroy();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除蒙层占位节点
|
|
||||||
*/
|
|
||||||
public destroyDragElList(): void {
|
|
||||||
this.dragElList.forEach((dragElItem) => dragElItem?.remove());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,60 +260,19 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
private update(isResize = false): void {
|
private update(isResize = false): void {
|
||||||
if (this.targetList.length === 0) return;
|
if (this.targetList.length === 0) return;
|
||||||
|
|
||||||
const { contentWindow } = this.core.renderer;
|
const doc = this.getRenderDocument();
|
||||||
const doc = contentWindow?.document;
|
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
this.emit('update', {
|
const data = this.targetList.map((targetItem) => {
|
||||||
data: this.targetList.map((targetItem) => {
|
const left = calcValueByFontsize(doc, targetItem.offsetLeft);
|
||||||
const offset = { left: targetItem.offsetLeft, top: targetItem.offsetTop };
|
const top = calcValueByFontsize(doc, targetItem.offsetTop);
|
||||||
const left = calcValueByFontsize(doc, offset.left);
|
|
||||||
const top = calcValueByFontsize(doc, offset.top);
|
|
||||||
const width = calcValueByFontsize(doc, targetItem.clientWidth);
|
const width = calcValueByFontsize(doc, targetItem.clientWidth);
|
||||||
const height = calcValueByFontsize(doc, targetItem.clientHeight);
|
const height = calcValueByFontsize(doc, targetItem.clientHeight);
|
||||||
return {
|
return {
|
||||||
el: targetItem,
|
el: targetItem,
|
||||||
style: isResize ? { left, top, width, height } : { left, top },
|
style: isResize ? { left, top, width, height } : { left, top },
|
||||||
};
|
};
|
||||||
}),
|
|
||||||
parentEl: null,
|
|
||||||
});
|
});
|
||||||
}
|
this.emit('update', data, null);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取moveable options参数
|
|
||||||
* @param {MoveableOptions} options
|
|
||||||
* @return {MoveableOptions} moveable options参数
|
|
||||||
*/
|
|
||||||
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
|
||||||
let { multiMoveableOptions = {} } = this.core.config;
|
|
||||||
|
|
||||||
if (typeof multiMoveableOptions === 'function') {
|
|
||||||
multiMoveableOptions = multiMoveableOptions(this.core);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
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 },
|
|
||||||
snappable: true,
|
|
||||||
bounds: {
|
|
||||||
top: 0,
|
|
||||||
// 设置0的话无法移动到left为0,所以只能设置为-1
|
|
||||||
left: -1,
|
|
||||||
right: this.container.clientWidth - 1,
|
|
||||||
bottom: this.container.clientHeight,
|
|
||||||
...(multiMoveableOptions.bounds || {}),
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
...multiMoveableOptions,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,28 +18,30 @@
|
|||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
import { Id } from '@tmagic/schema';
|
||||||
import { getHost, injectStyle, isSameDomain } from '@tmagic/utils';
|
import { getHost, injectStyle, isSameDomain } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import { DEFAULT_ZOOM } from './const';
|
||||||
import style from './style.css?raw';
|
import style from './style.css?raw';
|
||||||
import type { Runtime, RuntimeWindow, StageRenderConfig } from './types';
|
import type { Point, RemoveData, Runtime, RuntimeWindow, StageRenderConfig, UpdateData } from './types';
|
||||||
|
import { addSelectedClassName, removeSelectedClassName } from './util';
|
||||||
|
|
||||||
export default class StageRender extends EventEmitter {
|
export default class StageRender extends EventEmitter {
|
||||||
/** 组件的js、css执行的环境,直接渲染为当前window,iframe渲染则为iframe.contentWindow */
|
/** 组件的js、css执行的环境,直接渲染为当前window,iframe渲染则为iframe.contentWindow */
|
||||||
public contentWindow: RuntimeWindow | null = null;
|
public contentWindow: RuntimeWindow | null = null;
|
||||||
|
|
||||||
public runtime: Runtime | null = null;
|
public runtime: Runtime | null = null;
|
||||||
|
|
||||||
public iframe?: HTMLIFrameElement;
|
public iframe?: HTMLIFrameElement;
|
||||||
|
|
||||||
public runtimeUrl?: string;
|
private runtimeUrl?: string;
|
||||||
|
private zoom = DEFAULT_ZOOM;
|
||||||
|
private customizedRender?: () => Promise<HTMLElement | null>;
|
||||||
|
|
||||||
private render?: () => Promise<HTMLElement | null>;
|
constructor({ runtimeUrl, zoom, customizedRender }: StageRenderConfig) {
|
||||||
|
|
||||||
constructor({ runtimeUrl, render }: StageRenderConfig) {
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.runtimeUrl = runtimeUrl || '';
|
this.runtimeUrl = runtimeUrl || '';
|
||||||
this.render = render;
|
this.customizedRender = customizedRender;
|
||||||
|
this.setZoom(zoom);
|
||||||
|
|
||||||
this.iframe = globalThis.document.createElement('iframe');
|
this.iframe = globalThis.document.createElement('iframe');
|
||||||
// 同源,直接加载
|
// 同源,直接加载
|
||||||
@ -63,6 +65,38 @@ export default class StageRender extends EventEmitter {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public async add(data: UpdateData): Promise<void> {
|
||||||
|
const runtime = await this.getRuntime();
|
||||||
|
return runtime?.add?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(data: RemoveData): Promise<void> {
|
||||||
|
const runtime = await this.getRuntime();
|
||||||
|
return runtime?.remove?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(data: UpdateData): Promise<void> {
|
||||||
|
const runtime = await this.getRuntime();
|
||||||
|
// 更新画布中的组件
|
||||||
|
runtime?.update?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async select(els: HTMLElement[]): Promise<void> {
|
||||||
|
const runtime = await this.getRuntime();
|
||||||
|
|
||||||
|
for (const el of els) {
|
||||||
|
await runtime?.select?.(el.id);
|
||||||
|
if (runtime?.beforeSelect) {
|
||||||
|
await runtime.beforeSelect(el);
|
||||||
|
}
|
||||||
|
this.flagSelectedEl(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setZoom(zoom: number = DEFAULT_ZOOM): void {
|
||||||
|
this.zoom = zoom;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 挂载Dom节点
|
* 挂载Dom节点
|
||||||
* @param el 将页面挂载到该Dom节点上
|
* @param el 将页面挂载到该Dom节点上
|
||||||
@ -101,6 +135,35 @@ export default class StageRender extends EventEmitter {
|
|||||||
return this.contentWindow?.document;
|
return this.contentWindow?.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过坐标获得坐标下所有HTML元素数组
|
||||||
|
* @param point 坐标
|
||||||
|
* @returns 坐标下方所有HTML元素数组,会包含父元素直至html,元素层叠时返回顺序是从上到下
|
||||||
|
*/
|
||||||
|
public getElementsFromPoint(point: Point): HTMLElement[] {
|
||||||
|
let x = point.clientX;
|
||||||
|
let y = point.clientY;
|
||||||
|
|
||||||
|
if (this.iframe) {
|
||||||
|
const rect = this.iframe.getClientRects()[0];
|
||||||
|
if (rect) {
|
||||||
|
x = x - rect.left;
|
||||||
|
y = y - rect.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getDocument()?.elementsFromPoint(x / this.zoom, y / this.zoom) as HTMLElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTargetElement(idOrEl: Id | HTMLElement): HTMLElement {
|
||||||
|
if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
|
||||||
|
const el = this.getDocument()?.getElementById(`${idOrEl}`);
|
||||||
|
if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
return idOrEl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁实例
|
* 销毁实例
|
||||||
*/
|
*/
|
||||||
@ -112,6 +175,18 @@ export default class StageRender extends EventEmitter {
|
|||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在runtime中对被选中的元素进行标记,部分组件有对选中态进行特殊显示的需求
|
||||||
|
* @param el 被选中的元素
|
||||||
|
*/
|
||||||
|
private flagSelectedEl(el: HTMLElement): void {
|
||||||
|
const doc = this.getDocument();
|
||||||
|
if (doc) {
|
||||||
|
removeSelectedClassName(doc);
|
||||||
|
addSelectedClassName(el, doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private loadHandler = async () => {
|
private loadHandler = async () => {
|
||||||
if (!this.contentWindow?.magic) {
|
if (!this.contentWindow?.magic) {
|
||||||
this.postTmagicRuntimeReady();
|
this.postTmagicRuntimeReady();
|
||||||
@ -119,8 +194,8 @@ export default class StageRender extends EventEmitter {
|
|||||||
|
|
||||||
if (!this.contentWindow) return;
|
if (!this.contentWindow) return;
|
||||||
|
|
||||||
if (this.render) {
|
if (this.customizedRender) {
|
||||||
const el = await this.render();
|
const el = await this.customizedRender();
|
||||||
if (el) {
|
if (el) {
|
||||||
this.contentWindow.document?.body?.appendChild(el);
|
this.contentWindow.document?.body?.appendChild(el);
|
||||||
}
|
}
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { Mode, ZIndex } from './const';
|
|
||||||
import StageCore from './StageCore';
|
|
||||||
import StageDragResize from './StageDragResize';
|
|
||||||
import StageMask from './StageMask';
|
|
||||||
import type { Offset, TargetCalibrateConfig } from './types';
|
|
||||||
import { getMode } from './util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将选中的节点修正定位后,添加一个操作节点到蒙层上
|
|
||||||
*/
|
|
||||||
export default class TargetCalibrate extends EventEmitter {
|
|
||||||
public parent: HTMLElement;
|
|
||||||
public mask: StageMask;
|
|
||||||
public dr: StageDragResize;
|
|
||||||
public core: StageCore;
|
|
||||||
public operationEl: HTMLDivElement;
|
|
||||||
|
|
||||||
constructor(config: TargetCalibrateConfig) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.parent = config.parent;
|
|
||||||
this.mask = config.mask;
|
|
||||||
this.dr = config.dr;
|
|
||||||
this.core = config.core;
|
|
||||||
|
|
||||||
this.operationEl = globalThis.document.createElement('div');
|
|
||||||
this.parent.append(this.operationEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(el: HTMLElement, prefix: String): HTMLElement {
|
|
||||||
const { left, top } = this.getOffset(el);
|
|
||||||
const { transform } = getComputedStyle(el);
|
|
||||||
this.operationEl.style.cssText = `
|
|
||||||
position: absolute;
|
|
||||||
transform: ${transform};
|
|
||||||
left: ${left}px;
|
|
||||||
top: ${top}px;
|
|
||||||
width: ${el.clientWidth}px;
|
|
||||||
height: ${el.clientHeight}px;
|
|
||||||
z-index: ${ZIndex.HIGHLIGHT_EL};
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.operationEl.id = `${prefix}${el.id}`;
|
|
||||||
|
|
||||||
if (typeof this.core.config.updateDragEl === 'function') {
|
|
||||||
this.core.config.updateDragEl(this.operationEl, el);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.operationEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.operationEl?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOffset(el: HTMLElement): Offset {
|
|
||||||
const { offsetParent } = el;
|
|
||||||
|
|
||||||
const left = el.offsetLeft;
|
|
||||||
const top = el.offsetTop;
|
|
||||||
|
|
||||||
if (offsetParent) {
|
|
||||||
const parentOffset = this.getOffset(offsetParent as HTMLElement);
|
|
||||||
return {
|
|
||||||
left: left + parentOffset.left,
|
|
||||||
top: top + parentOffset.top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中固定定位元素后editor-mask高度被置为视窗大小
|
|
||||||
if (this.dr.mode === Mode.FIXED) {
|
|
||||||
// 弹窗的情况
|
|
||||||
if (getMode(el) === Mode.FIXED) {
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left: left - this.mask.scrollLeft,
|
|
||||||
top: top - this.mask.scrollTop,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无父元素的固定定位需按滚动值计算
|
|
||||||
if (getMode(el) === Mode.FIXED) {
|
|
||||||
return {
|
|
||||||
left: left + this.mask.scrollLeft,
|
|
||||||
top: top + this.mask.scrollTop,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
120
packages/stage/src/TargetShadow.ts
Normal file
120
packages/stage/src/TargetShadow.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Mode, ZIndex } from './const';
|
||||||
|
import type { TargetElement, TargetShadowConfig, UpdateDragEl } from './types';
|
||||||
|
import { getTargetElStyle, isFixedParent } from './util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将选中的节点修正定位后,添加一个操作节点到蒙层上
|
||||||
|
*/
|
||||||
|
export default class TargetShadow {
|
||||||
|
public el?: TargetElement;
|
||||||
|
public els: TargetElement[] = [];
|
||||||
|
|
||||||
|
private idPrefix = 'target_calibrate_';
|
||||||
|
private container: HTMLElement;
|
||||||
|
private scrollLeft = 0;
|
||||||
|
private scrollTop = 0;
|
||||||
|
private zIndex?: ZIndex;
|
||||||
|
|
||||||
|
private updateDragEl?: UpdateDragEl;
|
||||||
|
|
||||||
|
constructor(config: TargetShadowConfig) {
|
||||||
|
this.container = config.container;
|
||||||
|
|
||||||
|
if (config.updateDragEl) {
|
||||||
|
this.updateDragEl = config.updateDragEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof config.zIndex !== 'undefined') {
|
||||||
|
this.zIndex = config.zIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.idPrefix) {
|
||||||
|
this.idPrefix = config.idPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.addEventListener('customScroll', this.scrollHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(target: TargetElement): TargetElement {
|
||||||
|
this.el = this.updateEl(target, this.el);
|
||||||
|
|
||||||
|
return this.el;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGroup(targetGroup: TargetElement[]): TargetElement[] {
|
||||||
|
if (this.els.length > targetGroup.length) {
|
||||||
|
this.els.slice(targetGroup.length - 1).forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.els = targetGroup.map((target, index) => this.updateEl(target, this.els[index]));
|
||||||
|
|
||||||
|
return this.els;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroyEl(): void {
|
||||||
|
this.el?.remove();
|
||||||
|
this.el = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroyEls(): void {
|
||||||
|
this.els.forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
this.els = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.container.removeEventListener('customScroll', this.scrollHandler);
|
||||||
|
this.destroyEl();
|
||||||
|
this.destroyEls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEl(target: TargetElement, src?: TargetElement): TargetElement {
|
||||||
|
const el = src || globalThis.document.createElement('div');
|
||||||
|
|
||||||
|
el.id = `${this.idPrefix}${target.id}`;
|
||||||
|
|
||||||
|
el.style.cssText = getTargetElStyle(target, this.zIndex);
|
||||||
|
|
||||||
|
if (typeof this.updateDragEl === 'function') {
|
||||||
|
this.updateDragEl(el, target);
|
||||||
|
}
|
||||||
|
const isFixed = isFixedParent(target);
|
||||||
|
const mode = this.container.dataset.mode || Mode.ABSOLUTE;
|
||||||
|
if (isFixed && mode !== Mode.FIXED) {
|
||||||
|
el.style.transform = `translate3d(${this.scrollLeft}px, ${this.scrollTop}px, 0)`;
|
||||||
|
} else if (!isFixed && mode === Mode.FIXED) {
|
||||||
|
el.style.transform = `translate3d(${-this.scrollLeft}px, ${-this.scrollTop}px, 0)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.document.getElementById(el.id)) {
|
||||||
|
this.container.append(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollHandler = (e: any) => {
|
||||||
|
this.scrollLeft = e.detail.scrollLeft;
|
||||||
|
this.scrollTop = e.detail.scrollTop;
|
||||||
|
};
|
||||||
|
}
|
@ -25,7 +25,7 @@ export const DRAG_EL_ID_PREFIX = 'drag_el_';
|
|||||||
/** 高亮时需要在蒙层中创建一个占位节点,该节点的id前缀 */
|
/** 高亮时需要在蒙层中创建一个占位节点,该节点的id前缀 */
|
||||||
export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
|
export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
|
||||||
|
|
||||||
export const CONTAINER_HIGHLIGHT_CLASS = 'tmagic-stage-container-highlight';
|
export const CONTAINER_HIGHLIGHT_CLASS_NAME = 'tmagic-stage-container-highlight';
|
||||||
|
|
||||||
export const PAGE_CLASS = 'magic-ui-page';
|
export const PAGE_CLASS = 'magic-ui-page';
|
||||||
|
|
||||||
|
@ -21,20 +21,39 @@ import type { MoveableOptions } from 'moveable';
|
|||||||
import Core from '@tmagic/core';
|
import Core from '@tmagic/core';
|
||||||
import type { Id, MApp, MContainer, MNode } from '@tmagic/schema';
|
import type { Id, MApp, MContainer, MNode } from '@tmagic/schema';
|
||||||
|
|
||||||
import { GuidesType } from './const';
|
import { GuidesType, ZIndex } from './const';
|
||||||
import StageCore from './StageCore';
|
import StageCore from './StageCore';
|
||||||
import StageDragResize from './StageDragResize';
|
|
||||||
import StageMask from './StageMask';
|
export type TargetElement = HTMLElement | SVGElement;
|
||||||
|
|
||||||
export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
|
export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
|
||||||
export type IsContainer = (el: HTMLElement) => boolean | Promise<boolean>;
|
export type IsContainer = (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
|
export type CustomizeRender = (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
|
||||||
|
/** 业务方自定义的moveableOptions,可以是配置,也可以是回调函数 */
|
||||||
|
export type CustomizeMoveableOptions =
|
||||||
|
| ((config?: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions)
|
||||||
|
| MoveableOptions
|
||||||
|
| undefined;
|
||||||
|
/** render提供给的接口,如果是id则转成el,如果是el则直接返回 */
|
||||||
|
export type GetTargetElement = (idOrEl: Id | HTMLElement) => HTMLElement;
|
||||||
|
/** render提供的接口,通过坐标获得坐标下所有HTML元素数组 */
|
||||||
|
export type GetElementsFromPoint = (point: Point) => HTMLElement[];
|
||||||
|
export type GetRenderDocument = () => Document | undefined;
|
||||||
|
export type DelayedMarkContainer = (event: MouseEvent, exclude: Element[]) => NodeJS.Timeout | undefined;
|
||||||
|
export type MarkContainerEnd = () => HTMLElement | null;
|
||||||
|
export type GetRootContainer = () => HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
/** 将组件添加到容器的方式 */
|
||||||
export enum ContainerHighlightType {
|
export enum ContainerHighlightType {
|
||||||
|
/** 默认方式:组件在容器上方悬停一段时间后加入 */
|
||||||
DEFAULT = 'default',
|
DEFAULT = 'default',
|
||||||
|
/** 按住alt键,并在容器上方悬停一段时间后加入 */
|
||||||
ALT = 'alt',
|
ALT = 'alt',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StageCoreConfig = {
|
export type UpdateDragEl = (el: TargetElement, target: TargetElement) => void;
|
||||||
|
|
||||||
|
export interface StageCoreConfig {
|
||||||
/** 需要对齐的dom节点的CSS选择器字符串 */
|
/** 需要对齐的dom节点的CSS选择器字符串 */
|
||||||
snapElementQuerySelector?: string;
|
snapElementQuerySelector?: string;
|
||||||
/** 放大倍数,默认1倍 */
|
/** 放大倍数,默认1倍 */
|
||||||
@ -44,18 +63,45 @@ export type StageCoreConfig = {
|
|||||||
containerHighlightClassName?: string;
|
containerHighlightClassName?: string;
|
||||||
containerHighlightDuration?: number;
|
containerHighlightDuration?: number;
|
||||||
containerHighlightType?: ContainerHighlightType;
|
containerHighlightType?: ContainerHighlightType;
|
||||||
moveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
|
moveableOptions?: CustomizeMoveableOptions;
|
||||||
multiMoveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
|
multiMoveableOptions?: CustomizeMoveableOptions;
|
||||||
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
||||||
runtimeUrl?: string;
|
runtimeUrl?: string;
|
||||||
render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
|
render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
|
||||||
autoScrollIntoView?: boolean;
|
autoScrollIntoView?: boolean;
|
||||||
updateDragEl?: (el: HTMLDivElement, target: HTMLElement) => void;
|
updateDragEl?: UpdateDragEl;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface ActionManagerConfig {
|
||||||
|
container: HTMLElement;
|
||||||
|
containerHighlightClassName?: string;
|
||||||
|
containerHighlightDuration?: number;
|
||||||
|
containerHighlightType?: ContainerHighlightType;
|
||||||
|
moveableOptions?: CustomizeMoveableOptions;
|
||||||
|
multiMoveableOptions?: CustomizeMoveableOptions;
|
||||||
|
canSelect?: CanSelect;
|
||||||
|
isContainer: IsContainer;
|
||||||
|
getRootContainer: GetRootContainer;
|
||||||
|
getRenderDocument: GetRenderDocument;
|
||||||
|
updateDragEl?: UpdateDragEl;
|
||||||
|
getTargetElement: GetTargetElement;
|
||||||
|
getElementsFromPoint: GetElementsFromPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveableOptionsManagerConfig {
|
||||||
|
container: HTMLElement;
|
||||||
|
moveableOptions?: CustomizeMoveableOptions;
|
||||||
|
getRootContainer: GetRootContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomizeMoveableOptionsCallbackConfig {
|
||||||
|
targetElId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StageRenderConfig {
|
export interface StageRenderConfig {
|
||||||
runtimeUrl?: string;
|
runtimeUrl?: string;
|
||||||
render?: () => Promise<HTMLElement | null>;
|
zoom: number | undefined;
|
||||||
|
customizedRender?: () => Promise<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StageMaskConfig {
|
export interface StageMaskConfig {
|
||||||
@ -63,9 +109,29 @@ export interface StageMaskConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StageDragResizeConfig {
|
export interface StageDragResizeConfig {
|
||||||
core: StageCore;
|
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
mask: StageMask;
|
moveableOptions?: CustomizeMoveableOptions;
|
||||||
|
getRootContainer: GetRootContainer;
|
||||||
|
getRenderDocument: GetRenderDocument;
|
||||||
|
markContainerEnd: MarkContainerEnd;
|
||||||
|
delayedMarkContainer: DelayedMarkContainer;
|
||||||
|
updateDragEl?: UpdateDragEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StageMultiDragResizeConfig {
|
||||||
|
container: HTMLElement;
|
||||||
|
multiMoveableOptions?: CustomizeMoveableOptions;
|
||||||
|
getRootContainer: GetRootContainer;
|
||||||
|
getRenderDocument: GetRenderDocument;
|
||||||
|
updateDragEl?: UpdateDragEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择状态 */
|
||||||
|
export enum SelectStatus {
|
||||||
|
/** 单选 */
|
||||||
|
SELECT = 'select',
|
||||||
|
/** 多选 */
|
||||||
|
MULTI_SELECT = 'multiSelect',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拖动状态 */
|
/** 拖动状态 */
|
||||||
@ -88,6 +154,11 @@ export interface Offset {
|
|||||||
top: number;
|
top: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GuidesEventData {
|
export interface GuidesEventData {
|
||||||
type: GuidesType;
|
type: GuidesType;
|
||||||
guides: number[];
|
guides: number[];
|
||||||
@ -154,13 +225,14 @@ export interface RuntimeWindow extends Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StageHighlightConfig {
|
export interface StageHighlightConfig {
|
||||||
core: StageCore;
|
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
|
updateDragEl?: UpdateDragEl;
|
||||||
|
getRootContainer: GetRootContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetCalibrateConfig {
|
export interface TargetShadowConfig {
|
||||||
parent: HTMLElement;
|
container: HTMLElement;
|
||||||
mask: StageMask;
|
zIndex?: ZIndex;
|
||||||
dr: StageDragResize;
|
updateDragEl?: UpdateDragEl;
|
||||||
core: StageCore;
|
idPrefix?: string;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import { removeClassName } from '@tmagic/utils';
|
import { removeClassName } from '@tmagic/utils';
|
||||||
|
|
||||||
import { GHOST_EL_ID_PREFIX, Mode, SELECTED_CLASS, ZIndex } from './const';
|
import { GHOST_EL_ID_PREFIX, Mode, SELECTED_CLASS, ZIndex } from './const';
|
||||||
import type { Offset, SortEventData } from './types';
|
import type { Offset, SortEventData, TargetElement } from './types';
|
||||||
|
|
||||||
const getParents = (el: Element, relative: Element) => {
|
const getParents = (el: Element, relative: Element) => {
|
||||||
let cur: Element | null = el.parentElement;
|
let cur: Element | null = el.parentElement;
|
||||||
@ -30,12 +30,16 @@ const getParents = (el: Element, relative: Element) => {
|
|||||||
return parents;
|
return parents;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOffset = (el: HTMLElement): Offset => {
|
export const getOffset = (el: TargetElement): Offset => {
|
||||||
const { offsetParent } = el;
|
const htmlEl = el as HTMLElement;
|
||||||
|
const { offsetParent } = htmlEl;
|
||||||
|
|
||||||
const left = el.offsetLeft;
|
const left = htmlEl.offsetLeft || 0;
|
||||||
const top = el.offsetTop;
|
const top = htmlEl.offsetTop || 0;
|
||||||
|
|
||||||
|
// 在 Webkit 中,如果元素为隐藏的(该元素或其祖先元素的 style.display 为 "none"),或者该元素的 style.position 被设为 "fixed",则该属性返回 null。
|
||||||
|
// 在 IE 9 中,如果该元素的 style.position 被设置为 "fixed",则该属性返回 null。(display:none 无影响。)
|
||||||
|
// body offsetParent 为 null
|
||||||
if (offsetParent) {
|
if (offsetParent) {
|
||||||
const parentOffset = getOffset(offsetParent as HTMLElement);
|
const parentOffset = getOffset(offsetParent as HTMLElement);
|
||||||
return {
|
return {
|
||||||
@ -51,7 +55,7 @@ export const getOffset = (el: HTMLElement): Offset => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 将蒙层占位节点覆盖在原节点上方
|
// 将蒙层占位节点覆盖在原节点上方
|
||||||
export const getTargetElStyle = (el: HTMLElement) => {
|
export const getTargetElStyle = (el: TargetElement, zIndex?: ZIndex) => {
|
||||||
const offset = getOffset(el);
|
const offset = getOffset(el);
|
||||||
const { transform } = getComputedStyle(el);
|
const { transform } = getComputedStyle(el);
|
||||||
return `
|
return `
|
||||||
@ -61,13 +65,16 @@ export const getTargetElStyle = (el: HTMLElement) => {
|
|||||||
top: ${offset.top}px;
|
top: ${offset.top}px;
|
||||||
width: ${el.clientWidth}px;
|
width: ${el.clientWidth}px;
|
||||||
height: ${el.clientHeight}px;
|
height: ${el.clientHeight}px;
|
||||||
z-index: ${ZIndex.DRAG_EL};
|
${typeof zIndex !== 'undefined' ? `z-index: ${zIndex};` : ''}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
|
export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
|
||||||
const { offsetParent } = el;
|
const { offsetParent } = el;
|
||||||
|
|
||||||
|
// 在 Webkit 中,如果元素为隐藏的(该元素或其祖先元素的 style.display 为 "none"),或者该元素的 style.position 被设为 "fixed",则该属性返回 null。
|
||||||
|
// 在 IE 9 中,如果该元素的 style.position 被设置为 "fixed",则该属性返回 null。(display:none 无影响。)
|
||||||
|
// body offsetParent 为 null
|
||||||
if (offsetParent) {
|
if (offsetParent) {
|
||||||
const parentOffset = getOffset(offsetParent as HTMLElement);
|
const parentOffset = getOffset(offsetParent as HTMLElement);
|
||||||
return {
|
return {
|
||||||
@ -87,7 +94,7 @@ export const isStatic = (style: CSSStyleDeclaration): boolean => style.position
|
|||||||
|
|
||||||
export const isFixed = (style: CSSStyleDeclaration): boolean => style.position === 'fixed';
|
export const isFixed = (style: CSSStyleDeclaration): boolean => style.position === 'fixed';
|
||||||
|
|
||||||
export const isFixedParent = (el: HTMLElement) => {
|
export const isFixedParent = (el: Element) => {
|
||||||
let fixed = false;
|
let fixed = false;
|
||||||
let dom = el;
|
let dom = el;
|
||||||
while (dom) {
|
while (dom) {
|
||||||
@ -104,7 +111,7 @@ export const isFixedParent = (el: HTMLElement) => {
|
|||||||
return fixed;
|
return fixed;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMode = (el: HTMLElement): Mode => {
|
export const getMode = (el: Element): Mode => {
|
||||||
if (isFixedParent(el)) return Mode.FIXED;
|
if (isFixedParent(el)) return Mode.FIXED;
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
if (isStatic(style) || isRelative(style)) return Mode.SORTABLE;
|
if (isStatic(style) || isRelative(style)) return Mode.SORTABLE;
|
||||||
@ -168,7 +175,7 @@ export const calcValueByFontsize = (doc: Document, value: number) => {
|
|||||||
* @param {number} deltaTop 偏移量
|
* @param {number} deltaTop 偏移量
|
||||||
* @param {Object} detail 当前选中的组件配置
|
* @param {Object} detail 当前选中的组件配置
|
||||||
*/
|
*/
|
||||||
export const down = (deltaTop: number, target: HTMLElement | SVGElement): SortEventData | void => {
|
export const down = (deltaTop: number, target: TargetElement): SortEventData | void => {
|
||||||
let swapIndex = 0;
|
let swapIndex = 0;
|
||||||
let addUpH = target.clientHeight;
|
let addUpH = target.clientHeight;
|
||||||
const brothers = Array.from(target.parentNode?.children || []).filter(
|
const brothers = Array.from(target.parentNode?.children || []).filter(
|
||||||
@ -204,7 +211,7 @@ export const down = (deltaTop: number, target: HTMLElement | SVGElement): SortEv
|
|||||||
* @param {number} deltaTop 偏移量
|
* @param {number} deltaTop 偏移量
|
||||||
* @param {Object} detail 当前选中的组件配置
|
* @param {Object} detail 当前选中的组件配置
|
||||||
*/
|
*/
|
||||||
export const up = (deltaTop: number, target: HTMLElement | SVGElement): SortEventData | void => {
|
export const up = (deltaTop: number, target: TargetElement): SortEventData | void => {
|
||||||
const brothers = Array.from(target.parentNode?.children || []).filter(
|
const brothers = Array.from(target.parentNode?.children || []).filter(
|
||||||
(node) => !node.id.startsWith(GHOST_EL_ID_PREFIX),
|
(node) => !node.id.startsWith(GHOST_EL_ID_PREFIX),
|
||||||
);
|
);
|
||||||
|
@ -108,3 +108,5 @@ export const createDiv = ({ className, cssText }: { className: string; cssText:
|
|||||||
el.style.cssText = cssText;
|
el.style.cssText = cssText;
|
||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDocument = () => globalThis.document;
|
||||||
|
@ -41,7 +41,7 @@ import serialize from 'serialize-javascript';
|
|||||||
import { editorService, MenuBarData, MoveableOptions, TMagicEditor } from '@tmagic/editor';
|
import { editorService, MenuBarData, MoveableOptions, TMagicEditor } from '@tmagic/editor';
|
||||||
import type { Id, MContainer, MNode } from '@tmagic/schema';
|
import type { Id, MContainer, MNode } from '@tmagic/schema';
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
import StageCore from '@tmagic/stage';
|
import { CustomizeMoveableOptionsCallbackConfig } from '@tmagic/stage';
|
||||||
import { asyncLoadJs } from '@tmagic/utils';
|
import { asyncLoadJs } from '@tmagic/utils';
|
||||||
|
|
||||||
import DeviceGroup from '../components/DeviceGroup.vue';
|
import DeviceGroup from '../components/DeviceGroup.vue';
|
||||||
@ -128,10 +128,10 @@ const menu: MenuBarData = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveableOptions = (core?: StageCore): MoveableOptions => {
|
const moveableOptions = (config?: CustomizeMoveableOptionsCallbackConfig): MoveableOptions => {
|
||||||
const options: MoveableOptions = {};
|
const options: MoveableOptions = {};
|
||||||
const id = core?.dr?.target?.id;
|
|
||||||
|
|
||||||
|
const id = config?.targetElId;
|
||||||
if (!id || !editor.value) return options;
|
if (!id || !editor.value) return options;
|
||||||
|
|
||||||
const node = editor.value.editorService.getNodeById(id);
|
const node = editor.value.editorService.getNodeById(id);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user