@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.
package/src/StageCore.ts CHANGED
@@ -18,25 +18,37 @@
18
18
 
19
19
  import { EventEmitter } from 'events';
20
20
 
21
- import { GuidesEvents } from '@scena/guides';
22
-
23
21
  import { Id } from '@tmagic/schema';
24
22
 
25
- import { DEFAULT_ZOOM } from './const';
23
+ import { DEFAULT_ZOOM, GHOST_EL_ID_PREFIX } from './const';
26
24
  import StageDragResize from './StageDragResize';
25
+ import StageHighlight from './StageHighlight';
27
26
  import StageMask from './StageMask';
28
27
  import StageRender from './StageRender';
29
- import { RemoveData, Runtime, SortEventData, StageCoreConfig, UpdateData, UpdateEventData } from './types';
30
- import { isFixed } from './util';
28
+ import {
29
+ CanSelect,
30
+ GuidesEventData,
31
+ RemoveData,
32
+ Runtime,
33
+ SortEventData,
34
+ StageCoreConfig,
35
+ UpdateData,
36
+ UpdateEventData,
37
+ } from './types';
38
+ import { addSelectedClassName, removeSelectedClassName } from './util';
31
39
 
32
40
  export default class StageCore extends EventEmitter {
33
41
  public selectedDom: Element | undefined;
42
+ public highlightedDom: Element | undefined;
34
43
 
35
44
  public renderer: StageRender;
36
45
  public mask: StageMask;
37
46
  public dr: StageDragResize;
47
+ public highlightLayer: StageHighlight;
38
48
  public config: StageCoreConfig;
39
49
  public zoom = DEFAULT_ZOOM;
50
+ public container?: HTMLDivElement;
51
+ private canSelect: CanSelect;
40
52
 
41
53
  constructor(config: StageCoreConfig) {
42
54
  super();
@@ -44,77 +56,165 @@ export default class StageCore extends EventEmitter {
44
56
  this.config = config;
45
57
 
46
58
  this.setZoom(config.zoom);
59
+ this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id);
47
60
 
48
61
  this.renderer = new StageRender({ core: this });
49
62
  this.mask = new StageMask({ core: this });
50
63
  this.dr = new StageDragResize({ core: this, container: this.mask.content });
64
+ this.highlightLayer = new StageHighlight({ core: this, container: this.mask.wrapper });
51
65
 
52
- this.renderer.on('runtime-ready', (runtime: Runtime) => this.emit('runtime-ready', runtime));
53
- this.renderer.on('page-el-update', (el: HTMLElement) => this.mask?.observe(el));
54
-
55
- this.mask.on('select', async (el: Element, event: MouseEvent) => {
56
- await this.dr?.select(el as HTMLElement);
57
- setTimeout(() => this.dr?.moveable?.dragStart(event));
66
+ this.renderer.on('runtime-ready', (runtime: Runtime) => {
67
+ this.emit('runtime-ready', runtime);
58
68
  });
59
- this.mask.on('selected', (el: Element) => {
60
- this.select(el as HTMLElement);
61
- this.emit('select', el);
69
+ this.renderer.on('page-el-update', (el: HTMLElement) => {
70
+ this.mask?.observe(el);
62
71
  });
63
72
 
64
- this.dr.on('update', (data: UpdateEventData) => this.emit('update', data));
65
- this.dr.on('sort', (data: UpdateEventData) => this.emit('sort', data));
73
+ this.mask
74
+ .on('beforeSelect', (event: MouseEvent) => {
75
+ this.setElementFromPoint(event);
76
+ })
77
+ .on('select', () => {
78
+ this.emit('select', this.selectedDom);
79
+ })
80
+ .on('changeGuides', (data: GuidesEventData) => {
81
+ this.dr.setGuidelines(data.type, data.guides);
82
+ this.emit('changeGuides', data);
83
+ })
84
+ .on('highlight', async (event: MouseEvent) => {
85
+ await this.setElementFromPoint(event);
86
+ if (this.highlightedDom === this.selectedDom) {
87
+ this.highlightLayer.clearHighlight();
88
+ return;
89
+ }
90
+ this.highlightLayer.highlight(this.highlightedDom as HTMLElement);
91
+ this.emit('highlight', this.highlightedDom);
92
+ })
93
+ .on('clearHighlight', async () => {
94
+ this.highlightLayer.clearHighlight();
95
+ });
96
+
97
+ // 要先触发select,在触发update
98
+ this.dr
99
+ .on('update', (data: UpdateEventData) => {
100
+ setTimeout(() => this.emit('update', data));
101
+ })
102
+ .on('sort', (data: UpdateEventData) => {
103
+ setTimeout(() => this.emit('sort', data));
104
+ });
105
+ }
106
+
107
+ public async setElementFromPoint(event: MouseEvent) {
108
+ const { renderer, zoom } = this;
109
+
110
+ const doc = renderer.contentWindow?.document;
111
+ let x = event.clientX;
112
+ let y = event.clientY;
113
+
114
+ if (renderer.iframe) {
115
+ const rect = renderer.iframe.getClientRects()[0];
116
+ if (rect) {
117
+ x = x - rect.left;
118
+ y = y - rect.top;
119
+ }
120
+ }
121
+
122
+ const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
123
+
124
+ let stopped = false;
125
+ const stop = () => (stopped = true);
126
+ for (const el of els) {
127
+ if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, event, stop))) {
128
+ if (stopped) break;
129
+ if (event.type === 'mousemove') {
130
+ this.highlight(el);
131
+ break;
132
+ }
133
+ this.select(el, event);
134
+ break;
135
+ }
136
+ }
66
137
  }
67
138
 
68
139
  /**
69
140
  * 选中组件
70
141
  * @param idOrEl 组件Dom节点的id属性,或者Dom节点
71
142
  */
72
- public async select(idOrEl: Id | HTMLElement): Promise<void> {
73
- let el;
74
- if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
75
- const runtime = await this.renderer?.getRuntime();
76
- el = await runtime?.select?.(`${idOrEl}`);
77
- if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
78
- } else {
79
- el = idOrEl;
80
- }
143
+ public async select(idOrEl: Id | HTMLElement, event?: MouseEvent): Promise<void> {
144
+ const el = await this.getTargetElement(idOrEl);
81
145
 
82
- if (this.selectedDom === el) return;
146
+ if (el === this.selectedDom) return;
83
147
 
84
- await this.beforeSelect(el);
148
+ const runtime = await this.renderer.getRuntime();
149
+
150
+ await runtime?.select?.(el.id);
151
+
152
+ if (runtime?.beforeSelect) {
153
+ await runtime.beforeSelect(el);
154
+ }
85
155
 
156
+ this.mask.setLayout(el);
157
+ this.dr.select(el, event);
86
158
  this.selectedDom = el;
87
- this.setMaskLayout(el);
88
- this.dr?.select(el);
159
+
160
+ if (this.renderer.contentWindow) {
161
+ removeSelectedClassName(this.renderer.contentWindow.document);
162
+ if (this.selectedDom) {
163
+ addSelectedClassName(this.selectedDom);
164
+ }
165
+ }
89
166
  }
90
167
 
91
- public update(data: UpdateData): void {
168
+ /**
169
+ * 更新选中的节点
170
+ * @param data 更新的数据
171
+ */
172
+ public update(data: UpdateData): Promise<void> {
92
173
  const { config } = data;
93
174
 
94
- this.renderer?.getRuntime().then((runtime) => {
175
+ return this.renderer?.getRuntime().then((runtime) => {
95
176
  runtime?.update?.(data);
96
177
  // 更新配置后,需要等组件渲染更新
97
178
  setTimeout(() => {
98
179
  const el = this.renderer.contentWindow?.document.getElementById(`${config.id}`);
99
- if (el) {
180
+ // 有可能dom已经重新渲染,不再是原来的dom了,所以这里判断id,而不是判断el === this.selectedDom
181
+ if (el && el.id === this.selectedDom?.id) {
182
+ this.selectedDom = el;
100
183
  // 更新了组件的布局,需要重新设置mask是否可以滚动
101
- this.setMaskLayout(el);
184
+ this.mask.setLayout(el);
185
+ this.dr.updateMoveable(el);
102
186
  }
103
- this.dr?.refresh();
104
187
  }, 0);
105
188
  });
106
189
  }
107
190
 
108
- public sortNode(data: SortEventData): void {
109
- this.renderer?.getRuntime().then((runtime) => runtime?.sortNode?.(data));
191
+ /**
192
+ * 高亮选中组件
193
+ * @param el 页面Dom节点
194
+ */
195
+ public async highlight(idOrEl: HTMLElement | Id): Promise<void> {
196
+ let el;
197
+ try {
198
+ el = await this.getTargetElement(idOrEl);
199
+ } catch (error) {
200
+ this.highlightLayer.clearHighlight();
201
+ return;
202
+ }
203
+ if (el === this.highlightedDom) return;
204
+ this.highlightLayer.highlight(el);
205
+ this.highlightedDom = el;
110
206
  }
111
207
 
112
- public add(data: UpdateData): void {
113
- this.renderer?.getRuntime().then((runtime) => runtime?.add?.(data));
208
+ public sortNode(data: SortEventData): Promise<void> {
209
+ return this.renderer?.getRuntime().then((runtime) => runtime?.sortNode?.(data));
114
210
  }
115
211
 
116
- public remove(data: RemoveData): void {
117
- this.renderer?.getRuntime().then((runtime) => runtime?.remove?.(data));
212
+ public add(data: UpdateData): Promise<void> {
213
+ return this.renderer?.getRuntime().then((runtime) => runtime?.add?.(data));
214
+ }
215
+
216
+ public remove(data: RemoveData): Promise<void> {
217
+ return this.renderer?.getRuntime().then((runtime) => runtime?.remove?.(data));
118
218
  }
119
219
 
120
220
  public setZoom(zoom: number = DEFAULT_ZOOM): void {
@@ -126,17 +226,12 @@ export default class StageCore extends EventEmitter {
126
226
  * @param el 将stage挂载到该Dom节点上
127
227
  */
128
228
  public mount(el: HTMLDivElement): void {
229
+ this.container = el;
129
230
  const { mask, renderer } = this;
130
231
 
131
232
  renderer.mount(el);
132
233
  mask.mount(el);
133
234
 
134
- const { wrapper: maskWrapper, hGuides, vGuides } = mask;
135
-
136
- maskWrapper.addEventListener('scroll', this.maskScrollHandler);
137
- hGuides.on('changeGuides', this.hGuidesChangeGuidesHandler);
138
- vGuides.on('changeGuides', this.vGuidesChangeGuidesHandler);
139
-
140
235
  this.emit('mounted');
141
236
  }
142
237
 
@@ -145,78 +240,31 @@ export default class StageCore extends EventEmitter {
145
240
  */
146
241
  public clearGuides() {
147
242
  this.mask.clearGuides();
148
- this.dr.setHGuidelines([]);
149
- this.dr.setVGuidelines([]);
243
+ this.dr.clearGuides();
150
244
  }
151
245
 
152
246
  /**
153
247
  * 销毁实例
154
248
  */
155
249
  public destroy(): void {
156
- const { mask, renderer, dr } = this;
157
- const { wrapper: maskWrapper, hGuides, vGuides } = mask;
250
+ const { mask, renderer, dr, highlightLayer } = this;
158
251
 
159
252
  renderer.destroy();
160
253
  mask.destroy();
161
254
  dr.destroy();
162
- maskWrapper.removeEventListener('scroll', this.maskScrollHandler);
163
- hGuides.off('changeGuides', this.hGuidesChangeGuidesHandler);
164
- vGuides.off('changeGuides', this.vGuidesChangeGuidesHandler);
165
- this.removeAllListeners();
166
- }
167
-
168
- private maskScrollHandler = (event: Event) => {
169
- const { mask, renderer } = this;
170
- const { wrapper: maskWrapper, hGuides, vGuides } = mask;
171
- const { scrollTop } = maskWrapper;
172
-
173
- renderer?.contentWindow?.document.documentElement.scrollTo({ top: scrollTop });
174
-
175
- hGuides.scrollGuides(scrollTop);
176
- hGuides.scroll(0);
177
-
178
- vGuides.scrollGuides(0);
179
- vGuides.scroll(scrollTop);
255
+ highlightLayer.destroy();
180
256
 
181
- this.emit('scroll', event);
182
- };
183
-
184
- private hGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
185
- this.dr.setHGuidelines(e.guides);
186
- this.emit('changeGuides');
187
- };
188
-
189
- private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
190
- this.dr.setVGuidelines(e.guides);
191
- this.emit('changeGuides');
192
- };
193
-
194
- private setMaskLayout(el: HTMLElement): void {
195
- let fixed = false;
196
- let dom = el;
197
- while (dom) {
198
- fixed = isFixed(dom);
199
- if (fixed) {
200
- break;
201
- }
202
- const { parentElement } = dom;
203
- if (!parentElement || parentElement.tagName === 'BODY') {
204
- break;
205
- }
206
- dom = parentElement;
207
- }
257
+ this.removeAllListeners();
208
258
 
209
- if (fixed) {
210
- this.mask?.setFixed();
211
- } else {
212
- this.mask?.setAbsolute();
213
- }
259
+ this.container = undefined;
214
260
  }
215
261
 
216
- private async beforeSelect(el: HTMLElement): Promise<void> {
217
- const runtime = await this.renderer?.getRuntime();
218
- if (runtime?.beforeSelect) {
219
- await runtime.beforeSelect(el);
262
+ private async getTargetElement(idOrEl: Id | HTMLElement): Promise<HTMLElement> {
263
+ if (typeof idOrEl === 'string' || typeof idOrEl === 'number') {
264
+ const el = this.renderer.contentWindow?.document.getElementById(`${idOrEl}`);
265
+ if (!el) throw new Error(`不存在ID为${idOrEl}的元素`);
266
+ return el;
220
267
  }
268
+ return idOrEl;
221
269
  }
222
270
  }