@tmagic/stage 1.0.0-beta.1 → 1.0.0-beta.12

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