@tmagic/stage 1.1.0-beta.1 → 1.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tmagic-stage.es.js +292 -95
- package/dist/tmagic-stage.es.js.map +1 -1
- package/dist/tmagic-stage.umd.js +298 -97
- package/dist/tmagic-stage.umd.js.map +1 -1
- package/dist/types/src/StageCore.d.ts +12 -2
- package/dist/types/src/StageDragResize.d.ts +7 -18
- package/dist/types/src/StageMask.d.ts +1 -0
- package/dist/types/src/StageMultiDragResize.d.ts +42 -0
- package/dist/types/src/const.d.ts +1 -0
- package/dist/types/src/types.d.ts +3 -1
- package/dist/types/src/util.d.ts +16 -1
- package/package.json +5 -4
- package/src/StageCore.ts +84 -16
- package/src/StageDragResize.ts +40 -118
- package/src/StageMask.ts +23 -6
- package/src/StageMultiDragResize.ts +185 -0
- package/src/const.ts +2 -0
- package/src/types.ts +3 -1
- package/src/util.ts +87 -2
package/src/StageMask.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import KeyController from 'keycon';
|
|
19
20
|
import { throttle } from 'lodash-es';
|
|
20
21
|
|
|
21
22
|
import { createDiv, injectStyle } from '@tmagic/utils';
|
|
@@ -82,6 +83,7 @@ export default class StageMask extends Rule {
|
|
|
82
83
|
public maxScrollTop = 0;
|
|
83
84
|
public maxScrollLeft = 0;
|
|
84
85
|
public intersectionObserver: IntersectionObserver | null = null;
|
|
86
|
+
public shiftKeyDown: Boolean = false;
|
|
85
87
|
|
|
86
88
|
private mode: Mode = Mode.ABSOLUTE;
|
|
87
89
|
private pageResizeObserver: ResizeObserver | null = null;
|
|
@@ -106,6 +108,14 @@ export default class StageMask extends Rule {
|
|
|
106
108
|
this.content.addEventListener('wheel', this.mouseWheelHandler);
|
|
107
109
|
this.content.addEventListener('mousemove', this.highlightHandler);
|
|
108
110
|
this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
|
|
111
|
+
KeyController.global.keydown('shift', (e) => {
|
|
112
|
+
e.inputEvent.preventDefault();
|
|
113
|
+
this.shiftKeyDown = true;
|
|
114
|
+
});
|
|
115
|
+
KeyController.global.keyup('shift', (e) => {
|
|
116
|
+
e.inputEvent.preventDefault();
|
|
117
|
+
this.shiftKeyDown = false;
|
|
118
|
+
});
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
public setMode(mode: Mode) {
|
|
@@ -292,23 +302,30 @@ export default class StageMask extends Rule {
|
|
|
292
302
|
*/
|
|
293
303
|
private mouseDownHandler = (event: MouseEvent): void => {
|
|
294
304
|
this.emit('clearHighlight');
|
|
295
|
-
|
|
296
305
|
event.stopImmediatePropagation();
|
|
297
306
|
event.stopPropagation();
|
|
298
307
|
|
|
299
308
|
if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
|
|
300
309
|
|
|
301
|
-
//
|
|
310
|
+
// 如果单击多选选中区域,则不需要再触发选中了,而可能是拖动行为
|
|
311
|
+
if (!this.shiftKeyDown && (event.target as HTMLDivElement).className.indexOf('moveable-area') !== -1) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// 点击对象如果是边框锚点,则可能是resize
|
|
302
315
|
if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
|
|
303
316
|
return;
|
|
304
317
|
}
|
|
305
318
|
|
|
306
319
|
this.content.removeEventListener('mousemove', this.highlightHandler);
|
|
307
320
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
321
|
+
// 判断触发多选还是单选
|
|
322
|
+
if (this.shiftKeyDown) {
|
|
323
|
+
this.emit('beforeMultiSelect', event);
|
|
324
|
+
} else {
|
|
325
|
+
this.emit('beforeSelect', event);
|
|
326
|
+
// 如果是右键点击,这里的mouseup事件监听没有效果
|
|
327
|
+
globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
|
|
328
|
+
}
|
|
312
329
|
};
|
|
313
330
|
|
|
314
331
|
private mouseUpHandler = (): void => {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
|
|
21
|
+
import Moveable from 'moveable';
|
|
22
|
+
import MoveableHelper from 'moveable-helper';
|
|
23
|
+
|
|
24
|
+
import { DRAG_EL_ID_PREFIX } from './const';
|
|
25
|
+
import StageCore from './StageCore';
|
|
26
|
+
import StageMask from './StageMask';
|
|
27
|
+
import { StageDragResizeConfig } from './types';
|
|
28
|
+
import { calcValueByFontsize, getTargetElStyle } from './util';
|
|
29
|
+
export default class StageMultiDragResize extends EventEmitter {
|
|
30
|
+
public core: StageCore;
|
|
31
|
+
public mask: StageMask;
|
|
32
|
+
/** 画布容器 */
|
|
33
|
+
public container: HTMLElement;
|
|
34
|
+
/** 多选:目标节点组 */
|
|
35
|
+
public targetList: HTMLElement[] = [];
|
|
36
|
+
/** 多选:目标节点在蒙层中的占位节点组 */
|
|
37
|
+
public dragElList: HTMLDivElement[] = [];
|
|
38
|
+
/** Moveable多选拖拽类实例 */
|
|
39
|
+
public moveableForMulti?: Moveable;
|
|
40
|
+
private multiMoveableHelper?: MoveableHelper;
|
|
41
|
+
|
|
42
|
+
constructor(config: StageDragResizeConfig) {
|
|
43
|
+
super();
|
|
44
|
+
|
|
45
|
+
this.core = config.core;
|
|
46
|
+
this.container = config.container;
|
|
47
|
+
this.mask = config.mask;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 多选
|
|
52
|
+
* @param els
|
|
53
|
+
*/
|
|
54
|
+
public multiSelect(els: HTMLElement[]): void {
|
|
55
|
+
this.targetList = els;
|
|
56
|
+
this.core.dr.destroyDragEl();
|
|
57
|
+
this.destroyDragElList();
|
|
58
|
+
// 生成虚拟多选节点
|
|
59
|
+
this.dragElList = els.map((elItem) => {
|
|
60
|
+
const dragElDiv = globalThis.document.createElement('div');
|
|
61
|
+
this.container.append(dragElDiv);
|
|
62
|
+
dragElDiv.style.cssText = getTargetElStyle(elItem);
|
|
63
|
+
dragElDiv.id = `${DRAG_EL_ID_PREFIX}${elItem.id}`;
|
|
64
|
+
// 业务方校准
|
|
65
|
+
if (typeof this.core.config.updateDragEl === 'function') {
|
|
66
|
+
this.core.config.updateDragEl(dragElDiv, elItem);
|
|
67
|
+
}
|
|
68
|
+
return dragElDiv;
|
|
69
|
+
});
|
|
70
|
+
this.moveableForMulti?.destroy();
|
|
71
|
+
this.multiMoveableHelper?.clear();
|
|
72
|
+
|
|
73
|
+
this.moveableForMulti = new Moveable(this.container, {
|
|
74
|
+
target: this.dragElList,
|
|
75
|
+
defaultGroupRotate: 0,
|
|
76
|
+
defaultGroupOrigin: '50% 50%',
|
|
77
|
+
draggable: true,
|
|
78
|
+
resizable: true,
|
|
79
|
+
throttleDrag: 0,
|
|
80
|
+
startDragRotate: 0,
|
|
81
|
+
throttleDragRotate: 0,
|
|
82
|
+
zoom: 1,
|
|
83
|
+
origin: true,
|
|
84
|
+
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
85
|
+
});
|
|
86
|
+
this.multiMoveableHelper = MoveableHelper.create({
|
|
87
|
+
useBeforeRender: true,
|
|
88
|
+
useRender: false,
|
|
89
|
+
createAuto: true,
|
|
90
|
+
});
|
|
91
|
+
const frames: { left: number; top: number; id: string }[] = [];
|
|
92
|
+
this.moveableForMulti
|
|
93
|
+
.on('dragGroupStart', (params) => {
|
|
94
|
+
const { events } = params;
|
|
95
|
+
this.multiMoveableHelper?.onDragGroupStart(params);
|
|
96
|
+
// 记录拖动前快照
|
|
97
|
+
events.forEach((ev) => {
|
|
98
|
+
// 实际目标元素
|
|
99
|
+
const matchEventTarget = this.targetList.find(
|
|
100
|
+
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
101
|
+
);
|
|
102
|
+
if (!matchEventTarget) return;
|
|
103
|
+
frames.push({
|
|
104
|
+
left: matchEventTarget.offsetLeft,
|
|
105
|
+
top: matchEventTarget.offsetTop,
|
|
106
|
+
id: matchEventTarget.id,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
})
|
|
110
|
+
.on('dragGroup', (params) => {
|
|
111
|
+
const { events } = params;
|
|
112
|
+
// 拖动过程更新
|
|
113
|
+
events.forEach((ev) => {
|
|
114
|
+
const frameSnapShot = frames.find(
|
|
115
|
+
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
116
|
+
);
|
|
117
|
+
if (!frameSnapShot) return;
|
|
118
|
+
const targeEl = this.targetList.find(
|
|
119
|
+
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
120
|
+
);
|
|
121
|
+
if (!targeEl) return;
|
|
122
|
+
// 元素与其所属组同时加入多选列表时,只更新父元素
|
|
123
|
+
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
|
|
124
|
+
if (!isParentIncluded) {
|
|
125
|
+
// 更新页面元素位置
|
|
126
|
+
targeEl.style.left = `${frameSnapShot.left + ev.beforeTranslate[0]}px`;
|
|
127
|
+
targeEl.style.top = `${frameSnapShot.top + ev.beforeTranslate[1]}px`;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
this.multiMoveableHelper?.onDragGroup(params);
|
|
131
|
+
})
|
|
132
|
+
.on('dragGroupEnd', () => {
|
|
133
|
+
this.update();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 清除多选状态
|
|
139
|
+
*/
|
|
140
|
+
public clearSelectStatus(): void {
|
|
141
|
+
if (!this.moveableForMulti) return;
|
|
142
|
+
this.destroyDragElList();
|
|
143
|
+
this.moveableForMulti.target = null;
|
|
144
|
+
this.moveableForMulti.updateTarget();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 销毁实例
|
|
149
|
+
*/
|
|
150
|
+
public destroy(): void {
|
|
151
|
+
this.moveableForMulti?.destroy();
|
|
152
|
+
this.destroyDragElList();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 清除蒙层占位节点
|
|
157
|
+
*/
|
|
158
|
+
public destroyDragElList(): void {
|
|
159
|
+
this.dragElList.forEach((dragElItem) => dragElItem?.remove());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 拖拽完成后将更新的位置信息暴露给上层业务方,业务方可以接收事件进行保存
|
|
164
|
+
* @param isResize 是否进行大小缩放
|
|
165
|
+
*/
|
|
166
|
+
private update(isResize = false): void {
|
|
167
|
+
if (this.targetList.length === 0) return;
|
|
168
|
+
|
|
169
|
+
const { contentWindow } = this.core.renderer;
|
|
170
|
+
const doc = contentWindow?.document;
|
|
171
|
+
if (!doc) return;
|
|
172
|
+
|
|
173
|
+
this.targetList.forEach((targetItem) => {
|
|
174
|
+
const offset = { left: targetItem.offsetLeft, top: targetItem.offsetTop };
|
|
175
|
+
const left = calcValueByFontsize(doc, offset.left);
|
|
176
|
+
const top = calcValueByFontsize(doc, offset.top);
|
|
177
|
+
const width = calcValueByFontsize(doc, targetItem.clientWidth);
|
|
178
|
+
const height = calcValueByFontsize(doc, targetItem.clientHeight);
|
|
179
|
+
this.emit('update', {
|
|
180
|
+
el: targetItem,
|
|
181
|
+
style: isResize ? { left, top, width, height } : { left, top },
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/const.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { MoveableOptions } from 'moveable';
|
|
20
20
|
|
|
21
21
|
import Core from '@tmagic/core';
|
|
22
|
-
import type { Id, MApp, MNode } from '@tmagic/schema';
|
|
22
|
+
import type { Id, MApp, MContainer, MNode } from '@tmagic/schema';
|
|
23
23
|
|
|
24
24
|
import { GuidesType } from './const';
|
|
25
25
|
import StageCore from './StageCore';
|
|
@@ -57,6 +57,7 @@ export interface StageMaskConfig {
|
|
|
57
57
|
export interface StageDragResizeConfig {
|
|
58
58
|
core: StageCore;
|
|
59
59
|
container: HTMLElement;
|
|
60
|
+
mask: StageMask;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export type Rect = {
|
|
@@ -98,6 +99,7 @@ export interface SortEventData {
|
|
|
98
99
|
|
|
99
100
|
export interface UpdateData {
|
|
100
101
|
config: MNode;
|
|
102
|
+
parent?: MContainer;
|
|
101
103
|
root: MApp;
|
|
102
104
|
}
|
|
103
105
|
|
package/src/util.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { removeClassName } from '@tmagic/utils';
|
|
19
19
|
|
|
20
|
-
import { Mode, SELECTED_CLASS } from './const';
|
|
21
|
-
import type { Offset } from './types';
|
|
20
|
+
import { GHOST_EL_ID_PREFIX, Mode, SELECTED_CLASS, ZIndex } from './const';
|
|
21
|
+
import type { Offset, SortEventData } from './types';
|
|
22
22
|
|
|
23
23
|
const getParents = (el: Element, relative: Element) => {
|
|
24
24
|
let cur: Element | null = el.parentElement;
|
|
@@ -50,6 +50,21 @@ export const getOffset = (el: HTMLElement): Offset => {
|
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
// 将蒙层占位节点覆盖在原节点上方
|
|
54
|
+
export const getTargetElStyle = (el: HTMLElement) => {
|
|
55
|
+
const offset = getOffset(el);
|
|
56
|
+
const { transform } = getComputedStyle(el);
|
|
57
|
+
return `
|
|
58
|
+
position: absolute;
|
|
59
|
+
transform: ${transform};
|
|
60
|
+
left: ${offset.left}px;
|
|
61
|
+
top: ${offset.top}px;
|
|
62
|
+
width: ${el.clientWidth}px;
|
|
63
|
+
height: ${el.clientHeight}px;
|
|
64
|
+
z-index: ${ZIndex.DRAG_EL};
|
|
65
|
+
`;
|
|
66
|
+
};
|
|
67
|
+
|
|
53
68
|
export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
|
|
54
69
|
const { offsetParent } = el;
|
|
55
70
|
|
|
@@ -144,3 +159,73 @@ export const calcValueByFontsize = (doc: Document, value: number) => {
|
|
|
144
159
|
|
|
145
160
|
return value;
|
|
146
161
|
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 下移组件位置
|
|
165
|
+
* @param {number} deltaTop 偏移量
|
|
166
|
+
* @param {Object} detail 当前选中的组件配置
|
|
167
|
+
*/
|
|
168
|
+
export const down = (deltaTop: number, target: HTMLElement | SVGElement): SortEventData | void => {
|
|
169
|
+
let swapIndex = 0;
|
|
170
|
+
let addUpH = target.clientHeight;
|
|
171
|
+
const brothers = Array.from(target.parentNode?.children || []).filter(
|
|
172
|
+
(node) => !node.id.startsWith(GHOST_EL_ID_PREFIX),
|
|
173
|
+
);
|
|
174
|
+
const index = brothers.indexOf(target);
|
|
175
|
+
// 往下移动
|
|
176
|
+
const downEls = brothers.slice(index + 1) as HTMLElement[];
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < downEls.length; i++) {
|
|
179
|
+
const ele = downEls[i];
|
|
180
|
+
// 是 fixed 不做处理
|
|
181
|
+
if (ele.style?.position === 'fixed') {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
addUpH += ele.clientHeight / 2;
|
|
185
|
+
if (deltaTop <= addUpH) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
addUpH += ele.clientHeight / 2;
|
|
189
|
+
swapIndex = i;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
src: target.id,
|
|
193
|
+
dist: downEls.length && swapIndex > -1 ? downEls[swapIndex].id : target.id,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 上移组件位置
|
|
199
|
+
* @param {Array} brothers 处于同一容器下的所有子组件配置
|
|
200
|
+
* @param {number} index 当前组件所处的位置
|
|
201
|
+
* @param {number} deltaTop 偏移量
|
|
202
|
+
* @param {Object} detail 当前选中的组件配置
|
|
203
|
+
*/
|
|
204
|
+
export const up = (deltaTop: number, target: HTMLElement | SVGElement): SortEventData | void => {
|
|
205
|
+
const brothers = Array.from(target.parentNode?.children || []).filter(
|
|
206
|
+
(node) => !node.id.startsWith(GHOST_EL_ID_PREFIX),
|
|
207
|
+
);
|
|
208
|
+
const index = brothers.indexOf(target);
|
|
209
|
+
// 往上移动
|
|
210
|
+
const upEls = brothers.slice(0, index) as HTMLElement[];
|
|
211
|
+
|
|
212
|
+
let addUpH = target.clientHeight;
|
|
213
|
+
let swapIndex = upEls.length - 1;
|
|
214
|
+
|
|
215
|
+
for (let i = upEls.length - 1; i >= 0; i--) {
|
|
216
|
+
const ele = upEls[i];
|
|
217
|
+
if (!ele) continue;
|
|
218
|
+
// 是 fixed 不做处理
|
|
219
|
+
if (ele.style.position === 'fixed') continue;
|
|
220
|
+
|
|
221
|
+
addUpH += ele.clientHeight / 2;
|
|
222
|
+
if (-deltaTop <= addUpH) break;
|
|
223
|
+
addUpH += ele.clientHeight / 2;
|
|
224
|
+
|
|
225
|
+
swapIndex = i;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
src: target.id,
|
|
229
|
+
dist: upEls.length && swapIndex > -1 ? upEls[swapIndex].id : target.id,
|
|
230
|
+
};
|
|
231
|
+
};
|