@tmagic/stage 1.0.0-beta.7 → 1.0.0-beta.8

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.
@@ -22,10 +22,10 @@ import { EventEmitter } from 'events';
22
22
  import type { MoveableOptions } from 'moveable';
23
23
  import Moveable from 'moveable';
24
24
 
25
- import { GHOST_EL_ID_PREFIX } from './const';
25
+ import { GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
26
26
  import StageCore from './StageCore';
27
27
  import type { SortEventData, StageDragResizeConfig } from './types';
28
- import { getAbsolutePosition, getMode, getOffset, Mode } from './util';
28
+ import { getAbsolutePosition, getMode, getOffset } from './util';
29
29
 
30
30
  enum ActionStatus {
31
31
  START = 'start',
@@ -40,12 +40,13 @@ export default class StageDragResize extends EventEmitter {
40
40
  public core: StageCore;
41
41
  public container: HTMLElement;
42
42
  public target?: HTMLElement;
43
+ public dragEl?: HTMLElement;
43
44
  public moveable?: Moveable;
44
45
  public horizontalGuidelines: number[] = [];
45
46
  public verticalGuidelines: number[] = [];
46
47
 
48
+ private moveableOptions: MoveableOptions = {};
47
49
  private dragStatus: ActionStatus = ActionStatus.END;
48
- private elObserver?: ResizeObserver;
49
50
  private ghostEl: HTMLElement | undefined;
50
51
  private mode: Mode = Mode.ABSOLUTE;
51
52
 
@@ -54,81 +55,88 @@ export default class StageDragResize extends EventEmitter {
54
55
 
55
56
  this.core = config.core;
56
57
  this.container = config.container;
57
- this.initObserver();
58
58
  }
59
59
 
60
60
  /**
61
61
  * 将选中框渲染并覆盖到选中的组件Dom节点上方
62
+ * 当选中的节点是不是absolute时,会创建一个新的节点出来作为拖拽目标
62
63
  * @param el 选中组件的Dom节点元素
64
+ * @param event 鼠标事件
63
65
  */
64
- public async select(el: HTMLElement): Promise<void> {
65
- if (this.target === el) {
66
- this.refresh();
67
- return;
68
- }
69
-
70
- this.moveable?.destroy();
71
-
66
+ public async select(el: HTMLElement, event?: MouseEvent): Promise<void> {
72
67
  this.target = el;
68
+ // 如果有滚动条会导致resize时获取到width,height不准确
69
+ if (/(auto|scroll)/.test(this.target.style.overflow)) {
70
+ this.target.style.overflow = 'hidden';
71
+ }
73
72
  this.mode = getMode(el);
73
+ this.destroyDragEl();
74
+ this.destroyGhostEl();
74
75
 
75
- const options = await this.getOptions();
76
+ this.dragEl = this.generateDragEl(el);
76
77
 
77
- this.moveable = new Moveable(this.container, options);
78
- this.bindResizeEvent();
79
- this.bindDragEvent();
78
+ this.moveableOptions = await this.getOptions({
79
+ target: this.dragEl || this.target,
80
+ });
80
81
 
81
- this.syncRect(el);
82
+ this.initMoveable();
83
+
84
+ if (event) {
85
+ this.moveable?.dragStart(event);
86
+ }
82
87
  }
83
88
 
84
89
  /**
85
90
  * 初始化选中框并渲染出来
86
- * @param param0
87
91
  */
88
-
89
92
  public async refresh() {
93
+ if (!this.moveable) throw new Error('未初始化moveable');
94
+
90
95
  const options = await this.getOptions();
91
96
  Object.entries(options).forEach(([key, value]) => {
92
97
  (this.moveable as any)[key] = value;
93
98
  });
94
- this.updateMoveableTarget();
95
- }
96
-
97
- public async setVGuidelines(verticalGuidelines: number[]): Promise<void> {
98
- this.verticalGuidelines = verticalGuidelines;
99
- this.target && (await this.select(this.target));
100
- }
101
-
102
- public async setHGuidelines(horizontalGuidelines: number[]): Promise<void> {
103
- this.horizontalGuidelines = horizontalGuidelines;
104
- this.target && (await this.select(this.target));
99
+ this.moveable.updateTarget();
105
100
  }
106
101
 
107
- public updateMoveableTarget(target?: HTMLElement): void {
108
- if (!this.moveable) throw new Error('为初始化目标');
109
-
110
- if (target) {
111
- this.moveable.target = target;
102
+ public setGuidelines(type: GuidesType, guidelines: number[]): void {
103
+ if (type === GuidesType.HORIZONTAL) {
104
+ this.horizontalGuidelines = guidelines;
105
+ } else if (type === GuidesType.VERTICAL) {
106
+ this.verticalGuidelines = guidelines;
112
107
  }
113
108
 
114
- if (this.target) {
115
- this.mode = getMode(this.target);
116
- }
109
+ this.refresh();
110
+ }
117
111
 
118
- this.moveable.updateTarget();
112
+ public clearGuides() {
113
+ this.verticalGuidelines = [];
114
+ this.horizontalGuidelines = [];
115
+ this.refresh();
119
116
  }
120
117
 
121
118
  /**
122
119
  * 销毁实例
123
120
  */
124
121
  public destroy(): void {
125
- this.destroyGhostEl();
126
122
  this.moveable?.destroy();
123
+ this.destroyGhostEl();
124
+ this.destroyDragEl();
127
125
  this.dragStatus = ActionStatus.END;
128
- this.elObserver?.disconnect();
129
126
  this.removeAllListeners();
130
127
  }
131
128
 
129
+ private initMoveable() {
130
+ this.moveable?.destroy();
131
+
132
+ this.moveable = new Moveable(this.container, {
133
+ ...this.moveableOptions,
134
+ });
135
+
136
+ this.bindResizeEvent();
137
+ this.bindDragEvent();
138
+ }
139
+
132
140
  private bindResizeEvent(): void {
133
141
  if (!this.moveable) throw new Error('moveable 为初始化');
134
142
 
@@ -142,50 +150,38 @@ export default class StageDragResize extends EventEmitter {
142
150
  const rect = this.moveable!.getRect();
143
151
  const offset = getAbsolutePosition(e.target as HTMLElement, rect);
144
152
  e.dragStart.set([offset.left, offset.top]);
145
-
146
- if (this.ghostEl) {
147
- this.destroyGhostEl();
148
- this.updateMoveableTarget(this.target);
149
- }
150
153
  }
151
154
  })
152
- .on('resize', ({ target, width, height, drag }) => {
153
- if (!this.moveable) return;
154
- if (!this.target) return;
155
+ .on('resize', ({ width, height, drag }) => {
156
+ if (!this.moveable || !this.target || !this.dragEl) return;
157
+
155
158
  const { beforeTranslate } = drag;
156
159
  frame.translate = beforeTranslate;
157
160
  this.dragStatus = ActionStatus.ING;
158
161
 
159
- target.style.width = `${width}px`;
160
- target.style.height = `${height}px`;
162
+ this.target.style.width = `${width}px`;
163
+ this.target.style.height = `${height}px`;
164
+ this.dragEl.style.width = `${width}px`;
165
+ this.dragEl.style.height = `${height}px`;
161
166
 
162
- if ([Mode.ABSOLUTE, Mode.FIXED].includes(this.mode)) {
163
- target.style.left = `${beforeTranslate[0]}px`;
164
- target.style.top = `${beforeTranslate[1]}px`;
167
+ // 流式布局
168
+ if (this.mode === Mode.SORTABLE) {
169
+ this.dragEl.style.top = `${beforeTranslate[1]}px`;
170
+ this.target.style.top = `0px`;
171
+ return;
165
172
  }
173
+
174
+ this.dragEl.style.left = `${beforeTranslate[0]}px`;
175
+ this.dragEl.style.top = `${beforeTranslate[1]}px`;
176
+
177
+ const offset = getAbsolutePosition(this.target, { left: beforeTranslate[0], top: beforeTranslate[1] });
178
+
179
+ this.target.style.left = `${offset.left}px`;
180
+ this.target.style.top = `${offset.top}px`;
166
181
  })
167
- .on('resizeEnd', ({ target }) => {
182
+ .on('resizeEnd', () => {
168
183
  this.dragStatus = ActionStatus.END;
169
-
170
- const rect = this.moveable!.getRect();
171
- const offset = getAbsolutePosition(target as HTMLElement, rect);
172
-
173
- this.updateMoveableTarget(this.target);
174
-
175
- this.emit('update', {
176
- el: this.target,
177
- style: {
178
- width: this.calcValueByFontsize(rect.width),
179
- height: this.calcValueByFontsize(rect.height),
180
- position: this.target?.style.position,
181
- ...(this.mode === Mode.SORTABLE
182
- ? {}
183
- : {
184
- left: this.calcValueByFontsize(offset.left),
185
- top: this.calcValueByFontsize(offset.top),
186
- }),
187
- },
188
- });
184
+ this.update(true);
189
185
  });
190
186
  }
191
187
 
@@ -200,42 +196,40 @@ export default class StageDragResize extends EventEmitter {
200
196
 
201
197
  if (this.mode === Mode.SORTABLE) {
202
198
  this.ghostEl = this.generateGhostEl(this.target);
203
- this.updateMoveableTarget(this.ghostEl);
204
199
  }
205
200
  })
206
- .on('drag', ({ target, left, top }) => {
201
+ .on('drag', ({ left, top }) => {
202
+ if (!this.target || !this.dragEl) return;
207
203
  this.dragStatus = ActionStatus.ING;
208
- if (this.mode === Mode.SORTABLE && (!this.ghostEl || target !== this.ghostEl)) {
204
+
205
+ const offset = getAbsolutePosition(this.target, { left, top });
206
+
207
+ // 流式布局
208
+ if (this.ghostEl) {
209
+ this.dragEl.style.top = `${top}px`;
210
+ this.ghostEl.style.top = `${offset.top}px`;
209
211
  return;
210
212
  }
211
213
 
212
- if ([Mode.ABSOLUTE, Mode.FIXED].includes(this.mode)) {
213
- target.style.left = `${left}px`;
214
- target.style.top = `${top}px`;
215
- } else if (this.target) {
216
- const offset = getAbsolutePosition(this.target, getOffset(this.target));
217
- target.style.top = `${offset.top + top}px`;
218
- }
214
+ this.dragEl.style.left = `${left}px`;
215
+ this.dragEl.style.top = `${top}px`;
216
+
217
+ this.target.style.left = `${offset.left}px`;
218
+ this.target.style.top = `${offset.top}px`;
219
219
  })
220
220
  .on('dragEnd', () => {
221
221
  // 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
222
- if (this.dragStatus !== ActionStatus.ING) {
223
- return;
222
+ if (this.dragStatus === ActionStatus.ING) {
223
+ switch (this.mode) {
224
+ case Mode.SORTABLE:
225
+ this.sort();
226
+ break;
227
+ default:
228
+ this.update();
229
+ }
224
230
  }
225
231
 
226
- if (!this.target) return;
227
-
228
232
  this.dragStatus = ActionStatus.END;
229
- this.updateMoveableTarget(this.target);
230
-
231
- switch (this.mode) {
232
- case Mode.SORTABLE:
233
- this.sort();
234
- break;
235
- default:
236
- this.drag();
237
- }
238
-
239
233
  this.destroyGhostEl();
240
234
  });
241
235
  }
@@ -246,12 +240,13 @@ export default class StageDragResize extends EventEmitter {
246
240
  (await renderer.getRuntime())?.getSnapElements ||
247
241
  (() => {
248
242
  const doc = renderer.contentWindow?.document;
249
- const elementGuidelines = (doc ? Array.from(doc.querySelectorAll('[id]')) : [])
250
- // 排除掉当前组件本身
251
- .filter((element) => element !== this.target && !this.target?.contains(element));
252
- return elementGuidelines as HTMLElement[];
243
+ return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
253
244
  });
254
- return getSnapElements(el);
245
+ return (
246
+ getSnapElements(el)
247
+ // 排除掉当前组件本身
248
+ .filter((element) => element !== this.target && !this.target?.contains(element))
249
+ );
255
250
  }
256
251
 
257
252
  private sort(): void {
@@ -273,18 +268,19 @@ export default class StageDragResize extends EventEmitter {
273
268
  }
274
269
  }
275
270
 
276
- private drag(): void {
271
+ private update(isResize = false): void {
277
272
  const rect = this.moveable!.getRect();
278
- const offset = getAbsolutePosition(this.target as HTMLElement, rect);
273
+ const offset =
274
+ this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : getAbsolutePosition(this.target as HTMLElement, rect);
275
+
276
+ const left = this.calcValueByFontsize(offset.left);
277
+ const top = this.calcValueByFontsize(offset.top);
278
+ const width = this.calcValueByFontsize(rect.width);
279
+ const height = this.calcValueByFontsize(rect.height);
279
280
 
280
281
  this.emit('update', {
281
282
  el: this.target,
282
- style: {
283
- left: this.calcValueByFontsize(this.mode === Mode.FIXED ? rect.left : offset.left),
284
- top: this.calcValueByFontsize(this.mode === Mode.FIXED ? rect.top : offset.top),
285
- width: this.calcValueByFontsize(rect.width),
286
- height: this.calcValueByFontsize(rect.height),
287
- },
283
+ style: isResize ? { left, top, width, height } : { left, top },
288
284
  });
289
285
  }
290
286
 
@@ -310,87 +306,76 @@ export default class StageDragResize extends EventEmitter {
310
306
  this.ghostEl = undefined;
311
307
  }
312
308
 
309
+ private generateDragEl(el: HTMLElement): HTMLElement {
310
+ if (this.dragEl) {
311
+ this.destroyDragEl();
312
+ }
313
+
314
+ const { width, height } = el.getBoundingClientRect();
315
+ const offset = getOffset(el);
316
+ const dragEl = globalThis.document.createElement('div');
317
+ dragEl.style.cssText = `
318
+ position: absolute;
319
+ left: ${offset.left}px;
320
+ top: ${offset.top}px;
321
+ width: ${width}px;
322
+ height: ${height}px;
323
+ `;
324
+ this.container.append(dragEl);
325
+ return dragEl;
326
+ }
327
+
328
+ private destroyDragEl(): void {
329
+ this.dragEl?.remove();
330
+ this.dragEl = undefined;
331
+ }
332
+
313
333
  private async getOptions(options: MoveableOptions = {}): Promise<MoveableOptions> {
314
334
  if (!this.target) return {};
315
335
 
316
- const isSortable = this.mode === Mode.SORTABLE;
317
- const { config, renderer, mask } = this.core;
318
- const { iframe } = renderer;
336
+ const isAbsolute = this.mode === Mode.ABSOLUTE;
319
337
 
320
- let { moveableOptions = {} } = config;
338
+ let { moveableOptions = {} } = this.core.config;
321
339
 
322
340
  if (typeof moveableOptions === 'function') {
323
341
  moveableOptions = moveableOptions(this.core);
324
342
  }
325
343
 
326
- const boundsOptions = {
327
- top: 0,
328
- left: 0,
329
- right: iframe?.clientWidth,
330
- bottom: this.mode === Mode.FIXED ? iframe?.clientHeight : mask.page?.clientHeight,
331
- ...(moveableOptions.bounds || {}),
332
- };
333
-
334
344
  return {
335
- target: this.target,
336
345
  scrollable: true,
337
346
  origin: true,
338
347
  zoom: 1,
339
348
  dragArea: true,
340
349
  draggable: true,
341
350
  resizable: true,
342
- snappable: !isSortable,
343
- snapGap: !isSortable,
344
- snapCenter: !isSortable,
345
- container: renderer.contentWindow?.document.body,
351
+ snappable: isAbsolute,
352
+ snapGap: isAbsolute,
353
+ snapDirections: { center: isAbsolute, middle: isAbsolute },
354
+ elementSnapDirections: { center: isAbsolute, middle: isAbsolute },
346
355
 
347
- elementGuidelines: isSortable ? [] : await this.getSnapElements(this.target),
356
+ elementGuidelines: isAbsolute ? await this.getSnapElements(this.target) : [],
348
357
  horizontalGuidelines: this.horizontalGuidelines,
349
358
  verticalGuidelines: this.verticalGuidelines,
350
359
 
351
- bounds: boundsOptions,
360
+ bounds: {
361
+ top: 0,
362
+ left: 0,
363
+ right: this.container.clientWidth,
364
+ bottom: this.container.clientHeight,
365
+ ...(moveableOptions.bounds || {}),
366
+ },
352
367
  ...options,
353
368
  ...moveableOptions,
354
369
  };
355
370
  }
356
371
 
357
- private initObserver(): void {
358
- if (typeof ResizeObserver === 'undefined') {
359
- return;
360
- }
361
-
362
- this.elObserver = new ResizeObserver(() => {
363
- const doc = this.core.renderer.contentWindow?.document;
364
- if (!doc || !this.target || !this.moveable) return;
365
-
366
- /** 组件可能已经重新渲染了,所以需要重新获取新的dom */
367
- const target = doc.getElementById(this.target.id);
368
-
369
- if (this.ghostEl) {
370
- this.destroyGhostEl();
371
- }
372
-
373
- if (target && target !== this.target) {
374
- this.syncRect(target);
375
- this.target = target;
376
- }
377
-
378
- this.updateMoveableTarget(this.target);
379
- });
380
- }
381
-
382
- private syncRect(el: HTMLElement): void {
383
- this.elObserver?.disconnect();
384
- this.elObserver?.observe(el);
385
- }
386
-
387
372
  private calcValueByFontsize(value: number) {
388
373
  const { contentWindow } = this.core.renderer;
389
374
  const fontSize = contentWindow?.document.documentElement.style.fontSize;
390
375
 
391
376
  if (fontSize) {
392
377
  const times = globalThis.parseFloat(fontSize) / 100;
393
- return value / times;
378
+ return (value / times).toFixed(2);
394
379
  }
395
380
 
396
381
  return value;