@tmagic/stage 1.0.0-beta.6 → 1.0.0-beta.9

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