@tmagic/stage 1.0.0-beta.9 → 1.0.0-rc.11

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.
@@ -23,14 +23,18 @@ import type { MoveableOptions } from 'moveable';
23
23
  import Moveable from 'moveable';
24
24
  import MoveableHelper from 'moveable-helper';
25
25
 
26
- import { GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
26
+ import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode, ZIndex } from './const';
27
27
  import StageCore from './StageCore';
28
28
  import type { SortEventData, StageDragResizeConfig } from './types';
29
- import { getAbsolutePosition, getMode, getOffset } from './util';
29
+ import { getAbsolutePosition, getGuideLineFromCache, getMode, getOffset } from './util';
30
30
 
31
+ /** 拖动状态 */
31
32
  enum ActionStatus {
33
+ /** 开始拖动 */
32
34
  START = 'start',
35
+ /** 拖动中 */
33
36
  ING = 'ing',
37
+ /** 拖动结束 */
34
38
  END = 'end',
35
39
  }
36
40
 
@@ -39,17 +43,28 @@ enum ActionStatus {
39
43
  */
40
44
  export default class StageDragResize extends EventEmitter {
41
45
  public core: StageCore;
46
+ /** 画布容器 */
42
47
  public container: HTMLElement;
48
+ /** 目标节点 */
43
49
  public target?: HTMLElement;
44
- public dragEl?: HTMLElement;
50
+ /** 目标节点在蒙层中的占位节点 */
51
+ public dragEl: HTMLDivElement;
52
+ /** Moveable拖拽类实例 */
45
53
  public moveable?: Moveable;
46
- public horizontalGuidelines: number[] = [];
47
- public verticalGuidelines: number[] = [];
54
+ /** 水平参考线 */
55
+ public horizontalGuidelines: number[] = getGuideLineFromCache(GuidesType.HORIZONTAL);
56
+ /** 垂直参考线 */
57
+ public verticalGuidelines: number[] = getGuideLineFromCache(GuidesType.VERTICAL);
58
+ /** 对齐元素集合 */
59
+ public elementGuidelines: HTMLElement[] = [];
60
+ /** 布局方式:流式布局、绝对定位、固定定位 */
61
+ public mode: Mode = Mode.ABSOLUTE;
48
62
 
49
63
  private moveableOptions: MoveableOptions = {};
64
+ /** 拖动状态 */
50
65
  private dragStatus: ActionStatus = ActionStatus.END;
66
+ /** 流式布局下,目标节点的镜像节点 */
51
67
  private ghostEl: HTMLElement | undefined;
52
- private mode: Mode = Mode.ABSOLUTE;
53
68
  private moveableHelper?: MoveableHelper;
54
69
 
55
70
  constructor(config: StageDragResizeConfig) {
@@ -57,6 +72,9 @@ export default class StageDragResize extends EventEmitter {
57
72
 
58
73
  this.core = config.core;
59
74
  this.container = config.container;
75
+
76
+ this.dragEl = globalThis.document.createElement('div');
77
+ this.container.append(this.dragEl);
60
78
  }
61
79
 
62
80
  /**
@@ -65,29 +83,23 @@ export default class StageDragResize extends EventEmitter {
65
83
  * @param el 选中组件的Dom节点元素
66
84
  * @param event 鼠标事件
67
85
  */
68
- public async select(el: HTMLElement, event?: MouseEvent): Promise<void> {
86
+ public select(el: HTMLElement, event?: MouseEvent): void {
87
+ const oldTarget = this.target;
69
88
  this.target = el;
70
- // 如果有滚动条会导致resize时获取到width,height不准确
71
- if (/(auto|scroll)/.test(this.target.style.overflow)) {
72
- this.target.style.overflow = 'hidden';
73
- }
74
- this.mode = getMode(el);
75
- this.destroyDragEl();
76
- this.destroyGhostEl();
77
89
 
78
- this.dragEl = this.generateDragEl(el);
79
-
80
- this.moveableOptions = await this.getOptions({
81
- target: this.dragEl || this.target,
82
- });
83
-
84
- this.moveableHelper = MoveableHelper.create({
85
- useBeforeRender: true,
86
- useRender: false,
87
- createAuto: true,
88
- });
90
+ // 从不能拖动到能拖动的节点之间切换,要重新创建moveable,不然dragStart不生效
91
+ if (!this.moveable || this.target !== oldTarget) {
92
+ this.init(el);
93
+ this.moveableHelper = MoveableHelper.create({
94
+ useBeforeRender: true,
95
+ useRender: false,
96
+ createAuto: true,
97
+ });
89
98
 
90
- this.initMoveable();
99
+ this.initMoveable();
100
+ } else {
101
+ this.updateMoveable();
102
+ }
91
103
 
92
104
  if (event) {
93
105
  this.moveable?.dragStart(event);
@@ -97,11 +109,15 @@ export default class StageDragResize extends EventEmitter {
97
109
  /**
98
110
  * 初始化选中框并渲染出来
99
111
  */
100
- public async refresh() {
112
+ public updateMoveable(el = this.target): void {
101
113
  if (!this.moveable) throw new Error('未初始化moveable');
114
+ if (!el) throw new Error('为选中任何节点');
115
+
116
+ this.target = el;
102
117
 
103
- const options = await this.getOptions();
104
- Object.entries(options).forEach(([key, value]) => {
118
+ this.init(el);
119
+
120
+ Object.entries(this.moveableOptions).forEach(([key, value]) => {
105
121
  (this.moveable as any)[key] = value;
106
122
  });
107
123
  this.moveable.updateTarget();
@@ -110,17 +126,21 @@ export default class StageDragResize extends EventEmitter {
110
126
  public setGuidelines(type: GuidesType, guidelines: number[]): void {
111
127
  if (type === GuidesType.HORIZONTAL) {
112
128
  this.horizontalGuidelines = guidelines;
129
+ this.moveableOptions.horizontalGuidelines = guidelines;
113
130
  } else if (type === GuidesType.VERTICAL) {
114
131
  this.verticalGuidelines = guidelines;
132
+ this.moveableOptions.verticalGuidelines = guidelines;
115
133
  }
116
134
 
117
- this.refresh();
135
+ this.updateMoveable();
118
136
  }
119
137
 
120
138
  public clearGuides() {
121
- this.verticalGuidelines = [];
122
139
  this.horizontalGuidelines = [];
123
- this.refresh();
140
+ this.verticalGuidelines = [];
141
+ this.moveableOptions.horizontalGuidelines = [];
142
+ this.moveableOptions.verticalGuidelines = [];
143
+ this.updateMoveable();
124
144
  }
125
145
 
126
146
  /**
@@ -134,6 +154,45 @@ export default class StageDragResize extends EventEmitter {
134
154
  this.removeAllListeners();
135
155
  }
136
156
 
157
+ private init(el: HTMLElement): void {
158
+ // 如果有滚动条会导致resize时获取到width,height不准确
159
+ if (/(auto|scroll)/.test(el.style.overflow)) {
160
+ el.style.overflow = 'hidden';
161
+ }
162
+ this.mode = getMode(el);
163
+
164
+ this.destroyGhostEl();
165
+
166
+ this.updateDragEl(el);
167
+
168
+ this.moveableOptions = this.getOptions({
169
+ target: this.dragEl,
170
+ });
171
+ }
172
+
173
+ private setElementGuidelines(nodes: HTMLElement[]) {
174
+ this.elementGuidelines.forEach((node) => {
175
+ node.remove();
176
+ });
177
+ this.elementGuidelines = [];
178
+
179
+ if (this.mode === Mode.ABSOLUTE) {
180
+ const frame = document.createDocumentFragment();
181
+
182
+ for (const node of nodes) {
183
+ const { width, height } = node.getBoundingClientRect();
184
+ if (node === this.target) continue;
185
+ const { left, top } = getOffset(node as HTMLElement);
186
+ const elementGuideline = document.createElement('div');
187
+ elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
188
+ this.elementGuidelines.push(elementGuideline);
189
+ frame.append(elementGuideline);
190
+ }
191
+
192
+ this.container.append(frame);
193
+ }
194
+ }
195
+
137
196
  private initMoveable() {
138
197
  this.moveable?.destroy();
139
198
 
@@ -143,49 +202,47 @@ export default class StageDragResize extends EventEmitter {
143
202
 
144
203
  this.bindResizeEvent();
145
204
  this.bindDragEvent();
205
+ this.bindRotateEvent();
206
+ this.bindScaleEvent();
146
207
  }
147
208
 
148
209
  private bindResizeEvent(): void {
149
210
  if (!this.moveable) throw new Error('moveable 为初始化');
150
211
 
151
212
  const frame = {
152
- translate: [0, 0],
213
+ left: 0,
214
+ top: 0,
153
215
  };
154
216
 
155
217
  this.moveable
156
218
  .on('resizeStart', (e) => {
157
- if (e.dragStart) {
158
- const rect = this.moveable!.getRect();
159
- const offset = getAbsolutePosition(e.target as HTMLElement, rect);
160
- e.dragStart.set([offset.left, offset.top]);
161
- }
219
+ if (!this.target) return;
220
+
221
+ this.dragStatus = ActionStatus.START;
222
+ this.moveableHelper?.onResizeStart(e);
223
+
224
+ frame.top = this.target.offsetTop;
225
+ frame.left = this.target.offsetLeft;
162
226
  })
163
- .on('resize', ({ width, height, drag }) => {
227
+ .on('resize', (e) => {
228
+ const { width, height, drag } = e;
164
229
  if (!this.moveable || !this.target || !this.dragEl) return;
165
230
 
166
231
  const { beforeTranslate } = drag;
167
- frame.translate = beforeTranslate;
168
232
  this.dragStatus = ActionStatus.ING;
169
233
 
170
- this.target.style.width = `${width}px`;
171
- this.target.style.height = `${height}px`;
172
- this.dragEl.style.width = `${width}px`;
173
- this.dragEl.style.height = `${height}px`;
234
+ this.moveableHelper?.onResize(e);
174
235
 
175
236
  // 流式布局
176
237
  if (this.mode === Mode.SORTABLE) {
177
- this.dragEl.style.top = `${beforeTranslate[1]}px`;
178
- this.target.style.top = `0px`;
179
- return;
238
+ this.target.style.top = '0px';
239
+ } else {
240
+ this.target.style.left = `${frame.left + beforeTranslate[0]}px`;
241
+ this.target.style.top = `${frame.top + beforeTranslate[1]}px`;
180
242
  }
181
243
 
182
- this.dragEl.style.left = `${beforeTranslate[0]}px`;
183
- this.dragEl.style.top = `${beforeTranslate[1]}px`;
184
-
185
- const offset = getAbsolutePosition(this.target, { left: beforeTranslate[0], top: beforeTranslate[1] });
186
-
187
- this.target.style.left = `${offset.left}px`;
188
- this.target.style.top = `${offset.top}px`;
244
+ this.target.style.width = `${width}px`;
245
+ this.target.style.height = `${height}px`;
189
246
  })
190
247
  .on('resizeEnd', () => {
191
248
  this.dragStatus = ActionStatus.END;
@@ -196,7 +253,7 @@ export default class StageDragResize extends EventEmitter {
196
253
  private bindDragEvent(): void {
197
254
  if (!this.moveable) throw new Error('moveable 为初始化');
198
255
 
199
- let offset = {
256
+ const frame = {
200
257
  left: 0,
201
258
  top: 0,
202
259
  };
@@ -209,29 +266,28 @@ export default class StageDragResize extends EventEmitter {
209
266
 
210
267
  this.moveableHelper?.onDragStart(e);
211
268
 
212
- offset = getAbsolutePosition(this.target, { left: 0, top: 0 });
213
-
214
269
  if (this.mode === Mode.SORTABLE) {
215
270
  this.ghostEl = this.generateGhostEl(this.target);
216
271
  }
272
+
273
+ frame.top = this.target.offsetTop;
274
+ frame.left = this.target.offsetLeft;
217
275
  })
218
276
  .on('drag', (e) => {
219
277
  if (!this.target || !this.dragEl) return;
220
- this.dragStatus = ActionStatus.ING;
221
278
 
222
- const { left, top } = e;
279
+ this.dragStatus = ActionStatus.ING;
223
280
 
224
281
  // 流式布局
225
282
  if (this.ghostEl) {
226
- this.dragEl.style.top = `${top}px`;
227
- this.ghostEl.style.top = `${top + offset.top}px`;
283
+ this.ghostEl.style.top = `${frame.top + e.beforeTranslate[1]}px`;
228
284
  return;
229
285
  }
230
286
 
231
287
  this.moveableHelper?.onDrag(e);
232
288
 
233
- this.target.style.left = `${left + offset.left}px`;
234
- this.target.style.top = `${top + offset.top}px`;
289
+ this.target.style.left = `${frame.left + e.beforeTranslate[0]}px`;
290
+ this.target.style.top = `${frame.top + e.beforeTranslate[1]}px`;
235
291
  })
236
292
  .on('dragEnd', () => {
237
293
  // 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
@@ -250,19 +306,58 @@ export default class StageDragResize extends EventEmitter {
250
306
  });
251
307
  }
252
308
 
253
- private async getSnapElements(el: HTMLElement): Promise<HTMLElement[]> {
254
- const { renderer } = this.core;
255
- const getSnapElements =
256
- (await renderer.getRuntime())?.getSnapElements ||
257
- (() => {
258
- const doc = renderer.contentWindow?.document;
259
- return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
309
+ private bindRotateEvent(): void {
310
+ if (!this.moveable) throw new Error('moveable 为初始化');
311
+
312
+ this.moveable
313
+ .on('rotateStart', (e) => {
314
+ this.dragStatus = ActionStatus.START;
315
+ this.moveableHelper?.onRotateStart(e);
316
+ })
317
+ .on('rotate', (e) => {
318
+ if (!this.target || !this.dragEl) return;
319
+ this.dragStatus = ActionStatus.ING;
320
+ this.moveableHelper?.onRotate(e);
321
+ const frame = this.moveableHelper?.getFrame(e.target);
322
+ this.target.style.transform = frame?.toCSSObject().transform || '';
323
+ })
324
+ .on('rotateEnd', (e) => {
325
+ this.dragStatus = ActionStatus.END;
326
+ const frame = this.moveableHelper?.getFrame(e.target);
327
+ this.emit('update', {
328
+ el: this.target,
329
+ style: {
330
+ transform: frame?.get('transform'),
331
+ },
332
+ });
333
+ });
334
+ }
335
+
336
+ private bindScaleEvent(): void {
337
+ if (!this.moveable) throw new Error('moveable 为初始化');
338
+
339
+ this.moveable
340
+ .on('scaleStart', (e) => {
341
+ this.dragStatus = ActionStatus.START;
342
+ this.moveableHelper?.onScaleStart(e);
343
+ })
344
+ .on('scale', (e) => {
345
+ if (!this.target || !this.dragEl) return;
346
+ this.dragStatus = ActionStatus.ING;
347
+ this.moveableHelper?.onScale(e);
348
+ const frame = this.moveableHelper?.getFrame(e.target);
349
+ this.target.style.transform = frame?.toCSSObject().transform || '';
350
+ })
351
+ .on('scaleEnd', (e) => {
352
+ this.dragStatus = ActionStatus.END;
353
+ const frame = this.moveableHelper?.getFrame(e.target);
354
+ this.emit('update', {
355
+ el: this.target,
356
+ style: {
357
+ transform: frame?.get('transform'),
358
+ },
359
+ });
260
360
  });
261
- return (
262
- getSnapElements(el)
263
- // 排除掉当前组件本身
264
- .filter((element) => element !== this.target && !this.target?.contains(element))
265
- );
266
361
  }
267
362
 
268
363
  private sort(): void {
@@ -285,14 +380,15 @@ export default class StageDragResize extends EventEmitter {
285
380
  }
286
381
 
287
382
  private update(isResize = false): void {
288
- const rect = this.moveable!.getRect();
383
+ if (!this.target) return;
384
+
289
385
  const offset =
290
- this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : getAbsolutePosition(this.target as HTMLElement, rect);
386
+ this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : { left: this.target.offsetLeft, top: this.target.offsetTop };
291
387
 
292
388
  const left = this.calcValueByFontsize(offset.left);
293
389
  const top = this.calcValueByFontsize(offset.top);
294
- const width = this.calcValueByFontsize(rect.width);
295
- const height = this.calcValueByFontsize(rect.height);
390
+ const width = this.calcValueByFontsize(this.target.clientWidth);
391
+ const height = this.calcValueByFontsize(this.target.clientHeight);
296
392
 
297
393
  this.emit('update', {
298
394
  el: this.target,
@@ -307,8 +403,8 @@ export default class StageDragResize extends EventEmitter {
307
403
 
308
404
  const ghostEl = el.cloneNode(true) as HTMLElement;
309
405
  const { top, left } = getAbsolutePosition(el, getOffset(el));
310
- ghostEl.id = `${GHOST_EL_ID_PREFIX}${ghostEl.id}`;
311
- ghostEl.style.zIndex = '5';
406
+ ghostEl.id = `${GHOST_EL_ID_PREFIX}${el.id}`;
407
+ ghostEl.style.zIndex = ZIndex.GHOST_EL;
312
408
  ghostEl.style.opacity = '.5';
313
409
  ghostEl.style.position = 'absolute';
314
410
  ghostEl.style.left = `${left}px`;
@@ -322,34 +418,36 @@ export default class StageDragResize extends EventEmitter {
322
418
  this.ghostEl = undefined;
323
419
  }
324
420
 
325
- private generateDragEl(el: HTMLElement): HTMLElement {
326
- if (this.dragEl) {
327
- this.destroyDragEl();
328
- }
329
-
330
- const { width, height } = el.getBoundingClientRect();
421
+ private updateDragEl(el: HTMLElement) {
331
422
  const offset = getOffset(el);
332
- const dragEl = globalThis.document.createElement('div');
333
- dragEl.style.cssText = `
423
+ const { transform } = getComputedStyle(el);
424
+
425
+ this.dragEl.style.cssText = `
334
426
  position: absolute;
427
+ transform: ${transform};
335
428
  left: ${offset.left}px;
336
429
  top: ${offset.top}px;
337
- width: ${width}px;
338
- height: ${height}px;
430
+ width: ${el.clientWidth}px;
431
+ height: ${el.clientHeight}px;
432
+ z-index: ${ZIndex.DRAG_EL};
339
433
  `;
340
- this.container.append(dragEl);
341
- return dragEl;
434
+
435
+ this.dragEl.id = `${DRAG_EL_ID_PREFIX}${el.id}`;
436
+
437
+ if (typeof this.core.config.updateDragEl === 'function') {
438
+ this.core.config.updateDragEl(this.dragEl, el);
439
+ }
342
440
  }
343
441
 
344
442
  private destroyDragEl(): void {
345
443
  this.dragEl?.remove();
346
- this.dragEl = undefined;
347
444
  }
348
445
 
349
- private async getOptions(options: MoveableOptions = {}): Promise<MoveableOptions> {
446
+ private getOptions(options: MoveableOptions = {}): MoveableOptions {
350
447
  if (!this.target) return {};
351
448
 
352
449
  const isAbsolute = this.mode === Mode.ABSOLUTE;
450
+ const isFixed = this.mode === Mode.FIXED;
353
451
 
354
452
  let { moveableOptions = {} } = this.core.config;
355
453
 
@@ -357,15 +455,25 @@ export default class StageDragResize extends EventEmitter {
357
455
  moveableOptions = moveableOptions(this.core);
358
456
  }
359
457
 
458
+ const elementGuidelines: any = moveableOptions.elementGuidelines || this.target.parentElement?.children || [];
459
+
460
+ this.setElementGuidelines(elementGuidelines);
461
+
462
+ if (moveableOptions.elementGuidelines) {
463
+ delete moveableOptions.elementGuidelines;
464
+ }
465
+
360
466
  return {
361
- origin: true,
467
+ origin: false,
362
468
  rootContainer: this.core.container,
363
469
  zoom: 1,
364
470
  dragArea: false,
365
471
  draggable: true,
366
472
  resizable: true,
367
- snappable: isAbsolute,
368
- snapGap: isAbsolute,
473
+ scalable: false,
474
+ rotatable: false,
475
+ snappable: isAbsolute || isFixed,
476
+ snapGap: isAbsolute || isFixed,
369
477
  snapThreshold: 5,
370
478
  snapDigit: 0,
371
479
  throttleDrag: 0,
@@ -378,8 +486,16 @@ export default class StageDragResize extends EventEmitter {
378
486
  center: isAbsolute,
379
487
  middle: isAbsolute,
380
488
  },
489
+ elementSnapDirections: {
490
+ top: isAbsolute,
491
+ right: isAbsolute,
492
+ bottom: isAbsolute,
493
+ left: isAbsolute,
494
+ },
495
+ isDisplayInnerSnapDigit: true,
381
496
  horizontalGuidelines: this.horizontalGuidelines,
382
497
  verticalGuidelines: this.verticalGuidelines,
498
+ elementGuidelines: this.elementGuidelines,
383
499
 
384
500
  bounds: {
385
501
  top: 0,
@@ -21,19 +21,28 @@ import { EventEmitter } from 'events';
21
21
 
22
22
  import Moveable from 'moveable';
23
23
 
24
+ import { HIGHLIGHT_EL_ID_PREFIX } from './const';
24
25
  import StageCore from './StageCore';
26
+ import TargetCalibrate from './TargetCalibrate';
25
27
  import type { StageHighlightConfig } from './types';
26
28
  export default class StageHighlight extends EventEmitter {
27
29
  public core: StageCore;
28
30
  public container: HTMLElement;
29
31
  public target?: HTMLElement;
30
32
  public moveable?: Moveable;
33
+ public calibrationTarget: TargetCalibrate;
31
34
 
32
35
  constructor(config: StageHighlightConfig) {
33
36
  super();
34
37
 
35
38
  this.core = config.core;
36
39
  this.container = config.container;
40
+ this.calibrationTarget = new TargetCalibrate({
41
+ parent: this.core.mask.content,
42
+ mask: this.core.mask,
43
+ dr: this.core.dr,
44
+ core: this.core,
45
+ });
37
46
  }
38
47
 
39
48
  /**
@@ -44,8 +53,12 @@ export default class StageHighlight extends EventEmitter {
44
53
  if (!el || el === this.target) return;
45
54
  this.target = el;
46
55
  this.moveable?.destroy();
56
+
47
57
  this.moveable = new Moveable(this.container, {
48
- target: this.target,
58
+ target: this.calibrationTarget.update(el, HIGHLIGHT_EL_ID_PREFIX),
59
+ origin: false,
60
+ rootContainer: this.core.container,
61
+ zoom: 1,
49
62
  });
50
63
  }
51
64
 
@@ -54,6 +67,7 @@ export default class StageHighlight extends EventEmitter {
54
67
  */
55
68
  public clearHighlight(): void {
56
69
  if (!this.moveable) return;
70
+ this.target = undefined;
57
71
  this.moveable.target = null;
58
72
  this.moveable.updateTarget();
59
73
  }
@@ -63,5 +77,6 @@ export default class StageHighlight extends EventEmitter {
63
77
  */
64
78
  public destroy(): void {
65
79
  this.moveable?.destroy();
80
+ this.calibrationTarget.destroy();
66
81
  }
67
82
  }