@tmagic/stage 1.4.8 → 1.4.10

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.
@@ -0,0 +1,396 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2023 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 type {
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 { calcValueByFontsize } from '@tmagic/utils';
36
+
37
+ import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, Mode, ZIndex } from './const';
38
+ import TargetShadow from './TargetShadow';
39
+ import type { DragResizeHelperConfig, Rect, TargetElement } from './types';
40
+ import { getAbsolutePosition, getBorderWidth, getMarginValue, getOffset } from './util';
41
+
42
+ /**
43
+ * 拖拽/改变大小等操作发生时,moveable会抛出各种状态事件,DragResizeHelper负责响应这些事件,对目标节点target和拖拽节点targetShadow进行修改;
44
+ * 其中目标节点是DragResizeHelper直接改的,targetShadow作为直接被操作的拖拽框,是调用moveableHelper改的;
45
+ * 有个特殊情况是流式布局下,moveableHelper不支持,targetShadow也是DragResizeHelper直接改的
46
+ */
47
+ export default class DragResizeHelper {
48
+ /** 目标节点在蒙层上的占位节点,用于跟鼠标交互,避免鼠标事件直接作用到目标节点 */
49
+ private targetShadow: TargetShadow;
50
+ /** 要操作的原始目标节点 */
51
+ private target!: HTMLElement;
52
+ /** 多选:目标节点组 */
53
+ private targetList: HTMLElement[] = [];
54
+ /** 响应拖拽的状态事件,修改绝对定位布局下targetShadow的dom。
55
+ * MoveableHelper里面的方法是成员属性,如果DragResizeHelper用继承的方式将无法通过super去调这些方法 */
56
+ private moveableHelper: MoveableHelper;
57
+ /** 流式布局下,目标节点的镜像节点 */
58
+ private ghostEl: HTMLElement | undefined;
59
+ /** 用于记录节点被改变前的位置 */
60
+ private frameSnapShot = {
61
+ left: 0,
62
+ top: 0,
63
+ };
64
+ /** 多选模式下的多个节点 */
65
+ private framesSnapShot: { left: number; top: number; id: string }[] = [];
66
+ /** 布局方式:流式布局、绝对定位、固定定位 */
67
+ private mode: Mode = Mode.ABSOLUTE;
68
+
69
+ constructor(config: DragResizeHelperConfig) {
70
+ this.moveableHelper = MoveableHelper.create({
71
+ useBeforeRender: true,
72
+ useRender: false,
73
+ createAuto: true,
74
+ });
75
+
76
+ this.targetShadow = new TargetShadow({
77
+ container: config.container,
78
+ updateDragEl: config.updateDragEl,
79
+ zIndex: ZIndex.DRAG_EL,
80
+ idPrefix: DRAG_EL_ID_PREFIX,
81
+ });
82
+ }
83
+
84
+ public destroy(): void {
85
+ this.targetShadow.destroy();
86
+ this.destroyGhostEl();
87
+ this.moveableHelper.clear();
88
+ }
89
+
90
+ public destroyShadowEl(): void {
91
+ this.targetShadow.destroyEl();
92
+ }
93
+
94
+ public getShadowEl(): TargetElement | undefined {
95
+ return this.targetShadow.el;
96
+ }
97
+
98
+ public updateShadowEl(el: HTMLElement): void {
99
+ this.destroyGhostEl();
100
+ this.target = el;
101
+ this.targetShadow.update(el);
102
+ }
103
+
104
+ public setMode(mode: Mode): void {
105
+ this.mode = mode;
106
+ }
107
+
108
+ /**
109
+ * 改变大小事件开始
110
+ * @param e 包含了拖拽节点的dom,moveableHelper会直接修改拖拽节点
111
+ */
112
+ public onResizeStart(e: OnResizeStart): void {
113
+ this.moveableHelper.onResizeStart(e);
114
+
115
+ this.frameSnapShot.top = this.target.offsetTop;
116
+ this.frameSnapShot.left = this.target.offsetLeft;
117
+ }
118
+
119
+ public onResize(e: OnResize): void {
120
+ const { width, height, drag } = e;
121
+ const { beforeTranslate } = drag;
122
+ // 流式布局
123
+ if (this.mode === Mode.SORTABLE) {
124
+ this.target.style.top = '0px';
125
+ if (this.targetShadow.el) {
126
+ this.targetShadow.el.style.width = `${width}px`;
127
+ this.targetShadow.el.style.height = `${height}px`;
128
+ }
129
+ } else {
130
+ this.moveableHelper.onResize(e);
131
+ const { marginLeft, marginTop } = getMarginValue(this.target);
132
+ this.target.style.left = `${this.frameSnapShot.left + beforeTranslate[0] - marginLeft}px`;
133
+ this.target.style.top = `${this.frameSnapShot.top + beforeTranslate[1] - marginTop}px`;
134
+ }
135
+
136
+ const { borderLeftWidth, borderRightWidth, borderTopWidth, borderBottomWidth } = getBorderWidth(this.target);
137
+
138
+ this.target.style.width = `${width + borderLeftWidth + borderRightWidth}px`;
139
+ this.target.style.height = `${height + borderTopWidth + borderBottomWidth}px`;
140
+ }
141
+
142
+ public onDragStart(e: OnDragStart): void {
143
+ this.moveableHelper.onDragStart(e);
144
+
145
+ if (this.mode === Mode.SORTABLE) {
146
+ this.ghostEl = this.generateGhostEl(this.target);
147
+ }
148
+
149
+ this.frameSnapShot.top = this.target.offsetTop;
150
+ this.frameSnapShot.left = this.target.offsetLeft;
151
+ }
152
+
153
+ public onDrag(e: OnDrag): void {
154
+ // 流式布局
155
+ if (this.ghostEl) {
156
+ this.ghostEl.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1]}px`;
157
+ return;
158
+ }
159
+
160
+ this.moveableHelper.onDrag(e);
161
+
162
+ const { marginLeft, marginTop } = getMarginValue(this.target);
163
+
164
+ this.target.style.left = `${this.frameSnapShot.left + e.beforeTranslate[0] - marginLeft}px`;
165
+ this.target.style.top = `${this.frameSnapShot.top + e.beforeTranslate[1] - marginTop}px`;
166
+ }
167
+
168
+ public onRotateStart(e: OnRotateStart): void {
169
+ this.moveableHelper.onRotateStart(e);
170
+ }
171
+
172
+ public onRotate(e: OnRotate): void {
173
+ this.moveableHelper.onRotate(e);
174
+ const frame = this.moveableHelper.getFrame(e.target);
175
+ this.target.style.transform = frame?.toCSSObject().transform || '';
176
+ }
177
+
178
+ public onScaleStart(e: OnScaleStart): void {
179
+ this.moveableHelper.onScaleStart(e);
180
+ }
181
+
182
+ public onScale(e: OnScale): void {
183
+ this.moveableHelper.onScale(e);
184
+ const frame = this.moveableHelper.getFrame(e.target);
185
+ this.target.style.transform = frame?.toCSSObject().transform || '';
186
+ }
187
+
188
+ public getGhostEl(): HTMLElement | undefined {
189
+ return this.ghostEl;
190
+ }
191
+
192
+ public destroyGhostEl(): void {
193
+ this.ghostEl?.remove();
194
+ this.ghostEl = undefined;
195
+ }
196
+
197
+ public clear(): void {
198
+ this.moveableHelper.clear();
199
+ }
200
+
201
+ public getFrame(el: HTMLElement | SVGElement): ReturnType<MoveableHelper['getFrame']> {
202
+ return this.moveableHelper.getFrame(el);
203
+ }
204
+
205
+ public getShadowEls(): TargetElement[] {
206
+ return this.targetShadow.els;
207
+ }
208
+
209
+ public updateGroup(els: HTMLElement[]): void {
210
+ this.targetList = els;
211
+ this.framesSnapShot = [];
212
+ this.targetShadow.updateGroup(els);
213
+ }
214
+
215
+ public setTargetList(targetList: HTMLElement[]): void {
216
+ this.targetList = targetList;
217
+ }
218
+
219
+ public clearMultiSelectStatus(): void {
220
+ this.targetList = [];
221
+ this.targetShadow.destroyEls();
222
+ }
223
+
224
+ public onResizeGroupStart(e: OnResizeGroupStart): void {
225
+ const { events } = e;
226
+ this.moveableHelper.onResizeGroupStart(e);
227
+ this.setFramesSnapShot(events);
228
+ }
229
+
230
+ /**
231
+ * 多选状态下通过拖拽边框改变大小,所有选中组件会一起改变大小
232
+ */
233
+ public onResizeGroup(e: OnResizeGroup): void {
234
+ const { events } = e;
235
+
236
+ this.moveableHelper.onResizeGroup(e);
237
+
238
+ // 拖动过程更新
239
+ events.forEach((ev) => {
240
+ const { width, height, beforeTranslate } = ev.drag;
241
+ const frameSnapShot = this.framesSnapShot.find(
242
+ (frameItem) => frameItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
243
+ );
244
+ if (!frameSnapShot) return;
245
+ const targeEl = this.targetList.find(
246
+ (targetItem) => targetItem.id === ev.target.id.replace(DRAG_EL_ID_PREFIX, ''),
247
+ );
248
+ if (!targeEl) return;
249
+ // 元素与其所属组同时加入多选列表时,只更新父元素
250
+ const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
251
+
252
+ if (!isParentIncluded) {
253
+ // 更新页面元素位置
254
+ const { marginLeft, marginTop } = getMarginValue(targeEl);
255
+ targeEl.style.left = `${frameSnapShot.left + beforeTranslate[0] - marginLeft}px`;
256
+ targeEl.style.top = `${frameSnapShot.top + beforeTranslate[1] - marginTop}px`;
257
+ }
258
+
259
+ // 更新页面元素大小
260
+ targeEl.style.width = `${width}px`;
261
+ targeEl.style.height = `${height}px`;
262
+ });
263
+ }
264
+
265
+ public onDragGroupStart(e: OnDragGroupStart): void {
266
+ this.moveableHelper.onDragGroupStart(e);
267
+
268
+ const { events } = e;
269
+ // 记录拖动前快照
270
+ this.setFramesSnapShot(events);
271
+ }
272
+
273
+ public onDragGroup(e: OnDragGroup): void {
274
+ this.moveableHelper.onDragGroup(e);
275
+
276
+ const { events } = e;
277
+ // 拖动过程更新
278
+ events.forEach((ev) => {
279
+ const frameSnapShot = this.framesSnapShot.find(
280
+ (frameItem) => ev.target.id.startsWith(DRAG_EL_ID_PREFIX) && ev.target.id.endsWith(frameItem.id),
281
+ );
282
+ if (!frameSnapShot) return;
283
+ const targeEl = this.targetList.find(
284
+ (targetItem) => ev.target.id.startsWith(DRAG_EL_ID_PREFIX) && ev.target.id.endsWith(targetItem.id),
285
+ );
286
+ if (!targeEl) return;
287
+ // 元素与其所属组同时加入多选列表时,只更新父元素
288
+ const isParentIncluded = this.targetList.find((targetItem) => targetItem.id === targeEl.parentElement?.id);
289
+ if (!isParentIncluded) {
290
+ // 更新页面元素位置
291
+ const { marginLeft, marginTop } = getMarginValue(targeEl);
292
+ targeEl.style.left = `${frameSnapShot.left + ev.beforeTranslate[0] - marginLeft}px`;
293
+ targeEl.style.top = `${frameSnapShot.top + ev.beforeTranslate[1] - marginTop}px`;
294
+ }
295
+ });
296
+ }
297
+
298
+ public getUpdatedElRect(el: HTMLElement, parentEl: HTMLElement | null, doc: Document): Rect {
299
+ const offset = this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : { left: el.offsetLeft, top: el.offsetTop };
300
+
301
+ const { marginLeft, marginTop } = getMarginValue(el);
302
+
303
+ let left = calcValueByFontsize(doc, offset.left) - marginLeft;
304
+ let top = calcValueByFontsize(doc, offset.top) - marginTop;
305
+
306
+ const { borderLeftWidth, borderRightWidth, borderTopWidth, borderBottomWidth } = getBorderWidth(el);
307
+
308
+ const width = calcValueByFontsize(doc, el.clientWidth + borderLeftWidth + borderRightWidth);
309
+ const height = calcValueByFontsize(doc, el.clientHeight + borderTopWidth + borderBottomWidth);
310
+
311
+ let shadowEl = this.getShadowEl();
312
+ const shadowEls = this.getShadowEls();
313
+
314
+ if (shadowEls.length) {
315
+ shadowEl = shadowEls.find((item) => item.id.endsWith(el.id));
316
+ }
317
+
318
+ if (parentEl && this.mode === Mode.ABSOLUTE && shadowEl) {
319
+ const targetShadowHtmlEl = shadowEl as HTMLElement;
320
+ const targetShadowElOffsetLeft = targetShadowHtmlEl.offsetLeft || 0;
321
+ const targetShadowElOffsetTop = targetShadowHtmlEl.offsetTop || 0;
322
+
323
+ const frame = this.getFrame(shadowEl);
324
+
325
+ const [translateX, translateY] = frame?.properties.transform.translate.value;
326
+ const { left: parentLeft, top: parentTop } = getOffset(parentEl);
327
+
328
+ left =
329
+ calcValueByFontsize(doc, targetShadowElOffsetLeft) +
330
+ parseFloat(translateX) -
331
+ calcValueByFontsize(doc, parentLeft);
332
+ top =
333
+ calcValueByFontsize(doc, targetShadowElOffsetTop) +
334
+ parseFloat(translateY) -
335
+ calcValueByFontsize(doc, parentTop);
336
+ }
337
+
338
+ return { width, height, left, top };
339
+ }
340
+
341
+ /**
342
+ * 多选状态设置多个节点的快照
343
+ */
344
+ private setFramesSnapShot(events: OnDragStart[] | OnResizeStart[]): void {
345
+ // 同一组被选中的目标元素多次拖拽和改变大小会触发多次setFramesSnapShot,只有第一次可以设置成功
346
+ if (this.framesSnapShot.length > 0) return;
347
+ // 记录拖动前快照
348
+ events.forEach((ev) => {
349
+ // 实际目标元素
350
+ const matchEventTarget = this.targetList.find(
351
+ (targetItem) => ev.target.id.startsWith(DRAG_EL_ID_PREFIX) && ev.target.id.endsWith(targetItem.id),
352
+ );
353
+ if (!matchEventTarget) return;
354
+ this.framesSnapShot.push({
355
+ left: matchEventTarget.offsetLeft,
356
+ top: matchEventTarget.offsetTop,
357
+ id: matchEventTarget.id,
358
+ });
359
+ });
360
+ }
361
+
362
+ /**
363
+ * 流式布局把目标节点复制一份进行拖拽,在拖拽结束前不影响页面原布局样式
364
+ */
365
+ private generateGhostEl(el: HTMLElement): HTMLElement {
366
+ if (this.ghostEl) {
367
+ this.destroyGhostEl();
368
+ }
369
+
370
+ const ghostEl = el.cloneNode(true) as HTMLElement;
371
+ this.setGhostElChildrenId(ghostEl);
372
+ const { top, left } = getAbsolutePosition(el, getOffset(el));
373
+ ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
374
+ ghostEl.style.zIndex = ZIndex.GHOST_EL;
375
+ ghostEl.style.opacity = '.5';
376
+ ghostEl.style.position = 'absolute';
377
+ ghostEl.style.left = `${left}px`;
378
+ ghostEl.style.top = `${top}px`;
379
+ ghostEl.style.marginLeft = '0';
380
+ ghostEl.style.marginTop = '0';
381
+ el.after(ghostEl);
382
+ return ghostEl;
383
+ }
384
+
385
+ private setGhostElChildrenId(el: Element): void {
386
+ for (const child of Array.from(el.children)) {
387
+ if (child.id) {
388
+ child.id = `${GHOST_EL_ID_PREFIX}${child.id}`;
389
+ }
390
+
391
+ if (child.children.length) {
392
+ this.setGhostElChildrenId(child);
393
+ }
394
+ }
395
+ }
396
+ }
@@ -0,0 +1,91 @@
1
+ import { MoveableManagerInterface, Renderer } from 'moveable';
2
+
3
+ import { AbleActionEventType } from './const';
4
+ import ableCss from './moveable-able.css?raw';
5
+
6
+ export default (handler: (type: AbleActionEventType) => void) => ({
7
+ name: 'actions',
8
+ props: [],
9
+ events: [],
10
+ render(moveable: MoveableManagerInterface<any, any>, React: Renderer) {
11
+ const rect = moveable.getRect();
12
+ const { pos2 } = moveable.state;
13
+
14
+ // use css for able
15
+ const editableViewer = moveable.useCSS(
16
+ 'div',
17
+ `
18
+ {
19
+ position: absolute;
20
+ left: 0px;
21
+ top: 0px;
22
+ will-change: transform;
23
+ transform-origin: 60px 28px;
24
+ display: flex;
25
+ }
26
+ ${ableCss}
27
+ `,
28
+ );
29
+ // Add key (required)
30
+ // Add class prefix moveable-(required)
31
+ return React.createElement(
32
+ editableViewer,
33
+ {
34
+ className: 'moveable-editable',
35
+ style: {
36
+ transform: `translate(${pos2[0] - 60}px, ${pos2[1] - 28}px) rotate(${rect.rotation}deg)`,
37
+ },
38
+ },
39
+ [
40
+ React.createElement(
41
+ 'button',
42
+ {
43
+ className: 'moveable-button',
44
+ title: '选中父组件',
45
+ onClick: () => {
46
+ handler(AbleActionEventType.SELECT_PARENT);
47
+ },
48
+ },
49
+ React.createElement('div', {
50
+ className: 'moveable-select-parent-arrow-top-icon',
51
+ }),
52
+ React.createElement('div', {
53
+ className: 'moveable-select-parent-arrow-body-icon',
54
+ }),
55
+ ),
56
+ React.createElement('button', {
57
+ className: 'moveable-button moveable-remove-button',
58
+ title: '删除',
59
+ onClick: () => {
60
+ handler(AbleActionEventType.REMOVE);
61
+ },
62
+ }),
63
+ React.createElement(
64
+ 'button',
65
+ {
66
+ className: 'moveable-button moveable-drag-area-button',
67
+ title: '拖动',
68
+ },
69
+ React.createElement('div', {
70
+ className: 'moveable-select-parent-arrow-top-icon moveable-select-parent-arrow-top-icon-top',
71
+ }),
72
+ React.createElement('div', {
73
+ className: 'moveable-select-parent-arrow-top-icon moveable-select-parent-arrow-top-icon-bottom',
74
+ }),
75
+ React.createElement('div', {
76
+ className: 'moveable-select-parent-arrow-top-icon moveable-select-parent-arrow-top-icon-left',
77
+ }),
78
+ React.createElement('div', {
79
+ className: ' moveable-select-parent-arrow-top-icon moveable-select-parent-arrow-top-icon-right',
80
+ }),
81
+ React.createElement('div', {
82
+ className: 'moveable-select-parent-arrow-body-icon-horizontal',
83
+ }),
84
+ React.createElement('div', {
85
+ className: 'moveable-select-parent-arrow-body-icon-vertical',
86
+ }),
87
+ ),
88
+ ],
89
+ );
90
+ },
91
+ });