@tmagic/stage 1.0.0-rc.1 → 1.0.0-rc.4

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.
@@ -1,4 +1,5 @@
1
1
  import { MoveableOptions } from 'react-moveable/declaration/types';
2
+ import Core from '@tmagic/core';
2
3
  import { Id, MApp, MNode } from '@tmagic/schema';
3
4
  import { GuidesType } from './const';
4
5
  import StageCore from './StageCore';
@@ -15,6 +16,7 @@ export declare type StageCoreConfig = {
15
16
  /** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
16
17
  runtimeUrl?: string;
17
18
  render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
19
+ autoScrollIntoView?: boolean;
18
20
  };
19
21
  export interface StageRenderConfig {
20
22
  core: StageCore;
@@ -62,6 +64,7 @@ export interface RemoveData {
62
64
  root: MApp;
63
65
  }
64
66
  export interface Runtime {
67
+ getApp?: () => Core;
65
68
  beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
66
69
  getSnapElements?: (el?: HTMLElement) => HTMLElement[];
67
70
  updateRootConfig: (config: MApp) => void;
@@ -1,4 +1,4 @@
1
- import { Mode } from './const';
1
+ import { GuidesType, Mode } from './const';
2
2
  import type { Offset } from './types';
3
3
  export declare const getOffset: (el: HTMLElement) => Offset;
4
4
  export declare const getAbsolutePosition: (el: HTMLElement, { top, left }: Offset) => {
@@ -19,4 +19,5 @@ export declare const createDiv: ({ className, cssText }: {
19
19
  cssText: string;
20
20
  }) => HTMLDivElement;
21
21
  export declare const removeSelectedClassName: (doc: Document) => void;
22
- export declare const addSelectedClassName: (el: Element) => void;
22
+ export declare const addSelectedClassName: (el: Element, doc: Document) => void;
23
+ export declare const getGuideLineFromCache: (type: GuidesType) => number[];
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.1",
2
+ "version": "1.0.0-rc.4",
3
3
  "name": "@tmagic/stage",
4
4
  "sideEffects": [
5
5
  "dist/*"
@@ -27,8 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@scena/guides": "^0.17.0",
30
- "@tmagic/schema": "^1.0.0-rc.1",
31
- "@tmagic/utils": "^1.0.0-rc.1",
30
+ "@tmagic/core": "^1.0.0-rc.4",
31
+ "@tmagic/schema": "^1.0.0-rc.4",
32
+ "@tmagic/utils": "^1.0.0-rc.4",
32
33
  "events": "^3.3.0",
33
34
  "lodash-es": "^4.17.21",
34
35
  "moveable": "^0.29.4",
@@ -43,5 +44,5 @@
43
44
  "vite": "^2.3.7",
44
45
  "vite-plugin-dts": "^0.9.6"
45
46
  },
46
- "gitHead": "3b647794d087888bfec9772f54356fa48c0b6d90"
47
+ "gitHead": "28d5ad4573a27fa240f5e7d0902d3368e8d936b6"
47
48
  }
package/src/Rule.ts CHANGED
@@ -2,13 +2,14 @@ import EventEmitter from 'events';
2
2
 
3
3
  import Guides, { GuidesEvents } from '@scena/guides';
4
4
 
5
- import { GuidesType } from './const';
5
+ import { GuidesType, H_GUIDE_LINE_STORAGE_KEY, V_GUIDE_LINE_STORAGE_KEY } from './const';
6
+ import { getGuideLineFromCache } from './util';
6
7
 
7
8
  export default class Rule extends EventEmitter {
8
9
  public hGuides: Guides;
9
10
  public vGuides: Guides;
10
- public horizontalGuidelines: number[] = [];
11
- public verticalGuidelines: number[] = [];
11
+ public horizontalGuidelines: number[] = getGuideLineFromCache(GuidesType.HORIZONTAL);
12
+ public verticalGuidelines: number[] = getGuideLineFromCache(GuidesType.VERTICAL);
12
13
 
13
14
  private container: HTMLDivElement;
14
15
  private containerResizeObserver: ResizeObserver;
@@ -17,8 +18,8 @@ export default class Rule extends EventEmitter {
17
18
  super();
18
19
 
19
20
  this.container = container;
20
- this.hGuides = this.createGuides(GuidesType.HORIZONTAL);
21
- this.vGuides = this.createGuides(GuidesType.VERTICAL);
21
+ this.hGuides = this.createGuides(GuidesType.HORIZONTAL, this.horizontalGuidelines);
22
+ this.vGuides = this.createGuides(GuidesType.VERTICAL, this.verticalGuidelines);
22
23
 
23
24
  this.hGuides.on('changeGuides', this.hGuidesChangeGuidesHandler);
24
25
  this.vGuides.on('changeGuides', this.vGuidesChangeGuidesHandler);
@@ -68,6 +69,9 @@ export default class Rule extends EventEmitter {
68
69
  type: GuidesType.HORIZONTAL,
69
70
  guides: [],
70
71
  });
72
+
73
+ globalThis.localStorage.setItem(V_GUIDE_LINE_STORAGE_KEY, '[]');
74
+ globalThis.localStorage.setItem(H_GUIDE_LINE_STORAGE_KEY, '[]');
71
75
  }
72
76
 
73
77
  /**
@@ -138,6 +142,8 @@ export default class Rule extends EventEmitter {
138
142
  type: GuidesType.HORIZONTAL,
139
143
  guides: this.horizontalGuidelines,
140
144
  });
145
+
146
+ globalThis.localStorage.setItem(H_GUIDE_LINE_STORAGE_KEY, JSON.stringify(e.guides));
141
147
  };
142
148
 
143
149
  private vGuidesChangeGuidesHandler = (e: GuidesEvents['changeGuides']) => {
@@ -146,5 +152,7 @@ export default class Rule extends EventEmitter {
146
152
  type: GuidesType.VERTICAL,
147
153
  guides: this.verticalGuidelines,
148
154
  });
155
+
156
+ globalThis.localStorage.setItem(V_GUIDE_LINE_STORAGE_KEY, JSON.stringify(e.guides));
149
157
  };
150
158
  }
package/src/StageCore.ts CHANGED
@@ -155,12 +155,17 @@ export default class StageCore extends EventEmitter {
155
155
 
156
156
  this.mask.setLayout(el);
157
157
  this.dr.select(el, event);
158
+
159
+ if (this.config.autoScrollIntoView || el.dataset.autoScrollIntoView) {
160
+ this.mask.intersectionObserver?.observe(el);
161
+ }
162
+
158
163
  this.selectedDom = el;
159
164
 
160
165
  if (this.renderer.contentWindow) {
161
166
  removeSelectedClassName(this.renderer.contentWindow.document);
162
167
  if (this.selectedDom) {
163
- addSelectedClassName(this.selectedDom);
168
+ addSelectedClassName(this.selectedDom, this.renderer.contentWindow.document);
164
169
  }
165
170
  }
166
171
  }
@@ -25,12 +25,16 @@ import MoveableHelper from 'moveable-helper';
25
25
 
26
26
  import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode } from './const';
27
27
  import StageCore from './StageCore';
28
- import type { Offset, Runtime, SortEventData, StageDragResizeConfig } from './types';
29
- import { getAbsolutePosition, getMode, getOffset } from './util';
28
+ import type { Offset, SortEventData, StageDragResizeConfig } from './types';
29
+ import { getAbsolutePosition, getGuideLineFromCache, getMode, getOffset } from './util';
30
30
 
31
+ /** 拖动状态 */
31
32
  enum ActionStatus {
33
+ /** 开始拖动 */
32
34
  START = 'start',
35
+ /** 拖动中 */
33
36
  ING = 'ing',
37
+ /** 拖动结束 */
34
38
  END = 'end',
35
39
  }
36
40
 
@@ -39,17 +43,27 @@ enum ActionStatus {
39
43
  */
40
44
  export default class StageDragResize extends EventEmitter {
41
45
  public core: StageCore;
46
+ /** 画布容器 */
42
47
  public container: HTMLElement;
48
+ /** 目标节点 */
43
49
  public target?: HTMLElement;
50
+ /** 目标节点在蒙层中的占位节点 */
44
51
  public dragEl: HTMLElement;
52
+ /** Moveable拖拽类实例 */
45
53
  public moveable?: Moveable;
46
- public horizontalGuidelines: number[] = [];
47
- public verticalGuidelines: number[] = [];
54
+ /** 水平参考线 */
55
+ public horizontalGuidelines: number[] = getGuideLineFromCache(GuidesType.HORIZONTAL);
56
+ /** 垂直参考线 */
57
+ public verticalGuidelines: number[] = getGuideLineFromCache(GuidesType.VERTICAL);
58
+ /** 对齐元素集合 */
48
59
  public elementGuidelines: HTMLElement[] = [];
60
+ /** 布局方式:流式布局、绝对定位、固定定位 */
49
61
  public mode: Mode = Mode.ABSOLUTE;
50
62
 
51
63
  private moveableOptions: MoveableOptions = {};
64
+ /** 拖动状态 */
52
65
  private dragStatus: ActionStatus = ActionStatus.END;
66
+ /** 流式布局下,目标节点的镜像节点 */
53
67
  private ghostEl: HTMLElement | undefined;
54
68
  private moveableHelper?: MoveableHelper;
55
69
 
@@ -152,11 +166,38 @@ export default class StageDragResize extends EventEmitter {
152
166
 
153
167
  this.updateDragEl(el);
154
168
 
169
+ this.setElementGuidelines(el);
170
+
155
171
  this.moveableOptions = this.getOptions({
156
172
  target: this.dragEl,
157
173
  });
158
174
  }
159
175
 
176
+ private setElementGuidelines(el: HTMLElement) {
177
+ const nodes = el.parentElement?.children || [];
178
+
179
+ this.elementGuidelines.forEach((node) => {
180
+ node.remove();
181
+ });
182
+ this.elementGuidelines = [];
183
+
184
+ if (this.mode === Mode.ABSOLUTE) {
185
+ const frame = document.createDocumentFragment();
186
+
187
+ for (const node of nodes) {
188
+ const { width, height } = node.getBoundingClientRect();
189
+ if (node === el) continue;
190
+ const { left, top } = getOffset(node as HTMLElement);
191
+ const elementGuideline = document.createElement('div');
192
+ elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
193
+ this.elementGuidelines.push(elementGuideline);
194
+ frame.append(elementGuideline);
195
+ }
196
+
197
+ this.container.append(frame);
198
+ }
199
+ }
200
+
160
201
  private initMoveable() {
161
202
  this.moveable?.destroy();
162
203
 
@@ -274,19 +315,6 @@ export default class StageDragResize extends EventEmitter {
274
315
  });
275
316
  }
276
317
 
277
- private getSnapElements(runtime: Runtime, el?: HTMLElement): HTMLElement[] {
278
- const { renderer, mask } = this.core;
279
- const getSnapElements =
280
- runtime?.getSnapElements ||
281
- (() => {
282
- const doc = renderer.contentWindow?.document;
283
- return doc ? Array.from(doc.querySelectorAll('[id]')) : [];
284
- });
285
- return getSnapElements(el).filter(
286
- (element) => element !== this.target && !this.target?.contains(element) && element !== mask.page,
287
- );
288
- }
289
-
290
318
  private sort(): void {
291
319
  if (!this.target || !this.ghostEl) throw new Error('未知错误');
292
320
  const { top } = this.ghostEl.getBoundingClientRect();
package/src/StageMask.ts CHANGED
@@ -83,6 +83,7 @@ export default class StageMask extends Rule {
83
83
  public wrapperWidth = 0;
84
84
  public maxScrollTop = 0;
85
85
  public maxScrollLeft = 0;
86
+ public intersectionObserver: IntersectionObserver | null = null;
86
87
 
87
88
  private mode: Mode = Mode.ABSOLUTE;
88
89
  private pageResizeObserver: ResizeObserver | null = null;
@@ -132,6 +133,27 @@ export default class StageMask extends Rule {
132
133
  this.page = page;
133
134
  this.pageScrollParent = getScrollParent(page) || this.core.renderer.contentWindow?.document.documentElement || null;
134
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
+ }
135
157
 
136
158
  if (typeof ResizeObserver !== 'undefined') {
137
159
  this.pageResizeObserver = new ResizeObserver((entries) => {
@@ -140,9 +162,10 @@ export default class StageMask extends Rule {
140
162
  this.setHeight(clientHeight);
141
163
  this.setWidth(clientWidth);
142
164
 
143
- this.fixScrollValue();
144
165
  this.scroll();
145
- this.core.dr.updateMoveable();
166
+ if (this.core.dr.moveable) {
167
+ this.core.dr.updateMoveable();
168
+ }
146
169
  });
147
170
 
148
171
  this.pageResizeObserver.observe(page);
@@ -173,6 +196,14 @@ export default class StageMask extends Rule {
173
196
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
174
197
  }
175
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
+
176
207
  /**
177
208
  * 销毁实例
178
209
  */
@@ -188,6 +219,8 @@ export default class StageMask extends Rule {
188
219
  }
189
220
 
190
221
  private scroll() {
222
+ this.fixScrollValue();
223
+
191
224
  let { scrollLeft, scrollTop } = this;
192
225
 
193
226
  if (this.pageScrollParent) {
@@ -234,14 +267,14 @@ export default class StageMask extends Rule {
234
267
  * 计算并设置最大滚动宽度
235
268
  */
236
269
  private setMaxScrollLeft(): void {
237
- this.maxScrollLeft = this.width - this.wrapperWidth;
270
+ this.maxScrollLeft = Math.max(this.width - this.wrapperWidth, 0);
238
271
  }
239
272
 
240
273
  /**
241
274
  * 计算并设置最大滚动高度
242
275
  */
243
276
  private setMaxScrollTop(): void {
244
- this.maxScrollTop = this.height - this.wrapperHeight;
277
+ this.maxScrollTop = Math.max(this.height - this.wrapperHeight, 0);
245
278
  }
246
279
 
247
280
  /**
@@ -303,8 +336,6 @@ export default class StageMask extends Rule {
303
336
  this.scrollLeft = this.scrollLeft + deltaX;
304
337
  }
305
338
 
306
- this.fixScrollValue();
307
-
308
339
  this.scroll();
309
340
 
310
341
  this.emit('scroll', event);
package/src/const.ts CHANGED
@@ -16,36 +16,56 @@
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
+ /** 拖动的时候需要在蒙层中创建一个占位节点,该节点的id前缀 */
22
23
  export const DRAG_EL_ID_PREFIX = 'drag_el_';
23
24
 
25
+ /** 高亮事需要在蒙层中创建一个占位节点,该节点的id前缀 */
24
26
  export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
25
27
 
26
- // 默认放到缩小倍数
28
+ /** 默认放到缩小倍数 */
27
29
  export const DEFAULT_ZOOM = 1;
28
30
 
31
+ /** 参考线类型 */
29
32
  export enum GuidesType {
33
+ /** 水平 */
30
34
  HORIZONTAL = 'horizontal',
35
+ /** 垂直 */
31
36
  VERTICAL = 'vertical',
32
37
  }
33
38
 
39
+ /** css z-index */
34
40
  export enum ZIndex {
41
+ /** 蒙层,用于监听用户操作,需要置于顶层 */
35
42
  MASK = '99999',
43
+ /** 选中的节点 */
36
44
  SELECTED_EL = '666',
37
45
  }
38
46
 
47
+ /** 鼠标按键 */
39
48
  export enum MouseButton {
49
+ /** 左键 */
40
50
  LEFT = 0,
51
+ /** z中健 */
41
52
  MIDDLE = 1,
53
+ /** 右键 */
42
54
  RIGHT = 2,
43
55
  }
44
56
 
57
+ /** 布局方式 */
45
58
  export enum Mode {
59
+ /** 绝对定位布局 */
46
60
  ABSOLUTE = 'absolute',
61
+ /** 固定定位布局 */
47
62
  FIXED = 'fixed',
63
+ /** 流式布局 */
48
64
  SORTABLE = 'sortable',
49
65
  }
50
66
 
67
+ /** 选中节点的class name */
51
68
  export const SELECTED_CLASS = 'tmagic-stage-selected-area';
69
+
70
+ export const H_GUIDE_LINE_STORAGE_KEY = '$MagicStageHorizontalGuidelinesData';
71
+ export const V_GUIDE_LINE_STORAGE_KEY = '$MagicStageVerticalGuidelinesData';
package/src/types.ts CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { MoveableOptions } from 'react-moveable/declaration/types';
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';
@@ -37,6 +38,7 @@ export type StageCoreConfig = {
37
38
  /** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
38
39
  runtimeUrl?: string;
39
40
  render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
41
+ autoScrollIntoView?: boolean;
40
42
  };
41
43
 
42
44
  export interface StageRenderConfig {
@@ -94,6 +96,7 @@ export interface RemoveData {
94
96
  }
95
97
 
96
98
  export interface Runtime {
99
+ getApp?: () => Core;
97
100
  beforeSelect?: (el: HTMLElement) => Promise<boolean> | boolean;
98
101
  getSnapElements?: (el?: HTMLElement) => HTMLElement[];
99
102
  updateRootConfig: (config: MApp) => void;
package/src/util.ts CHANGED
@@ -16,9 +16,19 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { Mode, SELECTED_CLASS } 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
33
  const { transform } = getComputedStyle(el);
24
34
  const { offsetParent } = el;
@@ -161,10 +171,36 @@ 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
+ });
186
+ };
187
+
188
+ export const getGuideLineFromCache = (type: GuidesType): number[] => {
189
+ const key = {
190
+ [GuidesType.HORIZONTAL]: H_GUIDE_LINE_STORAGE_KEY,
191
+ [GuidesType.VERTICAL]: V_GUIDE_LINE_STORAGE_KEY,
192
+ }[type];
193
+
194
+ if (!key) return [];
195
+
196
+ const guideLineCacheData = globalThis.localStorage.getItem(key);
197
+ if (guideLineCacheData) {
198
+ try {
199
+ return JSON.parse(guideLineCacheData) || [];
200
+ } catch (e) {
201
+ console.error(e);
202
+ }
203
+ }
204
+
205
+ return [];
170
206
  };