@tmagic/stage 1.2.0-beta.2 → 1.2.0-beta.21
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/README.md +62 -1
- package/dist/tmagic-stage.js +2157 -0
- package/dist/tmagic-stage.js.map +1 -0
- package/dist/{tmagic-stage.umd.js → tmagic-stage.umd.cjs} +1436 -993
- package/dist/tmagic-stage.umd.cjs.map +1 -0
- package/package.json +9 -8
- package/src/ActionManager.ts +534 -0
- package/src/DragResizeHelper.ts +338 -0
- package/src/MoveableOptionsManager.ts +249 -0
- package/src/MoveableSelectParentAble.ts +76 -0
- package/src/Rule.ts +13 -9
- package/src/StageCore.ts +220 -260
- package/src/StageDragResize.ts +83 -339
- package/src/StageHighlight.ts +17 -16
- package/src/StageMask.ts +88 -140
- package/src/StageMultiDragResize.ts +97 -198
- package/src/StageRender.ts +90 -15
- package/src/TargetShadow.ts +120 -0
- package/src/const.ts +1 -1
- package/src/types.ts +97 -19
- package/src/util.ts +24 -11
- package/types/ActionManager.d.ts +141 -0
- package/types/DragResizeHelper.d.ts +70 -0
- package/types/MoveableOptionsManager.d.ts +86 -0
- package/types/MoveableSelectParentAble.d.ts +8 -0
- package/types/Rule.d.ts +4 -3
- package/types/StageCore.d.ts +65 -39
- package/types/StageDragResize.d.ts +11 -40
- package/types/StageHighlight.d.ts +3 -4
- package/types/StageMask.d.ts +23 -22
- package/types/StageMultiDragResize.d.ts +8 -24
- package/types/StageRender.d.ts +24 -6
- package/types/TargetShadow.d.ts +22 -0
- package/types/const.d.ts +1 -1
- package/types/types.d.ts +85 -18
- package/types/util.d.ts +9 -8
- package/dist/tmagic-stage.mjs +0 -1715
- package/dist/tmagic-stage.mjs.map +0 -1
- package/dist/tmagic-stage.umd.js.map +0 -1
- package/src/TargetCalibrate.ts +0 -119
- package/types/TargetCalibrate.d.ts +0 -20
|
@@ -0,0 +1,338 @@
|
|
|
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 {
|
|
20
|
+
OnDrag,
|
|
21
|
+
OnDragGroup,
|
|
22
|
+
OnDragGroupStart,
|
|
23
|
+
OnDragStart,
|
|
24
|
+
OnResize,
|
|
25
|
+
OnResizeGroup,
|
|
26
|
+
OnResizeGroupStart,
|
|
27
|
+
OnResizeStart,
|
|
28
|
+
OnRotate,
|
|
29
|
+
OnRotateStart,
|
|
30
|
+
OnScale,
|
|
31
|
+
OnScaleStart,
|
|
32
|
+
} from 'moveable';
|
|
33
|
+
import MoveableHelper from 'moveable-helper';
|
|
34
|
+
|
|
35
|
+
import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const';
|
|
36
|
+
import TargetShadow from './TargetShadow';
|
|
37
|
+
import { DragResizeHelperConfig, TargetElement } from './types';
|
|
38
|
+
import { getAbsolutePosition, getOffset } from './util';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 拖拽/改变大小等操作发生时,moveable会抛出各种状态事件,DragResizeHelper负责响应这些事件,对目标节点target和拖拽节点targetShadow进行修改;
|
|
42
|
+
* 其中目标节点是DragResizeHelper直接改的,targetShadow作为直接被操作的拖拽框,是调用moveableHelper改的;
|
|
43
|
+
* 有个特殊情况是流式布局下,moveableHelper不支持,targetShadow也是DragResizeHelper直接改的
|
|
44
|
+
*/
|
|
45
|
+
export default class DragResizeHelper {
|
|
46
|
+
/** 目标节点在蒙层上的占位节点,用于跟鼠标交互,避免鼠标事件直接作用到目标节点 */
|
|
47
|
+
private targetShadow: TargetShadow;
|
|
48
|
+
/** 要操作的原始目标节点 */
|
|
49
|
+
private target!: HTMLElement;
|
|
50
|
+
/** 多选:目标节点组 */
|
|
51
|
+
private targetList: HTMLElement[] = [];
|
|
52
|
+
/** 响应拖拽的状态事件,修改绝对定位布局下targetShadow的dom。
|
|
53
|
+
* MoveableHelper里面的方法是成员属性,如果DragResizeHelper用继承的方式将无法通过super去调这些方法 */
|
|
54
|
+
private moveableHelper: MoveableHelper;
|
|
55
|
+
/** 流式布局下,目标节点的镜像节点 */
|
|
56
|
+
private ghostEl: HTMLElement | undefined;
|
|
57
|
+
/** 用于记录节点被改变前的位置 */
|
|
58
|
+
private frameSnapShot = {
|
|
59
|
+
left: 0,
|
|
60
|
+
top: 0,
|
|
61
|
+
};
|
|
62
|
+
/** 多选模式下的多个节点 */
|
|
63
|
+
private framesSnapShot: { left: number; top: number; id: string }[] = [];
|
|
64
|
+
/** 布局方式:流式布局、绝对定位、固定定位 */
|
|
65
|
+
private mode: Mode = Mode.ABSOLUTE;
|
|
66
|
+
|
|
67
|
+
constructor(config: DragResizeHelperConfig) {
|
|
68
|
+
this.moveableHelper = MoveableHelper.create({
|
|
69
|
+
useBeforeRender: true,
|
|
70
|
+
useRender: false,
|
|
71
|
+
createAuto: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.targetShadow = new TargetShadow({
|
|
75
|
+
container: config.container,
|
|
76
|
+
updateDragEl: config.updateDragEl,
|
|
77
|
+
zIndex: ZIndex.DRAG_EL,
|
|
78
|
+
idPrefix: DRAG_EL_ID_PREFIX,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public destroy(): void {
|
|
83
|
+
this.targetShadow.destroy();
|
|
84
|
+
this.destroyGhostEl();
|
|
85
|
+
this.moveableHelper.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public destroyShadowEl(): void {
|
|
89
|
+
this.targetShadow.destroyEl();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public getShadowEl(): TargetElement | undefined {
|
|
93
|
+
return this.targetShadow.el;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public updateShadowEl(el: HTMLElement): void {
|
|
97
|
+
this.destroyGhostEl();
|
|
98
|
+
this.target = el;
|
|
99
|
+
this.targetShadow.update(el);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public setMode(mode: Mode): void {
|
|
103
|
+
this.mode = mode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 改变大小事件开始
|
|
108
|
+
* @param e 包含了拖拽节点的dom,moveableHelper会直接修改拖拽节点
|
|
109
|
+
*/
|
|
110
|
+
public onResizeStart(e: OnResizeStart): void {
|
|
111
|
+
this.moveableHelper.onResizeStart(e);
|
|
112
|
+
|
|
113
|
+
this.frameSnapShot.top = this.target.offsetTop;
|
|
114
|
+
this.frameSnapShot.left = this.target.offsetLeft;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public onResize(e: OnResize): void {
|
|
118
|
+
const { width, height, drag } = e;
|
|
119
|
+
const { beforeTranslate } = drag;
|
|
120
|
+
// 流式布局
|
|
121
|
+
if (this.mode === Mode.SORTABLE) {
|
|
122
|
+
this.target.style.top = '0px';
|
|
123
|
+
if (this.targetShadow.el) {
|
|
124
|
+
this.targetShadow.el.style.width = `${width}px`;
|
|
125
|
+
this.targetShadow.el.style.height = `${height}px`;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
this.moveableHelper.onResize(e);
|
|
129
|
+
this.target.style.left = `${this.frameSnapShot.left + beforeTranslate[0]}px`;
|
|
130
|
+
this.target.style.top = `${this.frameSnapShot.top + beforeTranslate[1]}px`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.target.style.width = `${width}px`;
|
|
134
|
+
this.target.style.height = `${height}px`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public onDragStart(e: OnDragStart): void {
|
|
138
|
+
this.moveableHelper.onDragStart(e);
|
|
139
|
+
|
|
140
|
+
if (this.mode === Mode.SORTABLE) {
|
|
141
|
+
this.ghostEl = this.generateGhostEl(this.target);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.frameSnapShot.top = this.target.offsetTop;
|
|
145
|
+
this.frameSnapShot.left = this.target.offsetLeft;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public onDrag(e: OnDrag): void {
|
|
149
|
+
// 流式布局
|
|
150
|
+
if (this.ghostEl) {
|
|
151
|
+
this.ghostEl.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1]}px`;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.moveableHelper.onDrag(e);
|
|
156
|
+
|
|
157
|
+
this.target.style.left = `${this.frameSnapShot.left + e.beforeTranslate[0]}px`;
|
|
158
|
+
this.target.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1]}px`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public onRotateStart(e: OnRotateStart): void {
|
|
162
|
+
this.moveableHelper.onRotateStart(e);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public onRotate(e: OnRotate): void {
|
|
166
|
+
this.moveableHelper.onRotate(e);
|
|
167
|
+
const frame = this.moveableHelper.getFrame(e.target);
|
|
168
|
+
this.target.style.transform = frame?.toCSSObject().transform || '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public onScaleStart(e: OnScaleStart): void {
|
|
172
|
+
this.moveableHelper.onScaleStart(e);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public onScale(e: OnScale): void {
|
|
176
|
+
this.moveableHelper.onScale(e);
|
|
177
|
+
const frame = this.moveableHelper.getFrame(e.target);
|
|
178
|
+
this.target.style.transform = frame?.toCSSObject().transform || '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getGhostEl(): HTMLElement | undefined {
|
|
182
|
+
return this.ghostEl;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public destroyGhostEl(): void {
|
|
186
|
+
this.ghostEl?.remove();
|
|
187
|
+
this.ghostEl = undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public clear(): void {
|
|
191
|
+
this.moveableHelper.clear();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public getFrame(el: HTMLElement | SVGElement): ReturnType<MoveableHelper['getFrame']> {
|
|
195
|
+
return this.moveableHelper.getFrame(el);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public getShadowEls(): TargetElement[] {
|
|
199
|
+
return this.targetShadow.els;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public updateGroup(els: HTMLElement[]): void {
|
|
203
|
+
this.targetList = els;
|
|
204
|
+
this.framesSnapShot = [];
|
|
205
|
+
this.targetShadow.updateGroup(els);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public setTargetList(targetList: HTMLElement[]): void {
|
|
209
|
+
this.targetList = targetList;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public clearMultiSelectStatus(): void {
|
|
213
|
+
this.targetList = [];
|
|
214
|
+
this.targetShadow.destroyEls();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public onResizeGroupStart(e: OnResizeGroupStart): void {
|
|
218
|
+
const { events } = e;
|
|
219
|
+
this.moveableHelper.onResizeGroupStart(e);
|
|
220
|
+
this.setFramesSnapShot(events);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 多选状态下通过拖拽边框改变大小,所有选中组件会一起改变大小
|
|
225
|
+
*/
|
|
226
|
+
public onResizeGroup(e: OnResizeGroup): void {
|
|
227
|
+
const { events } = e;
|
|
228
|
+
// 拖动过程更新
|
|
229
|
+
events.forEach((ev) => {
|
|
230
|
+
const { width, height, beforeTranslate } = ev.drag;
|
|
231
|
+
const frameSnapShot = this.framesSnapShot.find(
|
|
232
|
+
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
233
|
+
);
|
|
234
|
+
if (!frameSnapShot) return;
|
|
235
|
+
const targeEl = this.targetList.find(
|
|
236
|
+
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
237
|
+
);
|
|
238
|
+
if (!targeEl) return;
|
|
239
|
+
// 元素与其所属组同时加入多选列表时,只更新父元素
|
|
240
|
+
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
|
|
241
|
+
|
|
242
|
+
if (!isParentIncluded) {
|
|
243
|
+
// 更新页面元素位置
|
|
244
|
+
targeEl.style.left = `${frameSnapShot.left + beforeTranslate[0]}px`;
|
|
245
|
+
targeEl.style.top = `${frameSnapShot.top + beforeTranslate[1]}px`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 更新页面元素大小
|
|
249
|
+
targeEl.style.width = `${width}px`;
|
|
250
|
+
targeEl.style.height = `${height}px`;
|
|
251
|
+
});
|
|
252
|
+
this.moveableHelper.onResizeGroup(e);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public onDragGroupStart(e: OnDragGroupStart): void {
|
|
256
|
+
const { events } = e;
|
|
257
|
+
this.moveableHelper.onDragGroupStart(e);
|
|
258
|
+
// 记录拖动前快照
|
|
259
|
+
this.setFramesSnapShot(events);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public onDragGroup(e: OnDragGroup): void {
|
|
263
|
+
const { events } = e;
|
|
264
|
+
// 拖动过程更新
|
|
265
|
+
events.forEach((ev) => {
|
|
266
|
+
const frameSnapShot = this.framesSnapShot.find(
|
|
267
|
+
(frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
268
|
+
);
|
|
269
|
+
if (!frameSnapShot) return;
|
|
270
|
+
const targeEl = this.targetList.find(
|
|
271
|
+
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
272
|
+
);
|
|
273
|
+
if (!targeEl) return;
|
|
274
|
+
// 元素与其所属组同时加入多选列表时,只更新父元素
|
|
275
|
+
const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
|
|
276
|
+
if (!isParentIncluded) {
|
|
277
|
+
// 更新页面元素位置
|
|
278
|
+
targeEl.style.left = `${frameSnapShot.left + ev.beforeTranslate[0]}px`;
|
|
279
|
+
targeEl.style.top = `${frameSnapShot.top + ev.beforeTranslate[1]}px`;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.moveableHelper.onDragGroup(e);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 多选状态设置多个节点的快照
|
|
287
|
+
*/
|
|
288
|
+
private setFramesSnapShot(events: OnDragStart[] | OnResizeStart[]): void {
|
|
289
|
+
// 同一组被选中的目标元素多次拖拽和改变大小会触发多次setFramesSnapShot,只有第一次可以设置成功
|
|
290
|
+
if (this.framesSnapShot.length > 0) return;
|
|
291
|
+
// 记录拖动前快照
|
|
292
|
+
events.forEach((ev) => {
|
|
293
|
+
// 实际目标元素
|
|
294
|
+
const matchEventTarget = this.targetList.find(
|
|
295
|
+
(targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
|
|
296
|
+
);
|
|
297
|
+
if (!matchEventTarget) return;
|
|
298
|
+
this.framesSnapShot.push({
|
|
299
|
+
left: matchEventTarget.offsetLeft,
|
|
300
|
+
top: matchEventTarget.offsetTop,
|
|
301
|
+
id: matchEventTarget.id,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 流式布局把目标节点复制一份进行拖拽,在拖拽结束前不影响页面原布局样式
|
|
308
|
+
*/
|
|
309
|
+
private generateGhostEl(el: HTMLElement): HTMLElement {
|
|
310
|
+
if (this.ghostEl) {
|
|
311
|
+
this.destroyGhostEl();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const ghostEl = el.cloneNode(true) as HTMLElement;
|
|
315
|
+
this.setGhostElChildrenId(ghostEl);
|
|
316
|
+
const { top, left } = getAbsolutePosition(el, getOffset(el));
|
|
317
|
+
ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
|
|
318
|
+
ghostEl.style.zIndex = ZIndex.GHOST_EL;
|
|
319
|
+
ghostEl.style.opacity = '.5';
|
|
320
|
+
ghostEl.style.position = 'absolute';
|
|
321
|
+
ghostEl.style.left = `${left}px`;
|
|
322
|
+
ghostEl.style.top = `${top}px`;
|
|
323
|
+
el.after(ghostEl);
|
|
324
|
+
return ghostEl;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private setGhostElChildrenId(el: Element): void {
|
|
328
|
+
for (const child of Array.from(el.children)) {
|
|
329
|
+
if (child.id) {
|
|
330
|
+
child.id = `${GHOST_EL_ID_PREFIX}${child.id}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (child.children.length) {
|
|
334
|
+
this.setGhostElChildrenId(child);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
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 { merge } from 'lodash-es';
|
|
22
|
+
import { MoveableOptions } from 'moveable';
|
|
23
|
+
|
|
24
|
+
import { GuidesType, Mode } from './const';
|
|
25
|
+
import selectParentAbles from './MoveableSelectParentAble';
|
|
26
|
+
import { GetRootContainer, MoveableOptionsManagerConfig } from './types';
|
|
27
|
+
import { getOffset } from './util';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 单选和多选的父类,用于管理moveableOptions
|
|
31
|
+
* @extends EventEmitter
|
|
32
|
+
*/
|
|
33
|
+
export default class MoveableOptionsManager extends EventEmitter {
|
|
34
|
+
/** 布局方式:流式布局、绝对定位、固定定位 */
|
|
35
|
+
public mode: Mode = Mode.ABSOLUTE;
|
|
36
|
+
|
|
37
|
+
/** 画布容器 */
|
|
38
|
+
protected container: HTMLElement;
|
|
39
|
+
|
|
40
|
+
/** 水平参考线 */
|
|
41
|
+
private horizontalGuidelines: number[] = [];
|
|
42
|
+
/** 垂直参考线 */
|
|
43
|
+
private verticalGuidelines: number[] = [];
|
|
44
|
+
/** 对齐元素集合 */
|
|
45
|
+
private elementGuidelines: HTMLElement[] = [];
|
|
46
|
+
/** 由外部调用方(编辑器)传入进来的moveable默认参数,可以为空,也可以是一个回调函数 */
|
|
47
|
+
private customizedOptions?: (() => MoveableOptions) | MoveableOptions;
|
|
48
|
+
/** 获取整个画布的根元素(在StageCore的mount函数中挂载的container) */
|
|
49
|
+
private getRootContainer: GetRootContainer;
|
|
50
|
+
|
|
51
|
+
constructor(config: MoveableOptionsManagerConfig) {
|
|
52
|
+
super();
|
|
53
|
+
this.customizedOptions = config.moveableOptions;
|
|
54
|
+
this.container = config.container;
|
|
55
|
+
this.getRootContainer = config.getRootContainer;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 设置水平/垂直参考线
|
|
60
|
+
* @param type 参考线类型
|
|
61
|
+
* @param guidelines 参考线坐标数组
|
|
62
|
+
*/
|
|
63
|
+
public setGuidelines(type: GuidesType, guidelines: number[]): void {
|
|
64
|
+
if (type === GuidesType.HORIZONTAL) {
|
|
65
|
+
this.horizontalGuidelines = guidelines;
|
|
66
|
+
} else if (type === GuidesType.VERTICAL) {
|
|
67
|
+
this.verticalGuidelines = guidelines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.emit('update-moveable');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 清除横向和纵向的参考线
|
|
75
|
+
*/
|
|
76
|
+
public clearGuides(): void {
|
|
77
|
+
this.horizontalGuidelines = [];
|
|
78
|
+
this.verticalGuidelines = [];
|
|
79
|
+
|
|
80
|
+
this.emit('update-moveable');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 设置有哪些元素要辅助对齐
|
|
85
|
+
* @param selectedElList 选中的元素列表,需要排除在对齐元素之外
|
|
86
|
+
* @param allElList 全部元素列表
|
|
87
|
+
*/
|
|
88
|
+
protected setElementGuidelines(selectedElList: HTMLElement[], allElList: HTMLElement[]): void {
|
|
89
|
+
this.elementGuidelines.forEach((node) => {
|
|
90
|
+
node.remove();
|
|
91
|
+
});
|
|
92
|
+
this.elementGuidelines = [];
|
|
93
|
+
|
|
94
|
+
if (this.mode === Mode.ABSOLUTE) {
|
|
95
|
+
this.container.append(this.createGuidelineElements(selectedElList, allElList));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 获取moveable参数
|
|
101
|
+
* @param isMultiSelect 是否多选模式
|
|
102
|
+
* @param runtimeOptions 调用时实时传进来的的moveable参数
|
|
103
|
+
* @returns moveable所需参数
|
|
104
|
+
*/
|
|
105
|
+
protected getOptions(isMultiSelect: boolean, runtimeOptions: MoveableOptions = {}): MoveableOptions {
|
|
106
|
+
const defaultOptions = this.getDefaultOptions(isMultiSelect);
|
|
107
|
+
const customizedOptions = this.getCustomizeOptions();
|
|
108
|
+
|
|
109
|
+
return merge(defaultOptions, customizedOptions, runtimeOptions);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取单选和多选的moveable公共参数
|
|
114
|
+
* @returns moveable公共参数
|
|
115
|
+
*/
|
|
116
|
+
private getDefaultOptions(isMultiSelect: boolean): MoveableOptions {
|
|
117
|
+
const isSortable = this.mode === Mode.SORTABLE;
|
|
118
|
+
|
|
119
|
+
const commonOptions = {
|
|
120
|
+
draggable: true,
|
|
121
|
+
resizable: true,
|
|
122
|
+
rootContainer: this.getRootContainer(),
|
|
123
|
+
zoom: 1,
|
|
124
|
+
throttleDrag: 0,
|
|
125
|
+
snappable: true,
|
|
126
|
+
horizontalGuidelines: this.horizontalGuidelines,
|
|
127
|
+
verticalGuidelines: this.verticalGuidelines,
|
|
128
|
+
elementGuidelines: this.elementGuidelines,
|
|
129
|
+
bounds: {
|
|
130
|
+
top: 0,
|
|
131
|
+
// 设置0的话无法移动到left为0,所以只能设置为-1
|
|
132
|
+
left: -1,
|
|
133
|
+
right: this.container.clientWidth - 1,
|
|
134
|
+
bottom: isSortable ? undefined : this.container.clientHeight,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const differenceOptions = isMultiSelect ? this.getMultiOptions() : this.getSingleOptions();
|
|
138
|
+
|
|
139
|
+
return merge(commonOptions, differenceOptions);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 获取单选下的差异化参数
|
|
144
|
+
* @returns {MoveableOptions} moveable options参数
|
|
145
|
+
*/
|
|
146
|
+
private getSingleOptions(): MoveableOptions {
|
|
147
|
+
const isAbsolute = this.mode === Mode.ABSOLUTE;
|
|
148
|
+
const isFixed = this.mode === Mode.FIXED;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
origin: false,
|
|
152
|
+
dragArea: false,
|
|
153
|
+
scalable: false,
|
|
154
|
+
rotatable: false,
|
|
155
|
+
snapGap: isAbsolute || isFixed,
|
|
156
|
+
snapThreshold: 5,
|
|
157
|
+
snapDigit: 0,
|
|
158
|
+
isDisplaySnapDigit: isAbsolute,
|
|
159
|
+
snapDirections: {
|
|
160
|
+
top: isAbsolute,
|
|
161
|
+
right: isAbsolute,
|
|
162
|
+
bottom: isAbsolute,
|
|
163
|
+
left: isAbsolute,
|
|
164
|
+
center: isAbsolute,
|
|
165
|
+
middle: isAbsolute,
|
|
166
|
+
},
|
|
167
|
+
elementSnapDirections: {
|
|
168
|
+
top: isAbsolute,
|
|
169
|
+
right: isAbsolute,
|
|
170
|
+
bottom: isAbsolute,
|
|
171
|
+
left: isAbsolute,
|
|
172
|
+
},
|
|
173
|
+
isDisplayInnerSnapDigit: true,
|
|
174
|
+
|
|
175
|
+
props: {
|
|
176
|
+
selectParent: true,
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
ables: [selectParentAbles(this.selectParentHandler.bind(this))],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 获取多选下的差异化参数
|
|
185
|
+
* @returns {MoveableOptions} moveable options参数
|
|
186
|
+
*/
|
|
187
|
+
private getMultiOptions(): MoveableOptions {
|
|
188
|
+
return {
|
|
189
|
+
defaultGroupRotate: 0,
|
|
190
|
+
defaultGroupOrigin: '50% 50%',
|
|
191
|
+
startDragRotate: 0,
|
|
192
|
+
throttleDragRotate: 0,
|
|
193
|
+
origin: true,
|
|
194
|
+
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 获取业务方自定义的moveable参数
|
|
200
|
+
*/
|
|
201
|
+
private getCustomizeOptions(): MoveableOptions | undefined {
|
|
202
|
+
if (typeof this.customizedOptions === 'function') {
|
|
203
|
+
return this.customizedOptions();
|
|
204
|
+
}
|
|
205
|
+
return this.customizedOptions;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 这是给selectParentAbles的回调函数,用于触发选中父元素事件
|
|
210
|
+
*/
|
|
211
|
+
private selectParentHandler(): void {
|
|
212
|
+
this.emit('select-parent');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 为需要辅助对齐的元素创建div
|
|
217
|
+
* @param selectedElList 选中的元素列表,需要排除在对齐元素之外
|
|
218
|
+
* @param allElList 全部元素列表
|
|
219
|
+
* @returns frame 辅助对齐元素集合的页面片
|
|
220
|
+
*/
|
|
221
|
+
private createGuidelineElements(selectedElList: HTMLElement[], allElList: HTMLElement[]): DocumentFragment {
|
|
222
|
+
const frame = globalThis.document.createDocumentFragment();
|
|
223
|
+
|
|
224
|
+
for (const node of allElList) {
|
|
225
|
+
const { width, height } = node.getBoundingClientRect();
|
|
226
|
+
if (this.isInElementList(node, selectedElList)) continue;
|
|
227
|
+
const { left, top } = getOffset(node);
|
|
228
|
+
const elementGuideline = globalThis.document.createElement('div');
|
|
229
|
+
elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
|
|
230
|
+
this.elementGuidelines.push(elementGuideline);
|
|
231
|
+
frame.append(elementGuideline);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return frame;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 判断一个元素是否在元素列表里面
|
|
239
|
+
* @param ele 元素
|
|
240
|
+
* @param eleList 元素列表
|
|
241
|
+
* @returns 是否在元素列表里面
|
|
242
|
+
*/
|
|
243
|
+
private isInElementList(ele: HTMLElement, eleList: HTMLElement[]): boolean {
|
|
244
|
+
for (const eleItem of eleList) {
|
|
245
|
+
if (ele === eleItem) return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { MoveableManagerInterface, Renderer } from 'moveable';
|
|
2
|
+
|
|
3
|
+
export default (selectParentHandler: () => void) => ({
|
|
4
|
+
name: 'select-parent',
|
|
5
|
+
props: {},
|
|
6
|
+
events: {},
|
|
7
|
+
render(moveable: MoveableManagerInterface<any, any>, React: Renderer) {
|
|
8
|
+
const rect = moveable.getRect();
|
|
9
|
+
const { pos2 } = moveable.state;
|
|
10
|
+
|
|
11
|
+
// use css for able
|
|
12
|
+
const editableViewer = moveable.useCSS(
|
|
13
|
+
'div',
|
|
14
|
+
`
|
|
15
|
+
{
|
|
16
|
+
position: absolute;
|
|
17
|
+
left: 0px;
|
|
18
|
+
top: 0px;
|
|
19
|
+
will-change: transform;
|
|
20
|
+
transform-origin: 0px 0px;
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
.moveable-button {
|
|
24
|
+
width: 20px;
|
|
25
|
+
height: 20px;
|
|
26
|
+
background: #4af;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
appearance: none;
|
|
29
|
+
border: 0;
|
|
30
|
+
color: white;
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
);
|
|
36
|
+
// Add key (required)
|
|
37
|
+
// Add class prefix moveable-(required)
|
|
38
|
+
return React.createElement(
|
|
39
|
+
editableViewer,
|
|
40
|
+
{
|
|
41
|
+
className: 'moveable-editable',
|
|
42
|
+
style: {
|
|
43
|
+
transform: `translate(${pos2[0] - 25}px, ${pos2[1] - 30}px) rotate(${rect.rotation}deg) translate(10px)`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
React.createElement(
|
|
47
|
+
'button',
|
|
48
|
+
{
|
|
49
|
+
className: 'moveable-button',
|
|
50
|
+
title: '选中父组件',
|
|
51
|
+
onClick: () => {
|
|
52
|
+
selectParentHandler();
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
React.createElement(
|
|
56
|
+
'svg',
|
|
57
|
+
{
|
|
58
|
+
width: '1em',
|
|
59
|
+
height: '1em',
|
|
60
|
+
viewBox: '0 0 16 16',
|
|
61
|
+
fill: 'none',
|
|
62
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
63
|
+
style: {
|
|
64
|
+
transform: 'rotate(90deg)',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
React.createElement('path', {
|
|
68
|
+
d: 'M13.0001 4V10H4.20718L5.85363 8.35355L5.14652 7.64645L2.64652 10.1464C2.45126 10.3417 2.45126 10.6583 2.64652 10.8536L5.14652 13.3536L5.85363 12.6464L4.20718 11H13.0001C13.5524 11 14.0001 10.5523 14.0001 10V4H13.0001Z',
|
|
69
|
+
fill: 'currentColor',
|
|
70
|
+
fillOpacity: '0.9',
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
});
|