@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.
- package/dist/tmagic-stage.js +12 -3
- package/dist/tmagic-stage.umd.cjs +12 -3
- package/package.json +6 -8
- package/src/ActionManager.ts +661 -0
- package/src/DragResizeHelper.ts +396 -0
- package/src/MoveableActionsAble.ts +91 -0
- package/src/MoveableOptionsManager.ts +274 -0
- package/src/Rule.ts +185 -0
- package/src/StageCore.ts +417 -0
- package/src/StageDragResize.ts +357 -0
- package/src/StageHighlight.ts +89 -0
- package/src/StageMask.ts +337 -0
- package/src/StageMultiDragResize.ts +229 -0
- package/src/StageRender.ts +252 -0
- package/src/TargetShadow.ts +122 -0
- package/src/const.ts +111 -0
- package/src/index.ts +29 -0
- package/src/logger.ts +37 -0
- package/src/moveable-able.css +108 -0
- package/src/style.css +15 -0
- package/src/types.ts +309 -0
- package/src/util.ts +272 -0
- package/types/Rule.d.ts +1 -0
package/src/StageCore.ts
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
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 { EventEmitter } from 'events';
|
|
20
|
+
|
|
21
|
+
import type { MoveableOptions, OnDragStart } from 'moveable';
|
|
22
|
+
|
|
23
|
+
import type { Id } from '@tmagic/schema';
|
|
24
|
+
|
|
25
|
+
import ActionManager from './ActionManager';
|
|
26
|
+
import { DEFAULT_ZOOM } from './const';
|
|
27
|
+
import StageMask from './StageMask';
|
|
28
|
+
import StageRender from './StageRender';
|
|
29
|
+
import type {
|
|
30
|
+
ActionManagerConfig,
|
|
31
|
+
CoreEvents,
|
|
32
|
+
CustomizeRender,
|
|
33
|
+
GuidesEventData,
|
|
34
|
+
Point,
|
|
35
|
+
RemoveData,
|
|
36
|
+
RemoveEventData,
|
|
37
|
+
Runtime,
|
|
38
|
+
SortEventData,
|
|
39
|
+
StageCoreConfig,
|
|
40
|
+
UpdateData,
|
|
41
|
+
UpdateEventData,
|
|
42
|
+
} from './types';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 负责管理画布,管理renderer、mask、actionManager三个核心类,并负责统一对外通信,包括提供接口和抛事件
|
|
46
|
+
*/
|
|
47
|
+
export default class StageCore extends EventEmitter {
|
|
48
|
+
public container?: HTMLDivElement;
|
|
49
|
+
public renderer: StageRender;
|
|
50
|
+
public mask: StageMask;
|
|
51
|
+
public actionManager: ActionManager;
|
|
52
|
+
|
|
53
|
+
private pageResizeObserver: ResizeObserver | null = null;
|
|
54
|
+
private autoScrollIntoView: boolean | undefined;
|
|
55
|
+
private customizedRender?: CustomizeRender;
|
|
56
|
+
|
|
57
|
+
constructor(config: StageCoreConfig) {
|
|
58
|
+
super();
|
|
59
|
+
|
|
60
|
+
this.autoScrollIntoView = config.autoScrollIntoView;
|
|
61
|
+
this.customizedRender = config.render;
|
|
62
|
+
|
|
63
|
+
this.renderer = new StageRender({
|
|
64
|
+
runtimeUrl: config.runtimeUrl,
|
|
65
|
+
zoom: config.zoom,
|
|
66
|
+
renderType: config.renderType,
|
|
67
|
+
customizedRender: async (): Promise<HTMLElement | null> => {
|
|
68
|
+
if (this?.customizedRender) {
|
|
69
|
+
return await this.customizedRender(this);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
this.mask = new StageMask({
|
|
75
|
+
guidesOptions: config.guidesOptions,
|
|
76
|
+
});
|
|
77
|
+
this.actionManager = new ActionManager(this.getActionManagerConfig(config));
|
|
78
|
+
|
|
79
|
+
this.initRenderEvent();
|
|
80
|
+
this.initActionEvent();
|
|
81
|
+
this.initMaskEvent();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 单选选中元素
|
|
86
|
+
* @param id 选中的id
|
|
87
|
+
*/
|
|
88
|
+
public async select(id: Id, event?: MouseEvent): Promise<void> {
|
|
89
|
+
const el = this.renderer.getTargetElement(id);
|
|
90
|
+
if (el === this.actionManager.getSelectedEl()) return;
|
|
91
|
+
|
|
92
|
+
await this.renderer.select([id]);
|
|
93
|
+
|
|
94
|
+
el && this.mask.setLayout(el);
|
|
95
|
+
|
|
96
|
+
this.actionManager.select(el, event);
|
|
97
|
+
|
|
98
|
+
if (el && (this.autoScrollIntoView || el.dataset.autoScrollIntoView)) {
|
|
99
|
+
this.mask.observerIntersection(el);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 多选选中多个元素
|
|
105
|
+
* @param ids 选中元素的id列表
|
|
106
|
+
*/
|
|
107
|
+
public async multiSelect(ids: Id[]): Promise<void> {
|
|
108
|
+
const els = ids.map((id) => this.renderer.getTargetElement(id)).filter((el) => Boolean(el));
|
|
109
|
+
if (els.length === 0) return;
|
|
110
|
+
|
|
111
|
+
const lastEl = els[els.length - 1];
|
|
112
|
+
// 是否减少了组件选择
|
|
113
|
+
const isReduceSelect = els.length < this.actionManager.getSelectedElList().length;
|
|
114
|
+
await this.renderer.select(ids);
|
|
115
|
+
|
|
116
|
+
lastEl && this.mask.setLayout(lastEl);
|
|
117
|
+
|
|
118
|
+
this.actionManager.multiSelect(ids);
|
|
119
|
+
|
|
120
|
+
if (lastEl && (this.autoScrollIntoView || lastEl.dataset.autoScrollIntoView) && !isReduceSelect) {
|
|
121
|
+
this.mask.observerIntersection(lastEl);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 高亮选中元素
|
|
127
|
+
* @param el 要高亮的元素
|
|
128
|
+
*/
|
|
129
|
+
public highlight(id: Id): void {
|
|
130
|
+
this.actionManager.highlight(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public clearHighlight(): void {
|
|
134
|
+
this.actionManager.clearHighlight();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 更新组件
|
|
139
|
+
* @param data 更新组件的数据
|
|
140
|
+
*/
|
|
141
|
+
public async update(data: UpdateData): Promise<void> {
|
|
142
|
+
const { config } = data;
|
|
143
|
+
|
|
144
|
+
await this.renderer.update(data);
|
|
145
|
+
// 通过setTimeout等画布中组件完成渲染更新
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
const el = this.renderer.getTargetElement(`${config.id}`);
|
|
148
|
+
if (el && this.actionManager.isSelectedEl(el)) {
|
|
149
|
+
// 更新了组件的布局,需要重新设置mask是否可以滚动
|
|
150
|
+
this.mask.setLayout(el);
|
|
151
|
+
// 组件有更新,需要set
|
|
152
|
+
this.actionManager.setSelectedEl(el);
|
|
153
|
+
this.actionManager.updateMoveable(el);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 往画布增加一个组件
|
|
160
|
+
* @param data 组件信息数据
|
|
161
|
+
*/
|
|
162
|
+
public async add(data: UpdateData): Promise<void> {
|
|
163
|
+
return await this.renderer.add(data);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 从画布删除一个组件
|
|
168
|
+
* @param data 组件信息数据
|
|
169
|
+
*/
|
|
170
|
+
public async remove(data: RemoveData): Promise<void> {
|
|
171
|
+
return await this.renderer.remove(data);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public setZoom(zoom: number = DEFAULT_ZOOM): void {
|
|
175
|
+
this.renderer.setZoom(zoom);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 挂载Dom节点
|
|
180
|
+
* @param el 将stage挂载到该Dom节点上
|
|
181
|
+
*/
|
|
182
|
+
public async mount(el: HTMLDivElement) {
|
|
183
|
+
this.container = el;
|
|
184
|
+
const { mask, renderer } = this;
|
|
185
|
+
|
|
186
|
+
await renderer.mount(el);
|
|
187
|
+
mask.mount(el);
|
|
188
|
+
|
|
189
|
+
this.emit('mounted');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 清空所有参考线
|
|
194
|
+
*/
|
|
195
|
+
public clearGuides() {
|
|
196
|
+
this.mask.clearGuides();
|
|
197
|
+
this.actionManager.clearGuides();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @deprecated 废弃接口,建议用delayedMarkContainer代替
|
|
202
|
+
*/
|
|
203
|
+
public getAddContainerHighlightClassNameTimeout(
|
|
204
|
+
event: MouseEvent,
|
|
205
|
+
excludeElList: Element[] = [],
|
|
206
|
+
): NodeJS.Timeout | undefined {
|
|
207
|
+
return this.delayedMarkContainer(event, excludeElList);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 鼠标拖拽着元素,在容器上方悬停,延迟一段时间后,对容器进行标记,如果悬停时间够长将标记成功,悬停时间短,调用方通过返回的timeoutId取消标记
|
|
212
|
+
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
|
213
|
+
* @param event 鼠标事件
|
|
214
|
+
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
|
215
|
+
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
|
216
|
+
*/
|
|
217
|
+
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
|
|
218
|
+
return this.actionManager.delayedMarkContainer(event, excludeElList);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public getMoveableOption<K extends keyof MoveableOptions>(key: K): MoveableOptions[K] | undefined {
|
|
222
|
+
return this.actionManager.getMoveableOption(key);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public getDragStatus() {
|
|
226
|
+
return this.actionManager.getDragStatus();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public disableMultiSelect() {
|
|
230
|
+
this.actionManager.disableMultiSelect();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public enableMultiSelect() {
|
|
234
|
+
this.actionManager.enableMultiSelect();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 销毁实例
|
|
239
|
+
*/
|
|
240
|
+
public destroy(): void {
|
|
241
|
+
const { mask, renderer, actionManager, pageResizeObserver } = this;
|
|
242
|
+
|
|
243
|
+
renderer.destroy();
|
|
244
|
+
mask.destroy();
|
|
245
|
+
actionManager.destroy();
|
|
246
|
+
pageResizeObserver?.disconnect();
|
|
247
|
+
|
|
248
|
+
this.removeAllListeners();
|
|
249
|
+
|
|
250
|
+
this.container = undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
public on<Name extends keyof CoreEvents, Param extends CoreEvents[Name]>(
|
|
254
|
+
eventName: Name,
|
|
255
|
+
listener: (...args: Param) => void | Promise<void>,
|
|
256
|
+
) {
|
|
257
|
+
return super.on(eventName, listener as any);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public emit<Name extends keyof CoreEvents, Param extends CoreEvents[Name]>(eventName: Name, ...args: Param) {
|
|
261
|
+
return super.emit(eventName, ...args);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 监听页面大小变化
|
|
266
|
+
*/
|
|
267
|
+
private observePageResize(page: HTMLElement): void {
|
|
268
|
+
if (this.pageResizeObserver) {
|
|
269
|
+
this.pageResizeObserver.disconnect();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
273
|
+
this.pageResizeObserver = new ResizeObserver((entries) => {
|
|
274
|
+
this.mask.pageResize(entries);
|
|
275
|
+
this.actionManager.updateMoveable();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this.pageResizeObserver.observe(page);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private getActionManagerConfig(config: StageCoreConfig): ActionManagerConfig {
|
|
283
|
+
const actionManagerConfig: ActionManagerConfig = {
|
|
284
|
+
containerHighlightClassName: config.containerHighlightClassName,
|
|
285
|
+
containerHighlightDuration: config.containerHighlightDuration,
|
|
286
|
+
containerHighlightType: config.containerHighlightType,
|
|
287
|
+
moveableOptions: config.moveableOptions,
|
|
288
|
+
container: this.mask.content,
|
|
289
|
+
disabledDragStart: config.disabledDragStart,
|
|
290
|
+
disabledMultiSelect: config.disabledMultiSelect,
|
|
291
|
+
canSelect: config.canSelect,
|
|
292
|
+
isContainer: config.isContainer,
|
|
293
|
+
updateDragEl: config.updateDragEl,
|
|
294
|
+
getRootContainer: () => this.container,
|
|
295
|
+
getRenderDocument: () => this.renderer.getDocument(),
|
|
296
|
+
getTargetElement: (id: Id) => this.renderer.getTargetElement(id),
|
|
297
|
+
getElementsFromPoint: (point: Point) => this.renderer.getElementsFromPoint(point),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return actionManagerConfig;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private initRenderEvent(): void {
|
|
304
|
+
this.renderer.on('runtime-ready', (runtime: Runtime) => {
|
|
305
|
+
this.emit('runtime-ready', runtime);
|
|
306
|
+
});
|
|
307
|
+
this.renderer.on('page-el-update', (el: HTMLElement) => {
|
|
308
|
+
this.mask?.observe(el);
|
|
309
|
+
this.observePageResize(el);
|
|
310
|
+
|
|
311
|
+
this.emit('page-el-update', el);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private initMaskEvent(): void {
|
|
316
|
+
this.mask.on('change-guides', (data: GuidesEventData) => {
|
|
317
|
+
this.actionManager.setGuidelines(data.type, data.guides);
|
|
318
|
+
this.emit('change-guides', data);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 初始化操作相关事件监听
|
|
324
|
+
*/
|
|
325
|
+
private initActionEvent(): void {
|
|
326
|
+
this.initActionManagerEvent();
|
|
327
|
+
this.initDrEvent();
|
|
328
|
+
this.initMulDrEvent();
|
|
329
|
+
this.initHighlightEvent();
|
|
330
|
+
this.initMouseEvent();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 初始化ActionManager类本身抛出来的事件监听
|
|
335
|
+
*/
|
|
336
|
+
private initActionManagerEvent(): void {
|
|
337
|
+
this.actionManager
|
|
338
|
+
.on('before-select', (el: HTMLElement, event?: MouseEvent) => {
|
|
339
|
+
this.select(el.id, event);
|
|
340
|
+
})
|
|
341
|
+
.on('select', (selectedEl: HTMLElement, event: MouseEvent) => {
|
|
342
|
+
this.emit('select', selectedEl, event);
|
|
343
|
+
})
|
|
344
|
+
.on('before-multi-select', (els: HTMLElement[]) => {
|
|
345
|
+
this.multiSelect(els.map((el) => el.id));
|
|
346
|
+
})
|
|
347
|
+
.on('multi-select', (selectedElList: HTMLElement[], event: MouseEvent) => {
|
|
348
|
+
this.emit('multi-select', selectedElList, event);
|
|
349
|
+
})
|
|
350
|
+
.on('dblclick', (event: MouseEvent) => {
|
|
351
|
+
this.emit('dblclick', event);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 初始化DragResize类通过ActionManager抛出来的事件监听
|
|
357
|
+
*/
|
|
358
|
+
private initDrEvent(): void {
|
|
359
|
+
this.actionManager
|
|
360
|
+
.on('update', (data: UpdateEventData) => {
|
|
361
|
+
this.emit('update', data);
|
|
362
|
+
})
|
|
363
|
+
.on('sort', (data: SortEventData) => {
|
|
364
|
+
this.emit('sort', data);
|
|
365
|
+
})
|
|
366
|
+
.on('select-parent', () => {
|
|
367
|
+
this.emit('select-parent');
|
|
368
|
+
})
|
|
369
|
+
.on('remove', (data: RemoveEventData) => {
|
|
370
|
+
this.emit('remove', data);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 初始化MultiDragResize类通过ActionManager抛出来的事件监听
|
|
376
|
+
*/
|
|
377
|
+
private initMulDrEvent(): void {
|
|
378
|
+
this.actionManager
|
|
379
|
+
// 多选切换到单选
|
|
380
|
+
.on('change-to-select', (id: Id, e: MouseEvent) => {
|
|
381
|
+
this.select(id);
|
|
382
|
+
// 先保证画布内完成渲染,再通知外部更新
|
|
383
|
+
setTimeout(() => {
|
|
384
|
+
const el = this.renderer.getTargetElement(id);
|
|
385
|
+
el && this.emit('select', el, e);
|
|
386
|
+
});
|
|
387
|
+
})
|
|
388
|
+
.on('multi-update', (data: UpdateEventData) => {
|
|
389
|
+
this.emit('update', data);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 初始化Highlight类通过ActionManager抛出来的事件监听
|
|
395
|
+
*/
|
|
396
|
+
private initHighlightEvent(): void {
|
|
397
|
+
this.actionManager.on('highlight', (highlightEl: HTMLElement) => {
|
|
398
|
+
this.emit('highlight', highlightEl);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 初始化Highlight类通过ActionManager抛出来的事件监听
|
|
404
|
+
*/
|
|
405
|
+
private initMouseEvent(): void {
|
|
406
|
+
this.actionManager
|
|
407
|
+
.on('mousemove', (event: MouseEvent) => {
|
|
408
|
+
this.emit('mousemove', event);
|
|
409
|
+
})
|
|
410
|
+
.on('mouseleave', (event: MouseEvent) => {
|
|
411
|
+
this.emit('mouseleave', event);
|
|
412
|
+
})
|
|
413
|
+
.on('drag-start', (e: OnDragStart) => {
|
|
414
|
+
this.emit('drag-start', e);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|