@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/StageMask.ts CHANGED
@@ -16,17 +16,16 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { EventEmitter } from 'events';
19
+ import { throttle } from 'lodash-es';
20
20
 
21
- import Guides from '@scena/guides';
22
- import { isNumber } from 'lodash';
23
-
24
- import { GHOST_EL_ID_PREFIX } from './const';
21
+ import { Mode, MouseButton, ZIndex } from './const';
22
+ import Rule from './Rule';
25
23
  import type StageCore from './StageCore';
26
- import type { CanSelect, StageMaskConfig } from './types';
27
- import { MouseButton, ZIndex } from './types';
24
+ import type { StageMaskConfig } from './types';
25
+ import { createDiv, getScrollParent, isFixedParent } from './util';
28
26
 
29
27
  const wrapperClassName = 'editor-mask-wrapper';
28
+ const throttleTime = 100;
30
29
 
31
30
  const hideScrollbar = () => {
32
31
  const style = globalThis.document.createElement('style');
@@ -36,30 +35,30 @@ const hideScrollbar = () => {
36
35
  globalThis.document.head.appendChild(style);
37
36
  };
38
37
 
39
- const createContent = (): HTMLDivElement => {
40
- const el = globalThis.document.createElement('div');
41
- el.className = 'editor-mask';
42
- el.style.cssText = `
38
+ const createContent = (): HTMLDivElement =>
39
+ createDiv({
40
+ className: 'editor-mask',
41
+ cssText: `
43
42
  position: absolute;
44
43
  top: 0;
45
44
  left: 0;
46
45
  transform: translate3d(0, 0, 0);
47
- `;
48
- return el;
49
- };
46
+ `,
47
+ });
50
48
 
51
49
  const createWrapper = (): HTMLDivElement => {
52
- const el = globalThis.document.createElement('div');
53
- el.className = wrapperClassName;
54
- el.style.cssText = `
55
- position: absolute;
56
- top: 0;
57
- left: 0;
58
- height: 100%;
59
- width: 100%;
60
- overflow: auto;
61
- z-index: ${ZIndex.MASK};
62
- `;
50
+ const el = createDiv({
51
+ className: wrapperClassName,
52
+ cssText: `
53
+ position: absolute;
54
+ top: 0;
55
+ left: 0;
56
+ height: 100%;
57
+ width: 100%;
58
+ overflow: hidden;
59
+ z-index: ${ZIndex.MASK};
60
+ `,
61
+ });
63
62
 
64
63
  hideScrollbar();
65
64
 
@@ -70,54 +69,56 @@ const createWrapper = (): HTMLDivElement => {
70
69
  * 蒙层
71
70
  * @description 用于拦截页面的点击动作,避免点击时触发组件自身动作;在编辑器中点击组件应当是选中组件;
72
71
  */
73
- export default class StageMask extends EventEmitter {
72
+ export default class StageMask extends Rule {
74
73
  public content: HTMLDivElement = createContent();
75
- public wrapper: HTMLDivElement = createWrapper();
74
+ public wrapper: HTMLDivElement;
76
75
  public core: StageCore;
77
76
  public page: HTMLElement | null = null;
78
-
79
- public hGuides: Guides;
80
- public vGuides: Guides;
81
-
82
- private target: Element | null = null;
83
- private resizeObserver: ResizeObserver | null = null;
84
- private parentResizeObserver: ResizeObserver | null = null;
85
- private canSelect: CanSelect;
77
+ public pageScrollParent: HTMLElement | null = null;
78
+ public scrollTop = 0;
79
+ public scrollLeft = 0;
80
+ public width = 0;
81
+ public height = 0;
82
+ public wrapperHeight = 0;
83
+ public wrapperWidth = 0;
84
+ public maxScrollTop = 0;
85
+ public maxScrollLeft = 0;
86
+
87
+ private mode: Mode = Mode.ABSOLUTE;
88
+ private pageResizeObserver: ResizeObserver | null = null;
89
+ private wrapperResizeObserver: ResizeObserver | null = null;
90
+ /**
91
+ * 高亮事件处理函数
92
+ * @param event 事件对象
93
+ */
94
+ private highlightHandler = throttle((event: MouseEvent): void => {
95
+ this.emit('highlight', event);
96
+ }, throttleTime);
86
97
 
87
98
  constructor(config: StageMaskConfig) {
88
- super();
99
+ const wrapper = createWrapper();
100
+ super(wrapper);
89
101
 
102
+ this.wrapper = wrapper;
90
103
  this.core = config.core;
91
- const { config: coreConfig } = config.core;
92
104
 
93
- this.canSelect = coreConfig.canSelect || ((el: HTMLElement) => !!el.id);
94
- this.content.addEventListener('mousedown', this.mouseDownHandler, true);
95
- this.content.addEventListener('contextmenu', this.contextmenuHandler, true);
105
+ this.content.addEventListener('mousedown', this.mouseDownHandler);
96
106
  this.wrapper.appendChild(this.content);
97
-
98
- this.hGuides = this.createGuides('horizontal');
99
- this.vGuides = this.createGuides('vertical');
107
+ this.content.addEventListener('wheel', this.mouseWheelHandler);
108
+ this.content.addEventListener('mousemove', this.highlightHandler);
109
+ this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
100
110
  }
101
111
 
102
- /**
103
- * 设置成固定定位模式
104
- */
105
- public setFixed(): void {
106
- this.wrapper.scrollTo({
107
- top: 0,
108
- });
109
- this.wrapper.style.overflow = 'hidden';
110
- // 要等滚动条滚上去,才刷新选中框
111
- setTimeout(() => {
112
- this.core.dr.refresh();
113
- });
114
- }
115
-
116
- /**
117
- * 设置成绝对定位模式
118
- */
119
- public setAbsolute(): void {
120
- this.wrapper.style.overflow = 'auto';
112
+ public setMode(mode: Mode) {
113
+ this.mode = mode;
114
+ this.scroll();
115
+ if (mode === Mode.FIXED) {
116
+ this.content.style.width = `${this.wrapperWidth}px`;
117
+ this.content.style.height = `${this.wrapperHeight}px`;
118
+ } else {
119
+ this.content.style.width = `${this.width}px`;
120
+ this.content.style.height = `${this.height}px`;
121
+ }
121
122
  }
122
123
 
123
124
  /**
@@ -129,17 +130,32 @@ export default class StageMask extends EventEmitter {
129
130
  if (!page) return;
130
131
 
131
132
  this.page = page;
132
- this.resizeObserver?.disconnect();
133
+ this.pageScrollParent = getScrollParent(page) || this.core.renderer.contentWindow?.document.documentElement || null;
134
+ this.pageResizeObserver?.disconnect();
133
135
 
134
136
  if (typeof ResizeObserver !== 'undefined') {
135
- this.resizeObserver = new ResizeObserver((entries) => {
137
+ this.pageResizeObserver = new ResizeObserver((entries) => {
136
138
  const [entry] = entries;
137
139
  const { clientHeight, clientWidth } = entry.target;
138
140
  this.setHeight(clientHeight);
139
141
  this.setWidth(clientWidth);
142
+
143
+ this.fixScrollValue();
144
+ this.scroll();
145
+ this.core.dr.updateMoveable();
140
146
  });
141
147
 
142
- this.resizeObserver.observe(page);
148
+ this.pageResizeObserver.observe(page);
149
+
150
+ this.wrapperResizeObserver = new ResizeObserver((entries) => {
151
+ const [entry] = entries;
152
+ const { clientHeight, clientWidth } = entry.target;
153
+ this.wrapperHeight = clientHeight;
154
+ this.wrapperWidth = clientWidth;
155
+ this.setMaxScrollLeft();
156
+ this.setMaxScrollTop();
157
+ });
158
+ this.wrapperResizeObserver.observe(this.wrapper);
143
159
  }
144
160
  }
145
161
 
@@ -151,12 +167,10 @@ export default class StageMask extends EventEmitter {
151
167
  if (!this.content) throw new Error('content 不存在');
152
168
 
153
169
  el.appendChild(this.wrapper);
170
+ }
154
171
 
155
- this.parentResizeObserver = new ResizeObserver(() => {
156
- this.vGuides.resize();
157
- this.hGuides.resize();
158
- });
159
- this.parentResizeObserver.observe(el);
172
+ public setLayout(el: HTMLElement): void {
173
+ this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
160
174
  }
161
175
 
162
176
  /**
@@ -165,144 +179,134 @@ export default class StageMask extends EventEmitter {
165
179
  public destroy(): void {
166
180
  this.content?.remove();
167
181
  this.page = null;
168
- this.resizeObserver?.disconnect();
169
- this.parentResizeObserver?.disconnect();
170
- this.removeAllListeners();
182
+ this.pageScrollParent = null;
183
+ this.pageResizeObserver?.disconnect();
184
+ this.wrapperResizeObserver?.disconnect();
185
+
186
+ this.content.removeEventListener('mouseleave', this.mouseLeaveHandler);
187
+ super.destroy();
188
+ }
189
+
190
+ private scroll() {
191
+ let { scrollLeft, scrollTop } = this;
192
+
193
+ if (this.pageScrollParent) {
194
+ this.pageScrollParent.scrollTo({
195
+ top: scrollTop,
196
+ left: scrollLeft,
197
+ });
198
+ }
199
+
200
+ if (this.mode === Mode.FIXED) {
201
+ scrollLeft = 0;
202
+ scrollTop = 0;
203
+ }
204
+
205
+ this.scrollRule(scrollTop);
206
+ this.scrollTo(scrollLeft, scrollTop);
207
+ }
208
+
209
+ private scrollTo(scrollLeft: number, scrollTop: number): void {
210
+ this.content.style.transform = `translate3d(${-scrollLeft}px, ${-scrollTop}px, 0)`;
171
211
  }
172
212
 
173
213
  /**
174
- * 是否显示标尺
175
- * @param show 是否显示
214
+ * 设置蒙层高度
215
+ * @param height 高度
176
216
  */
177
- public showGuides(show = true) {
178
- this.hGuides.setState({
179
- showGuides: show,
180
- });
181
-
182
- this.vGuides.setState({
183
- showGuides: show,
184
- });
217
+ private setHeight(height: number): void {
218
+ this.height = height;
219
+ this.setMaxScrollTop();
220
+ this.content.style.height = `${height}px`;
185
221
  }
186
222
 
187
223
  /**
188
- * 是否显示标尺
189
- * @param show 是否显示
224
+ * 设置蒙层宽度
225
+ * @param width 宽度
190
226
  */
191
- public showRule(show = true) {
192
- this.hGuides.setState({
193
- rulerStyle: {
194
- visibility: show ? '' : 'hidden',
195
- },
196
- });
197
-
198
- this.vGuides.setState({
199
- rulerStyle: {
200
- visibility: show ? '' : 'hidden',
201
- },
202
- });
227
+ private setWidth(width: number): void {
228
+ this.width = width;
229
+ this.setMaxScrollLeft();
230
+ this.content.style.width = `${width}px`;
203
231
  }
204
232
 
205
233
  /**
206
- * 清空所有参考线
234
+ * 计算并设置最大滚动宽度
207
235
  */
208
- public clearGuides() {
209
- this.vGuides.setState({
210
- defaultGuides: [],
211
- });
212
- this.hGuides.setState({
213
- defaultGuides: [],
214
- });
236
+ private setMaxScrollLeft(): void {
237
+ this.maxScrollLeft = this.width - this.wrapperWidth;
215
238
  }
216
239
 
217
240
  /**
218
- * 设置蒙层高度
219
- * @param height 高度
241
+ * 计算并设置最大滚动高度
220
242
  */
221
- private setHeight(height: number | string): void {
222
- this.content.style.height = isNumber(height) ? `${height}px` : height;
243
+ private setMaxScrollTop(): void {
244
+ this.maxScrollTop = this.height - this.wrapperHeight;
223
245
  }
224
246
 
225
247
  /**
226
- * 设置蒙层宽度
227
- * @param width 宽度
248
+ * 修复滚动距离
249
+ * 由于滚动容器变化等因素,会导致当前滚动的距离不正确
228
250
  */
229
- private setWidth(width: number | string): void {
230
- this.content.style.width = isNumber(width) ? `${width}px` : width;
251
+ private fixScrollValue(): void {
252
+ if (this.scrollTop < 0) this.scrollTop = 0;
253
+ if (this.scrollLeft < 0) this.scrollLeft = 0;
254
+ if (this.maxScrollTop < this.scrollTop) this.scrollTop = this.maxScrollTop;
255
+ if (this.maxScrollLeft < this.scrollLeft) this.scrollLeft = this.maxScrollLeft;
231
256
  }
232
257
 
233
258
  /**
234
259
  * 点击事件处理函数
235
260
  * @param event 事件对象
236
261
  */
237
- private mouseDownHandler = async (event: MouseEvent): Promise<void> => {
238
- if (event.button !== MouseButton.LEFT) return;
262
+ private mouseDownHandler = (event: MouseEvent): void => {
263
+ this.emit('clearHighlight');
264
+
265
+ event.stopImmediatePropagation();
266
+ event.stopPropagation();
267
+
268
+ if (event.button !== MouseButton.LEFT && event.button !== MouseButton.RIGHT) return;
239
269
 
240
270
  // 点击的对象如果是选中框,则不需要再触发选中了,而可能是拖动行为
241
271
  if ((event.target as HTMLDivElement).className.indexOf('moveable-control') !== -1) {
242
272
  return;
243
273
  }
244
274
 
245
- this.select(event);
246
- };
275
+ this.content.removeEventListener('mousemove', this.highlightHandler);
247
276
 
248
- private mouseUpHandler = (): void => {
249
- this.content?.removeEventListener('mouseup', this.mouseUpHandler, true);
250
- this.emit('selected', this.target);
251
- this.target = null;
252
- };
277
+ this.emit('beforeSelect', event);
253
278
 
254
- private contextmenuHandler = async (event: MouseEvent): Promise<void> => {
255
- await this.select(event);
256
- this.mouseUpHandler();
279
+ // 如果是右键点击,这里的mouseup事件监听没有效果
280
+ globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
257
281
  };
258
282
 
259
- private async select(event: MouseEvent) {
260
- const { renderer, zoom } = this.core;
283
+ private mouseUpHandler = (): void => {
284
+ globalThis.document.removeEventListener('mouseup', this.mouseUpHandler);
285
+ this.content.addEventListener('mousemove', this.highlightHandler);
286
+ this.emit('select');
287
+ };
261
288
 
262
- const doc = renderer.contentWindow?.document;
263
- let x = event.clientX;
264
- let y = event.clientY;
289
+ private mouseWheelHandler = (event: WheelEvent) => {
290
+ this.emit('clearHighlight');
291
+ if (!this.page) throw new Error('page 未初始化');
265
292
 
266
- if (renderer.iframe) {
267
- const rect = renderer.iframe.getClientRects()[0];
268
- if (rect) {
269
- x = x - rect.left;
270
- y = y - rect.top;
271
- }
293
+ const { deltaY, deltaX } = event;
294
+ if (this.maxScrollTop > 0) {
295
+ this.scrollTop = this.scrollTop + deltaY;
272
296
  }
273
297
 
274
- const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
298
+ if (this.maxScrollLeft > 0) {
299
+ this.scrollLeft = this.scrollLeft + deltaX;
300
+ }
275
301
 
276
- let stopped = false;
277
- const stop = () => (stopped = true);
278
- for (const el of els) {
279
- if (!el.id.startsWith(GHOST_EL_ID_PREFIX) && (await this.canSelect(el, stop))) {
280
- if (stopped) break;
302
+ this.fixScrollValue();
281
303
 
282
- this.emit('select', el, event);
283
- this.target = el;
284
- // 如果是右键点击,这里的mouseup事件监听没有效果
285
- this.content?.addEventListener('mouseup', this.mouseUpHandler, true);
286
- break;
287
- }
288
- }
289
- }
304
+ this.scroll();
290
305
 
291
- private getGuidesStyle = (type: 'horizontal' | 'vertical') => ({
292
- position: 'fixed',
293
- left: type === 'horizontal' ? 0 : '-30px',
294
- top: type === 'horizontal' ? '-30px' : 0,
295
- width: type === 'horizontal' ? '100%' : '30px',
296
- height: type === 'horizontal' ? '30px' : '100%',
297
- });
306
+ this.emit('scroll', event);
307
+ };
298
308
 
299
- private createGuides = (type: 'horizontal' | 'vertical'): Guides =>
300
- new Guides(this.wrapper, {
301
- type,
302
- displayDragPos: true,
303
- backgroundColor: '#fff',
304
- lineColor: '#000',
305
- textColor: '#000',
306
- style: this.getGuidesStyle(type),
307
- });
309
+ private mouseLeaveHandler = () => {
310
+ setTimeout(() => this.emit('clearHighlight'), throttleTime);
311
+ };
308
312
  }
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { EventEmitter } from 'events';
20
20
 
21
+ import { SELECTED_CLASS, ZIndex } from './const';
21
22
  import StageCore from './StageCore';
22
23
  import type { Runtime, RuntimeWindow, StageRenderConfig } from './types';
23
24
  import { getHost, isSameDomain } from './util';
@@ -52,15 +53,7 @@ export default class StageRender extends EventEmitter {
52
53
  height: 100%;
53
54
  `;
54
55
 
55
- this.iframe.onload = async () => {
56
- this.emit('onload');
57
- if (this.render) {
58
- const el = await this.render(this.core);
59
- if (el) {
60
- this.iframe?.contentDocument?.body?.appendChild(el);
61
- }
62
- }
63
- };
56
+ this.iframe.addEventListener('load', this.loadHandler);
64
57
  }
65
58
 
66
59
  public getMagicApi = () => ({
@@ -113,9 +106,32 @@ export default class StageRender extends EventEmitter {
113
106
  * 销毁实例
114
107
  */
115
108
  public destroy(): void {
109
+ this.iframe?.removeEventListener('load', this.loadHandler);
116
110
  this.contentWindow = null;
117
111
  this.iframe?.remove();
118
112
  this.iframe = undefined;
119
113
  this.removeAllListeners();
120
114
  }
115
+
116
+ private loadHandler = async () => {
117
+ this.emit('onload');
118
+
119
+ if (this.render) {
120
+ const el = await this.render(this.core);
121
+ if (el) {
122
+ this.iframe?.contentDocument?.body?.appendChild(el);
123
+ }
124
+ }
125
+
126
+ if (this.contentWindow) {
127
+ const style = this.contentWindow.document.createElement('style');
128
+ style.id = 'tmagic-stage-render';
129
+ style.innerHTML = `
130
+ .${SELECTED_CLASS}, .${SELECTED_CLASS}-parent {
131
+ z-index: ${ZIndex.SELECTED_EL};
132
+ }
133
+ `;
134
+ this.contentWindow.document.head.appendChild(style);
135
+ }
136
+ };
121
137
  }
package/src/const.ts CHANGED
@@ -19,5 +19,31 @@
19
19
  // 流式布局下拖动时需要clone一个镜像节点,镜像节点的id前缀
20
20
  export const GHOST_EL_ID_PREFIX = 'ghost_el_';
21
21
 
22
+ export const DRAG_EL_ID_PREFIX = 'drag_el_';
23
+
22
24
  // 默认放到缩小倍数
23
25
  export const DEFAULT_ZOOM = 1;
26
+
27
+ export enum GuidesType {
28
+ HORIZONTAL = 'horizontal',
29
+ VERTICAL = 'vertical',
30
+ }
31
+
32
+ export enum ZIndex {
33
+ MASK = '99999',
34
+ SELECTED_EL = '666',
35
+ }
36
+
37
+ export enum MouseButton {
38
+ LEFT = 0,
39
+ MIDDLE = 1,
40
+ RIGHT = 2,
41
+ }
42
+
43
+ export enum Mode {
44
+ ABSOLUTE = 'absolute',
45
+ FIXED = 'fixed',
46
+ SORTABLE = 'sortable',
47
+ }
48
+
49
+ export const SELECTED_CLASS = 'tmagic-stage-selected-area';
package/src/types.ts CHANGED
@@ -1,10 +1,29 @@
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
+
1
19
  import { MoveableOptions } from 'react-moveable/declaration/types';
2
20
 
3
21
  import { Id, MApp, MNode } from '@tmagic/schema';
4
22
 
23
+ import { GuidesType } from './const';
5
24
  import StageCore from './StageCore';
6
25
 
7
- export type CanSelect = (el: HTMLElement, stop: () => boolean) => boolean | Promise<boolean>;
26
+ export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
8
27
 
9
28
  export type StageCoreConfig = {
10
29
  /** 需要对齐的dom节点的CSS选择器字符串 */
@@ -41,23 +60,10 @@ export interface Offset {
41
60
  top: number;
42
61
  }
43
62
 
44
- /*
45
- * Tencent is pleased to support the open source community by making TMagicEditor available.
46
- *
47
- * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
48
- *
49
- * Licensed under the Apache License, Version 2.0 (the "License");
50
- * you may not use this file except in compliance with the License.
51
- * You may obtain a copy of the License at
52
- *
53
- * http://www.apache.org/licenses/LICENSE-2.0
54
- *
55
- * Unless required by applicable law or agreed to in writing, software
56
- * distributed under the License is distributed on an "AS IS" BASIS,
57
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
58
- * See the License for the specific language governing permissions and
59
- * limitations under the License.
60
- */
63
+ export interface GuidesEventData {
64
+ type: GuidesType;
65
+ guides: number[];
66
+ }
61
67
 
62
68
  export interface UpdateEventData {
63
69
  el: HTMLElement;
@@ -88,7 +94,7 @@ export interface RemoveData {
88
94
 
89
95
  export interface Runtime {
90
96
  beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
91
- getSnapElements?: (el: HTMLElement) => HTMLElement[];
97
+ getSnapElements?: (el?: HTMLElement) => HTMLElement[];
92
98
  updateRootConfig: (config: MApp) => void;
93
99
  updatePageId?: (id: Id) => void;
94
100
  select?: (id: Id) => Promise<HTMLElement> | HTMLElement;
@@ -109,13 +115,7 @@ export interface RuntimeWindow extends Window {
109
115
  magic: Magic;
110
116
  }
111
117
 
112
- export enum ZIndex {
113
- MASK = '99999',
114
- GHOST_EL = '99998',
115
- }
116
-
117
- export enum MouseButton {
118
- LEFT = 0,
119
- MIDDLE = 1,
120
- RIGHT = 2,
118
+ export interface StageHighlightConfig {
119
+ core: StageCore;
120
+ container: HTMLElement;
121
121
  }