@tmagic/stage 1.0.0-beta.8 → 1.0.0-rc.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/src/StageMask.ts CHANGED
@@ -16,6 +16,8 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ import { throttle } from 'lodash-es';
20
+
19
21
  import { Mode, MouseButton, ZIndex } from './const';
20
22
  import Rule from './Rule';
21
23
  import type StageCore from './StageCore';
@@ -23,6 +25,7 @@ import type { StageMaskConfig } from './types';
23
25
  import { createDiv, getScrollParent, isFixedParent } from './util';
24
26
 
25
27
  const wrapperClassName = 'editor-mask-wrapper';
28
+ const throttleTime = 100;
26
29
 
27
30
  const hideScrollbar = () => {
28
31
  const style = globalThis.document.createElement('style');
@@ -78,10 +81,20 @@ export default class StageMask extends Rule {
78
81
  public height = 0;
79
82
  public wrapperHeight = 0;
80
83
  public wrapperWidth = 0;
84
+ public maxScrollTop = 0;
85
+ public maxScrollLeft = 0;
86
+ public intersectionObserver: IntersectionObserver | null = null;
81
87
 
82
88
  private mode: Mode = Mode.ABSOLUTE;
83
89
  private pageResizeObserver: ResizeObserver | null = null;
84
90
  private wrapperResizeObserver: ResizeObserver | null = null;
91
+ /**
92
+ * 高亮事件处理函数
93
+ * @param event 事件对象
94
+ */
95
+ private highlightHandler = throttle((event: MouseEvent): void => {
96
+ this.emit('highlight', event);
97
+ }, throttleTime);
85
98
 
86
99
  constructor(config: StageMaskConfig) {
87
100
  const wrapper = createWrapper();
@@ -93,6 +106,8 @@ export default class StageMask extends Rule {
93
106
  this.content.addEventListener('mousedown', this.mouseDownHandler);
94
107
  this.wrapper.appendChild(this.content);
95
108
  this.content.addEventListener('wheel', this.mouseWheelHandler);
109
+ this.content.addEventListener('mousemove', this.highlightHandler);
110
+ this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
96
111
  }
97
112
 
98
113
  public setMode(mode: Mode) {
@@ -118,6 +133,27 @@ export default class StageMask extends Rule {
118
133
  this.page = page;
119
134
  this.pageScrollParent = getScrollParent(page) || this.core.renderer.contentWindow?.document.documentElement || null;
120
135
  this.pageResizeObserver?.disconnect();
136
+ this.wrapperResizeObserver?.disconnect();
137
+ this.intersectionObserver?.disconnect();
138
+
139
+ if (typeof IntersectionObserver !== 'undefined') {
140
+ this.intersectionObserver = new IntersectionObserver(
141
+ (entries) => {
142
+ entries.forEach((entry) => {
143
+ const { target, intersectionRatio } = entry;
144
+ if (intersectionRatio <= 0) {
145
+ this.scrollIntoView(target);
146
+ }
147
+ this.intersectionObserver?.unobserve(target);
148
+ });
149
+ },
150
+ {
151
+ root: this.pageScrollParent,
152
+ rootMargin: '0px',
153
+ threshold: 1.0,
154
+ },
155
+ );
156
+ }
121
157
 
122
158
  if (typeof ResizeObserver !== 'undefined') {
123
159
  this.pageResizeObserver = new ResizeObserver((entries) => {
@@ -125,6 +161,11 @@ export default class StageMask extends Rule {
125
161
  const { clientHeight, clientWidth } = entry.target;
126
162
  this.setHeight(clientHeight);
127
163
  this.setWidth(clientWidth);
164
+
165
+ this.scroll();
166
+ if (this.core.dr.moveable) {
167
+ this.core.dr.updateMoveable();
168
+ }
128
169
  });
129
170
 
130
171
  this.pageResizeObserver.observe(page);
@@ -134,6 +175,8 @@ export default class StageMask extends Rule {
134
175
  const { clientHeight, clientWidth } = entry.target;
135
176
  this.wrapperHeight = clientHeight;
136
177
  this.wrapperWidth = clientWidth;
178
+ this.setMaxScrollLeft();
179
+ this.setMaxScrollTop();
137
180
  });
138
181
  this.wrapperResizeObserver.observe(this.wrapper);
139
182
  }
@@ -153,6 +196,14 @@ export default class StageMask extends Rule {
153
196
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
154
197
  }
155
198
 
199
+ public scrollIntoView(el: Element): void {
200
+ el.scrollIntoView();
201
+ if (!this.pageScrollParent) return;
202
+ this.scrollLeft = this.pageScrollParent.scrollLeft;
203
+ this.scrollTop = this.pageScrollParent.scrollTop;
204
+ this.scroll();
205
+ }
206
+
156
207
  /**
157
208
  * 销毁实例
158
209
  */
@@ -162,12 +213,23 @@ export default class StageMask extends Rule {
162
213
  this.pageScrollParent = null;
163
214
  this.pageResizeObserver?.disconnect();
164
215
  this.wrapperResizeObserver?.disconnect();
216
+
217
+ this.content.removeEventListener('mouseleave', this.mouseLeaveHandler);
165
218
  super.destroy();
166
219
  }
167
220
 
168
221
  private scroll() {
222
+ this.fixScrollValue();
223
+
169
224
  let { scrollLeft, scrollTop } = this;
170
225
 
226
+ if (this.pageScrollParent) {
227
+ this.pageScrollParent.scrollTo({
228
+ top: scrollTop,
229
+ left: scrollLeft,
230
+ });
231
+ }
232
+
171
233
  if (this.mode === Mode.FIXED) {
172
234
  scrollLeft = 0;
173
235
  scrollTop = 0;
@@ -187,6 +249,7 @@ export default class StageMask extends Rule {
187
249
  */
188
250
  private setHeight(height: number): void {
189
251
  this.height = height;
252
+ this.setMaxScrollTop();
190
253
  this.content.style.height = `${height}px`;
191
254
  }
192
255
 
@@ -196,14 +259,42 @@ export default class StageMask extends Rule {
196
259
  */
197
260
  private setWidth(width: number): void {
198
261
  this.width = width;
262
+ this.setMaxScrollLeft();
199
263
  this.content.style.width = `${width}px`;
200
264
  }
201
265
 
266
+ /**
267
+ * 计算并设置最大滚动宽度
268
+ */
269
+ private setMaxScrollLeft(): void {
270
+ this.maxScrollLeft = Math.max(this.width - this.wrapperWidth, 0);
271
+ }
272
+
273
+ /**
274
+ * 计算并设置最大滚动高度
275
+ */
276
+ private setMaxScrollTop(): void {
277
+ this.maxScrollTop = Math.max(this.height - this.wrapperHeight, 0);
278
+ }
279
+
280
+ /**
281
+ * 修复滚动距离
282
+ * 由于滚动容器变化等因素,会导致当前滚动的距离不正确
283
+ */
284
+ private fixScrollValue(): void {
285
+ if (this.scrollTop < 0) this.scrollTop = 0;
286
+ if (this.scrollLeft < 0) this.scrollLeft = 0;
287
+ if (this.maxScrollTop < this.scrollTop) this.scrollTop = this.maxScrollTop;
288
+ if (this.maxScrollLeft < this.scrollLeft) this.scrollLeft = this.maxScrollLeft;
289
+ }
290
+
202
291
  /**
203
292
  * 点击事件处理函数
204
293
  * @param event 事件对象
205
294
  */
206
- private mouseDownHandler = async (event: MouseEvent): Promise<void> => {
295
+ private mouseDownHandler = (event: MouseEvent): void => {
296
+ this.emit('clearHighlight');
297
+
207
298
  event.stopImmediatePropagation();
208
299
  event.stopPropagation();
209
300
 
@@ -214,6 +305,8 @@ export default class StageMask extends Rule {
214
305
  return;
215
306
  }
216
307
 
308
+ this.content.removeEventListener('mousemove', this.highlightHandler);
309
+
217
310
  this.emit('beforeSelect', event);
218
311
 
219
312
  // 如果是右键点击,这里的mouseup事件监听没有效果
@@ -222,46 +315,33 @@ export default class StageMask extends Rule {
222
315
 
223
316
  private mouseUpHandler = (): void => {
224
317
  globalThis.document.removeEventListener('mouseup', this.mouseUpHandler);
318
+ this.content.addEventListener('mousemove', this.highlightHandler);
225
319
  this.emit('select');
226
320
  };
227
321
 
228
322
  private mouseWheelHandler = (event: WheelEvent) => {
323
+ this.emit('clearHighlight');
229
324
  if (!this.page) throw new Error('page 未初始化');
230
325
 
231
326
  const { deltaY, deltaX } = event;
232
- const { height, wrapperHeight, width, wrapperWidth } = this;
233
-
234
- const maxScrollTop = height - wrapperHeight;
235
- const maxScrollLeft = width - wrapperWidth;
236
327
 
237
- if (maxScrollTop > 0) {
238
- if (deltaY > 0) {
239
- this.scrollTop = this.scrollTop + Math.min(maxScrollTop - this.scrollTop, deltaY);
240
- } else {
241
- this.scrollTop = Math.max(this.scrollTop + deltaY, 0);
242
- }
243
- }
328
+ if (this.page.clientHeight < this.wrapperHeight && deltaY) return;
329
+ if (this.page.clientWidth < this.wrapperWidth && deltaX) return;
244
330
 
245
- if (width > wrapperWidth) {
246
- if (deltaX > 0) {
247
- this.scrollLeft = this.scrollLeft + Math.min(maxScrollLeft - this.scrollLeft, deltaX);
248
- } else {
249
- this.scrollLeft = Math.max(this.scrollLeft + deltaX, 0);
250
- }
331
+ if (this.maxScrollTop > 0) {
332
+ this.scrollTop = this.scrollTop + deltaY;
251
333
  }
252
334
 
253
- if (this.mode !== Mode.FIXED) {
254
- this.scrollTo(this.scrollLeft, this.scrollTop);
335
+ if (this.maxScrollLeft > 0) {
336
+ this.scrollLeft = this.scrollLeft + deltaX;
255
337
  }
256
338
 
257
- if (this.pageScrollParent) {
258
- this.pageScrollParent.scrollTo({
259
- top: this.scrollTop,
260
- left: this.scrollLeft,
261
- });
262
- }
263
339
  this.scroll();
264
340
 
265
341
  this.emit('scroll', event);
266
342
  };
343
+
344
+ private mouseLeaveHandler = () => {
345
+ setTimeout(() => this.emit('clearHighlight'), throttleTime);
346
+ };
267
347
  }
@@ -114,6 +114,7 @@ export default class StageRender extends EventEmitter {
114
114
 
115
115
  private loadHandler = async () => {
116
116
  this.emit('onload');
117
+
117
118
  if (this.render) {
118
119
  const el = await this.render(this.core);
119
120
  if (el) {
@@ -0,0 +1,119 @@
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 { Mode } from './const';
23
+ import StageCore from './StageCore';
24
+ import StageDragResize from './StageDragResize';
25
+ import StageMask from './StageMask';
26
+ import type { Offset, TargetCalibrateConfig } from './types';
27
+ import { getMode } from './util';
28
+
29
+ /**
30
+ * 将选中的节点修正定位后,添加一个操作节点到蒙层上
31
+ */
32
+ export default class TargetCalibrate extends EventEmitter {
33
+ public parent: HTMLElement;
34
+ public mask: StageMask;
35
+ public dr: StageDragResize;
36
+ public core: StageCore;
37
+ public operationEl: HTMLDivElement;
38
+
39
+ constructor(config: TargetCalibrateConfig) {
40
+ super();
41
+
42
+ this.parent = config.parent;
43
+ this.mask = config.mask;
44
+ this.dr = config.dr;
45
+ this.core = config.core;
46
+
47
+ this.operationEl = globalThis.document.createElement('div');
48
+ this.parent.append(this.operationEl);
49
+ }
50
+
51
+ public update(el: HTMLElement, prefix: String): HTMLElement {
52
+ const { left, top } = this.getOffset(el);
53
+ const { transform } = getComputedStyle(el);
54
+ this.operationEl.style.cssText = `
55
+ position: absolute;
56
+ transform: ${transform};
57
+ left: ${left}px;
58
+ top: ${top}px;
59
+ width: ${el.clientWidth}px;
60
+ height: ${el.clientHeight}px;
61
+ `;
62
+
63
+ this.operationEl.id = `${prefix}${el.id}`;
64
+
65
+ if (typeof this.core.config.updateDragEl === 'function') {
66
+ this.core.config.updateDragEl(this.operationEl, el);
67
+ }
68
+
69
+ return this.operationEl;
70
+ }
71
+
72
+ public destroy(): void {
73
+ this.operationEl?.remove();
74
+ }
75
+
76
+ private getOffset(el: HTMLElement): Offset {
77
+ const { offsetParent } = el;
78
+
79
+ const left = el.offsetLeft;
80
+ const top = el.offsetTop;
81
+
82
+ if (offsetParent) {
83
+ const parentOffset = this.getOffset(offsetParent as HTMLElement);
84
+ return {
85
+ left: left + parentOffset.left,
86
+ top: top + parentOffset.top,
87
+ };
88
+ }
89
+
90
+ // 选中固定定位元素后editor-mask高度被置为视窗大小
91
+ if (this.dr.mode === Mode.FIXED) {
92
+ // 弹窗的情况
93
+ if (getMode(el) === Mode.FIXED) {
94
+ return {
95
+ left,
96
+ top,
97
+ };
98
+ }
99
+
100
+ return {
101
+ left: left - this.mask.scrollLeft,
102
+ top: top - this.mask.scrollTop,
103
+ };
104
+ }
105
+
106
+ // 无父元素的固定定位需按滚动值计算
107
+ if (getMode(el) === Mode.FIXED) {
108
+ return {
109
+ left: left + this.mask.scrollLeft,
110
+ top: top + this.mask.scrollTop,
111
+ };
112
+ }
113
+
114
+ return {
115
+ left,
116
+ top,
117
+ };
118
+ }
119
+ }
package/src/const.ts CHANGED
@@ -16,29 +16,58 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- // 流式布局下拖动时需要clone一个镜像节点,镜像节点的id前缀
19
+ /** 流式布局下拖动时需要clone一个镜像节点,镜像节点的id前缀 */
20
20
  export const GHOST_EL_ID_PREFIX = 'ghost_el_';
21
21
 
22
- // 默认放到缩小倍数
22
+ /** 拖动的时候需要在蒙层中创建一个占位节点,该节点的id前缀 */
23
+ export const DRAG_EL_ID_PREFIX = 'drag_el_';
24
+
25
+ /** 高亮事需要在蒙层中创建一个占位节点,该节点的id前缀 */
26
+ export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
27
+
28
+ /** 默认放到缩小倍数 */
23
29
  export const DEFAULT_ZOOM = 1;
24
30
 
31
+ /** 参考线类型 */
25
32
  export enum GuidesType {
33
+ /** 水平 */
26
34
  HORIZONTAL = 'horizontal',
35
+ /** 垂直 */
27
36
  VERTICAL = 'vertical',
28
37
  }
29
38
 
39
+ /** css z-index */
30
40
  export enum ZIndex {
41
+ /** 蒙层,用于监听用户操作,需要置于顶层 */
31
42
  MASK = '99999',
43
+ /** 选中的节点 */
44
+ SELECTED_EL = '666',
45
+ GHOST_EL = '700',
46
+ DRAG_EL = '9',
32
47
  }
33
48
 
49
+ /** 鼠标按键 */
34
50
  export enum MouseButton {
51
+ /** 左键 */
35
52
  LEFT = 0,
53
+ /** z中健 */
36
54
  MIDDLE = 1,
55
+ /** 右键 */
37
56
  RIGHT = 2,
38
57
  }
39
58
 
59
+ /** 布局方式 */
40
60
  export enum Mode {
61
+ /** 绝对定位布局 */
41
62
  ABSOLUTE = 'absolute',
63
+ /** 固定定位布局 */
42
64
  FIXED = 'fixed',
65
+ /** 流式布局 */
43
66
  SORTABLE = 'sortable',
44
67
  }
68
+
69
+ /** 选中节点的class name */
70
+ export const SELECTED_CLASS = 'tmagic-stage-selected-area';
71
+
72
+ export const H_GUIDE_LINE_STORAGE_KEY = '$MagicStageHorizontalGuidelinesData';
73
+ export const V_GUIDE_LINE_STORAGE_KEY = '$MagicStageVerticalGuidelinesData';
package/src/types.ts CHANGED
@@ -16,14 +16,17 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { MoveableOptions } from 'react-moveable/declaration/types';
19
+ import { MoveableOptions } from 'moveable';
20
20
 
21
+ import Core from '@tmagic/core';
21
22
  import { Id, MApp, MNode } from '@tmagic/schema';
22
23
 
23
24
  import { GuidesType } from './const';
24
25
  import StageCore from './StageCore';
26
+ import StageDragResize from './StageDragResize';
27
+ import StageMask from './StageMask';
25
28
 
26
- export type CanSelect = (el: HTMLElement, stop: () => boolean) => boolean | Promise<boolean>;
29
+ export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
27
30
 
28
31
  export type StageCoreConfig = {
29
32
  /** 需要对齐的dom节点的CSS选择器字符串 */
@@ -35,6 +38,8 @@ export type StageCoreConfig = {
35
38
  /** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
36
39
  runtimeUrl?: string;
37
40
  render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
41
+ autoScrollIntoView?: boolean;
42
+ updateDragEl?: (el: HTMLDivElement, target: HTMLElement) => void;
38
43
  };
39
44
 
40
45
  export interface StageRenderConfig {
@@ -69,10 +74,14 @@ export interface UpdateEventData {
69
74
  el: HTMLElement;
70
75
  ghostEl: HTMLElement;
71
76
  style: {
72
- width: number;
73
- height: number;
77
+ width?: number;
78
+ height?: number;
74
79
  left?: number;
75
80
  top?: number;
81
+ transform?: {
82
+ rotate?: string;
83
+ scale?: string;
84
+ };
76
85
  };
77
86
  }
78
87
 
@@ -93,8 +102,9 @@ export interface RemoveData {
93
102
  }
94
103
 
95
104
  export interface Runtime {
105
+ getApp?: () => Core;
96
106
  beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
97
- getSnapElements?: (el: HTMLElement) => HTMLElement[];
107
+ getSnapElements?: (el?: HTMLElement) => HTMLElement[];
98
108
  updateRootConfig: (config: MApp) => void;
99
109
  updatePageId?: (id: Id) => void;
100
110
  select?: (id: Id) => Promise<HTMLElement> | HTMLElement;
@@ -114,3 +124,15 @@ export interface Magic {
114
124
  export interface RuntimeWindow extends Window {
115
125
  magic: Magic;
116
126
  }
127
+
128
+ export interface StageHighlightConfig {
129
+ core: StageCore;
130
+ container: HTMLElement;
131
+ }
132
+
133
+ export interface TargetCalibrateConfig {
134
+ parent: HTMLElement;
135
+ mask: StageMask;
136
+ dr: StageDragResize;
137
+ core: StageCore;
138
+ }
package/src/util.ts CHANGED
@@ -16,39 +16,24 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { Mode } from './const';
19
+ import { GuidesType, H_GUIDE_LINE_STORAGE_KEY, Mode, SELECTED_CLASS, V_GUIDE_LINE_STORAGE_KEY } from './const';
20
20
  import type { Offset } from './types';
21
21
 
22
+ const getParents = (el: Element, relative: Element) => {
23
+ let cur: Element | null = el.parentElement;
24
+ const parents: Element[] = [];
25
+ while (cur && cur !== relative) {
26
+ parents.push(cur);
27
+ cur = cur.parentElement;
28
+ }
29
+ return parents;
30
+ };
31
+
22
32
  export const getOffset = (el: HTMLElement): Offset => {
23
- const { transform } = getComputedStyle(el);
24
33
  const { offsetParent } = el;
25
34
 
26
- let left = el.offsetLeft;
27
- let top = el.offsetTop;
28
-
29
- if (transform.indexOf('matrix') > -1) {
30
- let a = 1;
31
- let b = 1;
32
- let c = 1;
33
- let d = 1;
34
- let e = 0;
35
- let f = 0;
36
- transform.replace(
37
- /matrix\((.+), (.+), (.+), (.+), (.+), (.+)\)/,
38
- ($0: string, $1: string, $2: string, $3: string, $4: string, $5: string, $6: string): string => {
39
- a = +$1;
40
- b = +$2;
41
- c = +$3;
42
- d = +$4;
43
- e = +$5;
44
- f = +$6;
45
- return transform;
46
- },
47
- );
48
-
49
- left = a * left + c * top + e;
50
- top = b * left + d * top + f;
51
- }
35
+ const left = el.offsetLeft;
36
+ const top = el.offsetTop;
52
37
 
53
38
  if (offsetParent) {
54
39
  const parentOffset = getOffset(offsetParent as HTMLElement);
@@ -154,3 +139,43 @@ export const createDiv = ({ className, cssText }: { className: string; cssText:
154
139
  el.style.cssText = cssText;
155
140
  return el;
156
141
  };
142
+
143
+ export const removeSelectedClassName = (doc: Document) => {
144
+ const oldEl = doc.querySelector(`.${SELECTED_CLASS}`);
145
+
146
+ if (oldEl) {
147
+ oldEl.classList.remove(SELECTED_CLASS);
148
+ (oldEl.parentNode as HTMLDivElement)?.classList.remove(`${SELECTED_CLASS}-parent`);
149
+ doc.querySelectorAll(`.${SELECTED_CLASS}-parents`).forEach((item) => {
150
+ item.classList.remove(`${SELECTED_CLASS}-parents`);
151
+ });
152
+ }
153
+ };
154
+
155
+ export const addSelectedClassName = (el: Element, doc: Document) => {
156
+ el.classList.add(SELECTED_CLASS);
157
+ (el.parentNode as Element)?.classList.add(`${SELECTED_CLASS}-parent`);
158
+ getParents(el, doc.body).forEach((item) => {
159
+ item.classList.add(`${SELECTED_CLASS}-parents`);
160
+ });
161
+ };
162
+
163
+ export const getGuideLineFromCache = (type: GuidesType): number[] => {
164
+ const key = {
165
+ [GuidesType.HORIZONTAL]: H_GUIDE_LINE_STORAGE_KEY,
166
+ [GuidesType.VERTICAL]: V_GUIDE_LINE_STORAGE_KEY,
167
+ }[type];
168
+
169
+ if (!key) return [];
170
+
171
+ const guideLineCacheData = globalThis.localStorage.getItem(key);
172
+ if (guideLineCacheData) {
173
+ try {
174
+ return JSON.parse(guideLineCacheData) || [];
175
+ } catch (e) {
176
+ console.error(e);
177
+ }
178
+ }
179
+
180
+ return [];
181
+ };