@tmagic/stage 1.0.0-beta.9 → 1.0.0-rc.3

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
@@ -81,6 +81,9 @@ export default class StageMask extends Rule {
81
81
  public height = 0;
82
82
  public wrapperHeight = 0;
83
83
  public wrapperWidth = 0;
84
+ public maxScrollTop = 0;
85
+ public maxScrollLeft = 0;
86
+ public intersectionObserver: IntersectionObserver | null = null;
84
87
 
85
88
  private mode: Mode = Mode.ABSOLUTE;
86
89
  private pageResizeObserver: ResizeObserver | null = null;
@@ -104,6 +107,7 @@ export default class StageMask extends Rule {
104
107
  this.wrapper.appendChild(this.content);
105
108
  this.content.addEventListener('wheel', this.mouseWheelHandler);
106
109
  this.content.addEventListener('mousemove', this.highlightHandler);
110
+ this.content.addEventListener('mouseleave', this.mouseLeaveHandler);
107
111
  }
108
112
 
109
113
  public setMode(mode: Mode) {
@@ -129,6 +133,27 @@ export default class StageMask extends Rule {
129
133
  this.page = page;
130
134
  this.pageScrollParent = getScrollParent(page) || this.core.renderer.contentWindow?.document.documentElement || null;
131
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
+ }
132
157
 
133
158
  if (typeof ResizeObserver !== 'undefined') {
134
159
  this.pageResizeObserver = new ResizeObserver((entries) => {
@@ -136,6 +161,10 @@ export default class StageMask extends Rule {
136
161
  const { clientHeight, clientWidth } = entry.target;
137
162
  this.setHeight(clientHeight);
138
163
  this.setWidth(clientWidth);
164
+
165
+ this.fixScrollValue();
166
+ this.scroll();
167
+ this.core.dr.updateMoveable();
139
168
  });
140
169
 
141
170
  this.pageResizeObserver.observe(page);
@@ -145,6 +174,8 @@ export default class StageMask extends Rule {
145
174
  const { clientHeight, clientWidth } = entry.target;
146
175
  this.wrapperHeight = clientHeight;
147
176
  this.wrapperWidth = clientWidth;
177
+ this.setMaxScrollLeft();
178
+ this.setMaxScrollTop();
148
179
  });
149
180
  this.wrapperResizeObserver.observe(this.wrapper);
150
181
  }
@@ -164,6 +195,14 @@ export default class StageMask extends Rule {
164
195
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
165
196
  }
166
197
 
198
+ public scrollIntoView(el: Element): void {
199
+ el.scrollIntoView();
200
+ if (!this.pageScrollParent) return;
201
+ this.scrollLeft = this.pageScrollParent.scrollLeft;
202
+ this.scrollTop = this.pageScrollParent.scrollTop;
203
+ this.scroll();
204
+ }
205
+
167
206
  /**
168
207
  * 销毁实例
169
208
  */
@@ -173,12 +212,21 @@ export default class StageMask extends Rule {
173
212
  this.pageScrollParent = null;
174
213
  this.pageResizeObserver?.disconnect();
175
214
  this.wrapperResizeObserver?.disconnect();
215
+
216
+ this.content.removeEventListener('mouseleave', this.mouseLeaveHandler);
176
217
  super.destroy();
177
218
  }
178
219
 
179
220
  private scroll() {
180
221
  let { scrollLeft, scrollTop } = this;
181
222
 
223
+ if (this.pageScrollParent) {
224
+ this.pageScrollParent.scrollTo({
225
+ top: scrollTop,
226
+ left: scrollLeft,
227
+ });
228
+ }
229
+
182
230
  if (this.mode === Mode.FIXED) {
183
231
  scrollLeft = 0;
184
232
  scrollTop = 0;
@@ -198,6 +246,7 @@ export default class StageMask extends Rule {
198
246
  */
199
247
  private setHeight(height: number): void {
200
248
  this.height = height;
249
+ this.setMaxScrollTop();
201
250
  this.content.style.height = `${height}px`;
202
251
  }
203
252
 
@@ -207,14 +256,42 @@ export default class StageMask extends Rule {
207
256
  */
208
257
  private setWidth(width: number): void {
209
258
  this.width = width;
259
+ this.setMaxScrollLeft();
210
260
  this.content.style.width = `${width}px`;
211
261
  }
212
262
 
263
+ /**
264
+ * 计算并设置最大滚动宽度
265
+ */
266
+ private setMaxScrollLeft(): void {
267
+ this.maxScrollLeft = this.width - this.wrapperWidth;
268
+ }
269
+
270
+ /**
271
+ * 计算并设置最大滚动高度
272
+ */
273
+ private setMaxScrollTop(): void {
274
+ this.maxScrollTop = this.height - this.wrapperHeight;
275
+ }
276
+
277
+ /**
278
+ * 修复滚动距离
279
+ * 由于滚动容器变化等因素,会导致当前滚动的距离不正确
280
+ */
281
+ private fixScrollValue(): void {
282
+ if (this.scrollTop < 0) this.scrollTop = 0;
283
+ if (this.scrollLeft < 0) this.scrollLeft = 0;
284
+ if (this.maxScrollTop < this.scrollTop) this.scrollTop = this.maxScrollTop;
285
+ if (this.maxScrollLeft < this.scrollLeft) this.scrollLeft = this.maxScrollLeft;
286
+ }
287
+
213
288
  /**
214
289
  * 点击事件处理函数
215
290
  * @param event 事件对象
216
291
  */
217
292
  private mouseDownHandler = (event: MouseEvent): void => {
293
+ this.emit('clearHighlight');
294
+
218
295
  event.stopImmediatePropagation();
219
296
  event.stopPropagation();
220
297
 
@@ -225,12 +302,12 @@ export default class StageMask extends Rule {
225
302
  return;
226
303
  }
227
304
 
305
+ this.content.removeEventListener('mousemove', this.highlightHandler);
306
+
228
307
  this.emit('beforeSelect', event);
229
308
 
230
309
  // 如果是右键点击,这里的mouseup事件监听没有效果
231
310
  globalThis.document.addEventListener('mouseup', this.mouseUpHandler);
232
- this.content.removeEventListener('mousemove', this.highlightHandler);
233
- this.emit('clearHighlight');
234
311
  };
235
312
 
236
313
  private mouseUpHandler = (): void => {
@@ -240,42 +317,30 @@ export default class StageMask extends Rule {
240
317
  };
241
318
 
242
319
  private mouseWheelHandler = (event: WheelEvent) => {
320
+ this.emit('clearHighlight');
243
321
  if (!this.page) throw new Error('page 未初始化');
244
322
 
245
323
  const { deltaY, deltaX } = event;
246
- const { height, wrapperHeight, width, wrapperWidth } = this;
247
324
 
248
- const maxScrollTop = height - wrapperHeight;
249
- const maxScrollLeft = width - wrapperWidth;
325
+ if (this.page.clientHeight < this.wrapperHeight && deltaY) return;
326
+ if (this.page.clientWidth < this.wrapperWidth && deltaX) return;
250
327
 
251
- if (maxScrollTop > 0) {
252
- if (deltaY > 0) {
253
- this.scrollTop = this.scrollTop + Math.min(maxScrollTop - this.scrollTop, deltaY);
254
- } else {
255
- this.scrollTop = Math.max(this.scrollTop + deltaY, 0);
256
- }
328
+ if (this.maxScrollTop > 0) {
329
+ this.scrollTop = this.scrollTop + deltaY;
257
330
  }
258
331
 
259
- if (width > wrapperWidth) {
260
- if (deltaX > 0) {
261
- this.scrollLeft = this.scrollLeft + Math.min(maxScrollLeft - this.scrollLeft, deltaX);
262
- } else {
263
- this.scrollLeft = Math.max(this.scrollLeft + deltaX, 0);
264
- }
332
+ if (this.maxScrollLeft > 0) {
333
+ this.scrollLeft = this.scrollLeft + deltaX;
265
334
  }
266
335
 
267
- if (this.mode !== Mode.FIXED) {
268
- this.scrollTo(this.scrollLeft, this.scrollTop);
269
- }
336
+ this.fixScrollValue();
270
337
 
271
- if (this.pageScrollParent) {
272
- this.pageScrollParent.scrollTo({
273
- top: this.scrollTop,
274
- left: this.scrollLeft,
275
- });
276
- }
277
338
  this.scroll();
278
339
 
279
340
  this.emit('scroll', event);
280
341
  };
342
+
343
+ private mouseLeaveHandler = () => {
344
+ setTimeout(() => this.emit('clearHighlight'), throttleTime);
345
+ };
281
346
  }
@@ -0,0 +1,135 @@
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 StageDragResize from './StageDragResize';
24
+ import StageMask from './StageMask';
25
+ import type { Offset, TargetCalibrateConfig } from './types';
26
+ import { getMode } from './util';
27
+
28
+ /**
29
+ * 将选中的节点修正定位后,添加一个操作节点到蒙层上
30
+ */
31
+ export default class TargetCalibrate extends EventEmitter {
32
+ public parent: HTMLElement;
33
+ public mask: StageMask;
34
+ public dr: StageDragResize;
35
+ public operationEl: HTMLElement;
36
+
37
+ constructor(config: TargetCalibrateConfig) {
38
+ super();
39
+
40
+ this.parent = config.parent;
41
+ this.mask = config.mask;
42
+ this.dr = config.dr;
43
+
44
+ this.operationEl = globalThis.document.createElement('div');
45
+ this.parent.append(this.operationEl);
46
+ }
47
+
48
+ public update(el: HTMLElement, prefix: String): HTMLElement {
49
+ const { width, height } = el.getBoundingClientRect();
50
+ const { left, top } = this.getOffset(el);
51
+ this.operationEl.style.cssText = `
52
+ position: absolute;
53
+ left: ${left}px;
54
+ top: ${top}px;
55
+ width: ${width}px;
56
+ height: ${height}px;
57
+ `;
58
+
59
+ this.operationEl.id = `${prefix}${el.id}`;
60
+ return this.operationEl;
61
+ }
62
+
63
+ public destroy(): void {
64
+ this.operationEl?.remove();
65
+ }
66
+
67
+ private getOffset(el: HTMLElement): Offset {
68
+ const { transform } = getComputedStyle(el);
69
+ const { offsetParent } = el;
70
+
71
+ let left = el.offsetLeft;
72
+ let top = el.offsetTop;
73
+
74
+ if (transform.indexOf('matrix') > -1) {
75
+ let a = 1;
76
+ let b = 1;
77
+ let c = 1;
78
+ let d = 1;
79
+ let e = 0;
80
+ let f = 0;
81
+ transform.replace(
82
+ /matrix\((.+), (.+), (.+), (.+), (.+), (.+)\)/,
83
+ ($0: string, $1: string, $2: string, $3: string, $4: string, $5: string, $6: string): string => {
84
+ a = +$1;
85
+ b = +$2;
86
+ c = +$3;
87
+ d = +$4;
88
+ e = +$5;
89
+ f = +$6;
90
+ return transform;
91
+ },
92
+ );
93
+
94
+ left = a * left + c * top + e;
95
+ top = b * left + d * top + f;
96
+ }
97
+
98
+ if (offsetParent) {
99
+ const parentOffset = this.getOffset(offsetParent as HTMLElement);
100
+ return {
101
+ left: left + parentOffset.left,
102
+ top: top + parentOffset.top,
103
+ };
104
+ }
105
+
106
+ // 选中固定定位元素后editor-mask高度被置为视窗大小
107
+ if (this.dr.mode === Mode.FIXED) {
108
+ // 弹窗的情况
109
+ if (getMode(el) === Mode.FIXED) {
110
+ return {
111
+ left,
112
+ top,
113
+ };
114
+ }
115
+
116
+ return {
117
+ left: left - this.mask.scrollLeft,
118
+ top: top - this.mask.scrollTop,
119
+ };
120
+ }
121
+
122
+ // 无父元素的固定定位需按滚动值计算
123
+ if (getMode(el) === Mode.FIXED) {
124
+ return {
125
+ left: left + this.mask.scrollLeft,
126
+ top: top + this.mask.scrollTop,
127
+ };
128
+ }
129
+
130
+ return {
131
+ left,
132
+ top,
133
+ };
134
+ }
135
+ }
package/src/const.ts CHANGED
@@ -19,6 +19,10 @@
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
+
24
+ export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
25
+
22
26
  // 默认放到缩小倍数
23
27
  export const DEFAULT_ZOOM = 1;
24
28
 
package/src/types.ts CHANGED
@@ -22,8 +22,10 @@ import { Id, MApp, MNode } from '@tmagic/schema';
22
22
 
23
23
  import { GuidesType } from './const';
24
24
  import StageCore from './StageCore';
25
+ import StageDragResize from './StageDragResize';
26
+ import StageMask from './StageMask';
25
27
 
26
- export type CanSelect = (el: HTMLElement, stop: () => boolean) => boolean | Promise<boolean>;
28
+ export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
27
29
 
28
30
  export type StageCoreConfig = {
29
31
  /** 需要对齐的dom节点的CSS选择器字符串 */
@@ -35,6 +37,7 @@ export type StageCoreConfig = {
35
37
  /** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
36
38
  runtimeUrl?: string;
37
39
  render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
40
+ autoScrollIntoView?: boolean;
38
41
  };
39
42
 
40
43
  export interface StageRenderConfig {
@@ -54,7 +57,6 @@ export type Rect = {
54
57
  width: number;
55
58
  height: number;
56
59
  } & Offset;
57
-
58
60
  export interface Offset {
59
61
  left: number;
60
62
  top: number;
@@ -94,7 +96,7 @@ export interface RemoveData {
94
96
 
95
97
  export interface Runtime {
96
98
  beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
97
- getSnapElements?: (el: HTMLElement) => HTMLElement[];
99
+ getSnapElements?: (el?: HTMLElement) => HTMLElement[];
98
100
  updateRootConfig: (config: MApp) => void;
99
101
  updatePageId?: (id: Id) => void;
100
102
  select?: (id: Id) => Promise<HTMLElement> | HTMLElement;
@@ -119,3 +121,9 @@ export interface StageHighlightConfig {
119
121
  core: StageCore;
120
122
  container: HTMLElement;
121
123
  }
124
+
125
+ export interface TargetCalibrateConfig {
126
+ parent: HTMLElement;
127
+ mask: StageMask;
128
+ dr: StageDragResize;
129
+ }
package/src/util.ts CHANGED
@@ -19,6 +19,16 @@
19
19
  import { Mode, SELECTED_CLASS } 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
33
  const { transform } = getComputedStyle(el);
24
34
  const { offsetParent } = el;
@@ -161,10 +171,16 @@ export const removeSelectedClassName = (doc: Document) => {
161
171
  if (oldEl) {
162
172
  oldEl.classList.remove(SELECTED_CLASS);
163
173
  (oldEl.parentNode as HTMLDivElement)?.classList.remove(`${SELECTED_CLASS}-parent`);
174
+ doc.querySelectorAll(`.${SELECTED_CLASS}-parents`).forEach((item) => {
175
+ item.classList.remove(`${SELECTED_CLASS}-parents`);
176
+ });
164
177
  }
165
178
  };
166
179
 
167
- export const addSelectedClassName = (el: Element) => {
180
+ export const addSelectedClassName = (el: Element, doc: Document) => {
168
181
  el.classList.add(SELECTED_CLASS);
169
182
  (el.parentNode as Element)?.classList.add(`${SELECTED_CLASS}-parent`);
183
+ getParents(el, doc.body).forEach((item) => {
184
+ item.classList.add(`${SELECTED_CLASS}-parents`);
185
+ });
170
186
  };
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "baseUrl": "../..",
5
- },
6
- "exclude": [
7
- "**/dist/**/*"
8
- ],
9
- }
package/vite.config.ts DELETED
@@ -1,27 +0,0 @@
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
- import { defineConfig } from 'vite';
20
-
21
- import { getBaseConfig } from '../vite-config';
22
-
23
- import pkg from './package.json';
24
-
25
- const deps = Object.keys(pkg.dependencies);
26
-
27
- export default defineConfig(getBaseConfig(deps, 'TMagicStage'));