@tmagic/stage 1.0.3 → 1.1.0-beta.0

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.
@@ -5,8 +5,6 @@ export declare const getAbsolutePosition: (el: HTMLElement, { top, left }: Offse
5
5
  left: number;
6
6
  top: number;
7
7
  };
8
- export declare const getHost: (targetUrl: string) => string | undefined;
9
- export declare const isSameDomain: (targetUrl?: string, source?: string) => boolean;
10
8
  export declare const isAbsolute: (style: CSSStyleDeclaration) => boolean;
11
9
  export declare const isRelative: (style: CSSStyleDeclaration) => boolean;
12
10
  export declare const isStatic: (style: CSSStyleDeclaration) => boolean;
@@ -14,9 +12,6 @@ export declare const isFixed: (style: CSSStyleDeclaration) => boolean;
14
12
  export declare const isFixedParent: (el: HTMLElement) => boolean;
15
13
  export declare const getMode: (el: HTMLElement) => Mode;
16
14
  export declare const getScrollParent: (element: HTMLElement, includeHidden?: boolean) => HTMLElement | null;
17
- export declare const createDiv: ({ className, cssText }: {
18
- className: string;
19
- cssText: string;
20
- }) => HTMLDivElement;
21
15
  export declare const removeSelectedClassName: (doc: Document) => void;
22
16
  export declare const addSelectedClassName: (el: Element, doc: Document) => void;
17
+ export declare const calcValueByFontsize: (doc: Document, value: number) => number;
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.3",
2
+ "version": "1.1.0-beta.0",
3
3
  "name": "@tmagic/stage",
4
4
  "sideEffects": [
5
5
  "dist/*"
@@ -27,9 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@scena/guides": "^0.17.0",
30
- "@tmagic/core": "1.0.3",
31
- "@tmagic/schema": "1.0.3",
32
- "@tmagic/utils": "1.0.3",
30
+ "@tmagic/core": "1.1.0-beta.0",
31
+ "@tmagic/schema": "1.1.0-beta.0",
32
+ "@tmagic/utils": "1.1.0-beta.0",
33
33
  "events": "^3.3.0",
34
34
  "lodash-es": "^4.17.21",
35
35
  "moveable": "^0.30.0",
package/src/StageCore.ts CHANGED
@@ -28,6 +28,7 @@ import StageRender from './StageRender';
28
28
  import {
29
29
  CanSelect,
30
30
  GuidesEventData,
31
+ IsContainer,
31
32
  RemoveData,
32
33
  Runtime,
33
34
  SortEventData,
@@ -38,16 +39,20 @@ import {
38
39
  import { addSelectedClassName, removeSelectedClassName } from './util';
39
40
 
40
41
  export default class StageCore extends EventEmitter {
42
+ public container?: HTMLDivElement;
43
+
41
44
  public selectedDom: Element | undefined;
42
45
  public highlightedDom: Element | undefined;
43
-
44
46
  public renderer: StageRender;
45
47
  public mask: StageMask;
46
48
  public dr: StageDragResize;
47
49
  public highlightLayer: StageHighlight;
48
50
  public config: StageCoreConfig;
49
51
  public zoom = DEFAULT_ZOOM;
50
- public container?: HTMLDivElement;
52
+ public containerHighlightClassName: string;
53
+ public containerHighlightDuration: number;
54
+ public isContainer: IsContainer;
55
+
51
56
  private canSelect: CanSelect;
52
57
 
53
58
  constructor(config: StageCoreConfig) {
@@ -57,6 +62,9 @@ export default class StageCore extends EventEmitter {
57
62
 
58
63
  this.setZoom(config.zoom);
59
64
  this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id);
65
+ this.isContainer = config.isContainer;
66
+ this.containerHighlightClassName = config.containerHighlightClassName;
67
+ this.containerHighlightDuration = config.containerHighlightDuration;
60
68
 
61
69
  this.renderer = new StageRender({ core: this });
62
70
  this.mask = new StageMask({ core: this });
@@ -104,7 +112,7 @@ export default class StageCore extends EventEmitter {
104
112
  });
105
113
  }
106
114
 
107
- public async setElementFromPoint(event: MouseEvent) {
115
+ public getElementsFromPoint(event: MouseEvent) {
108
116
  const { renderer, zoom } = this;
109
117
 
110
118
  const doc = renderer.contentWindow?.document;
@@ -119,7 +127,11 @@ export default class StageCore extends EventEmitter {
119
127
  }
120
128
  }
121
129
 
122
- const els = doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
130
+ return doc?.elementsFromPoint(x / zoom, y / zoom) as HTMLElement[];
131
+ }
132
+
133
+ public async setElementFromPoint(event: MouseEvent) {
134
+ const els = this.getElementsFromPoint(event);
123
135
 
124
136
  let stopped = false;
125
137
  const stop = () => (stopped = true);
@@ -23,10 +23,12 @@ import type { MoveableOptions } from 'moveable';
23
23
  import Moveable from 'moveable';
24
24
  import MoveableHelper from 'moveable-helper';
25
25
 
26
+ import { addClassName, removeClassNameByClassName } from '@tmagic/utils';
27
+
26
28
  import { DRAG_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, Mode, ZIndex } from './const';
27
29
  import StageCore from './StageCore';
28
30
  import type { SortEventData, StageDragResizeConfig } from './types';
29
- import { getAbsolutePosition, getMode, getOffset } from './util';
31
+ import { calcValueByFontsize, getAbsolutePosition, getMode, getOffset } from './util';
30
32
 
31
33
  /** 拖动状态 */
32
34
  enum ActionStatus {
@@ -179,20 +181,24 @@ export default class StageDragResize extends EventEmitter {
179
181
  this.elementGuidelines = [];
180
182
 
181
183
  if (this.mode === Mode.ABSOLUTE) {
182
- const frame = document.createDocumentFragment();
183
-
184
- for (const node of nodes) {
185
- const { width, height } = node.getBoundingClientRect();
186
- if (node === this.target) continue;
187
- const { left, top } = getOffset(node as HTMLElement);
188
- const elementGuideline = document.createElement('div');
189
- elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
190
- this.elementGuidelines.push(elementGuideline);
191
- frame.append(elementGuideline);
192
- }
184
+ this.container.append(this.createGuidelineElements(nodes));
185
+ }
186
+ }
193
187
 
194
- this.container.append(frame);
188
+ private createGuidelineElements(nodes: HTMLElement[]) {
189
+ const frame = globalThis.document.createDocumentFragment();
190
+
191
+ for (const node of nodes) {
192
+ const { width, height } = node.getBoundingClientRect();
193
+ if (node === this.target) continue;
194
+ const { left, top } = getOffset(node as HTMLElement);
195
+ const elementGuideline = globalThis.document.createElement('div');
196
+ elementGuideline.style.cssText = `position: absolute;width: ${width}px;height: ${height}px;top: ${top}px;left: ${left}px`;
197
+ this.elementGuidelines.push(elementGuideline);
198
+ frame.append(elementGuideline);
195
199
  }
200
+
201
+ return frame;
196
202
  }
197
203
 
198
204
  private initMoveable() {
@@ -260,6 +266,11 @@ export default class StageDragResize extends EventEmitter {
260
266
  top: 0,
261
267
  };
262
268
 
269
+ let timeout: NodeJS.Timeout | undefined;
270
+
271
+ const { contentWindow } = this.core.renderer;
272
+ const doc = contentWindow?.document;
273
+
263
274
  this.moveable
264
275
  .on('dragStart', (e) => {
265
276
  if (!this.target) throw new Error('未选中组件');
@@ -278,6 +289,26 @@ export default class StageDragResize extends EventEmitter {
278
289
  .on('drag', (e) => {
279
290
  if (!this.target || !this.dragEl) return;
280
291
 
292
+ if (timeout) {
293
+ globalThis.clearTimeout(timeout);
294
+ timeout = undefined;
295
+ }
296
+
297
+ timeout = globalThis.setTimeout(async () => {
298
+ const els = this.core.getElementsFromPoint(e.inputEvent);
299
+ for (const el of els) {
300
+ if (
301
+ doc &&
302
+ !el.id.startsWith(GHOST_EL_ID_PREFIX) &&
303
+ el !== this.target &&
304
+ (await this.core.isContainer(el))
305
+ ) {
306
+ addClassName(el, doc, this.core.containerHighlightClassName);
307
+ break;
308
+ }
309
+ }
310
+ }, this.core.containerHighlightDuration);
311
+
281
312
  this.dragStatus = ActionStatus.ING;
282
313
 
283
314
  // 流式布局
@@ -292,14 +323,29 @@ export default class StageDragResize extends EventEmitter {
292
323
  this.target.style.top = `${frame.top + e.beforeTranslate[1]}px`;
293
324
  })
294
325
  .on('dragEnd', () => {
326
+ if (timeout) {
327
+ globalThis.clearTimeout(timeout);
328
+ timeout = undefined;
329
+ }
330
+
331
+ let parentEl: HTMLElement | null = null;
332
+
333
+ if (doc) {
334
+ parentEl = removeClassNameByClassName(doc, this.core.containerHighlightClassName);
335
+ }
336
+
295
337
  // 点击不拖动时会触发dragStart和dragEnd,但是不会有drag事件
296
338
  if (this.dragStatus === ActionStatus.ING) {
297
- switch (this.mode) {
298
- case Mode.SORTABLE:
299
- this.sort();
300
- break;
301
- default:
302
- this.update();
339
+ if (parentEl) {
340
+ this.update(false, parentEl);
341
+ } else {
342
+ switch (this.mode) {
343
+ case Mode.SORTABLE:
344
+ this.sort();
345
+ break;
346
+ default:
347
+ this.update();
348
+ }
303
349
  }
304
350
  }
305
351
 
@@ -381,19 +427,36 @@ export default class StageDragResize extends EventEmitter {
381
427
  }
382
428
  }
383
429
 
384
- private update(isResize = false): void {
430
+ private update(isResize = false, parentEl: HTMLElement | null = null): void {
385
431
  if (!this.target) return;
386
432
 
433
+ const { contentWindow } = this.core.renderer;
434
+ const doc = contentWindow?.document;
435
+
436
+ if (!doc) return;
437
+
387
438
  const offset =
388
439
  this.mode === Mode.SORTABLE ? { left: 0, top: 0 } : { left: this.target.offsetLeft, top: this.target.offsetTop };
389
440
 
390
- const left = this.calcValueByFontsize(offset.left);
391
- const top = this.calcValueByFontsize(offset.top);
392
- const width = this.calcValueByFontsize(this.target.clientWidth);
393
- const height = this.calcValueByFontsize(this.target.clientHeight);
441
+ let left = calcValueByFontsize(doc, offset.left);
442
+ let top = calcValueByFontsize(doc, offset.top);
443
+ const width = calcValueByFontsize(doc, this.target.clientWidth);
444
+ const height = calcValueByFontsize(doc, this.target.clientHeight);
445
+
446
+ if (parentEl && this.mode === Mode.ABSOLUTE && this.dragEl) {
447
+ const [translateX, translateY] = this.moveableHelper?.getFrame(this.dragEl).properties.transform.translate.value;
448
+ const { left: parentLeft, top: parentTop } = getOffset(parentEl);
449
+ left =
450
+ calcValueByFontsize(doc, this.dragEl.offsetLeft) +
451
+ parseFloat(translateX) -
452
+ calcValueByFontsize(doc, parentLeft);
453
+ top =
454
+ calcValueByFontsize(doc, this.dragEl.offsetTop) + parseFloat(translateY) - calcValueByFontsize(doc, parentTop);
455
+ }
394
456
 
395
457
  this.emit('update', {
396
458
  el: this.target,
459
+ parentEl,
397
460
  style: isResize ? { left, top, width, height } : { left, top },
398
461
  });
399
462
  }
@@ -511,18 +574,6 @@ export default class StageDragResize extends EventEmitter {
511
574
  ...moveableOptions,
512
575
  };
513
576
  }
514
-
515
- private calcValueByFontsize(value: number) {
516
- const { contentWindow } = this.core.renderer;
517
- const fontSize = contentWindow?.document.documentElement.style.fontSize;
518
-
519
- if (fontSize) {
520
- const times = globalThis.parseFloat(fontSize) / 100;
521
- return (value / times).toFixed(2);
522
- }
523
-
524
- return value;
525
- }
526
577
  }
527
578
 
528
579
  /**
@@ -16,7 +16,6 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- /* eslint-disable no-param-reassign */
20
19
  import { EventEmitter } from 'events';
21
20
 
22
21
  import Moveable from 'moveable';
package/src/StageMask.ts CHANGED
@@ -18,21 +18,19 @@
18
18
 
19
19
  import { throttle } from 'lodash-es';
20
20
 
21
+ import { createDiv, injectStyle } from '@tmagic/utils';
22
+
21
23
  import { Mode, MouseButton, ZIndex } from './const';
22
24
  import Rule from './Rule';
23
25
  import type StageCore from './StageCore';
24
26
  import type { StageMaskConfig } from './types';
25
- import { createDiv, getScrollParent, isFixedParent } from './util';
27
+ import { getScrollParent, isFixedParent } from './util';
26
28
 
27
29
  const wrapperClassName = 'editor-mask-wrapper';
28
30
  const throttleTime = 100;
29
31
 
30
32
  const hideScrollbar = () => {
31
- const style = globalThis.document.createElement('style');
32
- style.innerHTML = `
33
- .${wrapperClassName}::-webkit-scrollbar { width: 0 !important; display: none }
34
- `;
35
- globalThis.document.head.appendChild(style);
33
+ injectStyle(globalThis.document, `.${wrapperClassName}::-webkit-scrollbar { width: 0 !important; display: none }`);
36
34
  };
37
35
 
38
36
  const createContent = (): HTMLDivElement =>
@@ -18,9 +18,11 @@
18
18
 
19
19
  import { EventEmitter } from 'events';
20
20
 
21
+ import { getHost, injectStyle, isSameDomain } from '@tmagic/utils';
22
+
21
23
  import StageCore from './StageCore';
24
+ import style from './style.css?raw';
22
25
  import type { Runtime, RuntimeWindow, StageRenderConfig } from './types';
23
- import { getHost, isSameDomain } from './util';
24
26
 
25
27
  export default class StageRender extends EventEmitter {
26
28
  /** 组件的js、css执行的环境,直接渲染为当前window,iframe渲染则为iframe.contentWindow */
@@ -128,5 +130,7 @@ export default class StageRender extends EventEmitter {
128
130
  },
129
131
  '*',
130
132
  );
133
+
134
+ injectStyle(this.contentWindow.document, style);
131
135
  };
132
136
  }
@@ -16,7 +16,6 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- /* eslint-disable no-param-reassign */
20
19
  import { EventEmitter } from 'events';
21
20
 
22
21
  import { Mode } from './const';
package/src/const.ts CHANGED
@@ -25,6 +25,8 @@ export const DRAG_EL_ID_PREFIX = 'drag_el_';
25
25
  /** 高亮时需要在蒙层中创建一个占位节点,该节点的id前缀 */
26
26
  export const HIGHLIGHT_EL_ID_PREFIX = 'highlight_el_';
27
27
 
28
+ export const CONTAINER_HIGHLIGHT_CLASS = 'tmagic-stage-container-highlight';
29
+
28
30
  /** 默认放到缩小倍数 */
29
31
  export const DEFAULT_ZOOM = 1;
30
32
 
package/src/index.ts CHANGED
@@ -24,4 +24,5 @@ export { default as StageMask } from './StageMask';
24
24
  export { default as StageDragResize } from './StageDragResize';
25
25
  export * from './types';
26
26
  export * from './const';
27
+ export * from './util';
27
28
  export default StageCore;
package/src/style.css ADDED
@@ -0,0 +1,10 @@
1
+ .tmagic-stage-container-highlight::after {
2
+ content: '';
3
+ position: absolute;
4
+ width: 100%;
5
+ height: 100%;
6
+ top: 0;
7
+ left: 0;
8
+ background-color: #000;
9
+ opacity: .1;
10
+ }
package/src/types.ts CHANGED
@@ -27,6 +27,7 @@ import StageDragResize from './StageDragResize';
27
27
  import StageMask from './StageMask';
28
28
 
29
29
  export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
30
+ export type IsContainer = (el: HTMLElement) => boolean | Promise<boolean>;
30
31
 
31
32
  export type StageCoreConfig = {
32
33
  /** 需要对齐的dom节点的CSS选择器字符串 */
@@ -34,6 +35,9 @@ export type StageCoreConfig = {
34
35
  /** 放大倍数,默认1倍 */
35
36
  zoom?: number;
36
37
  canSelect?: CanSelect;
38
+ isContainer: IsContainer;
39
+ containerHighlightClassName: string;
40
+ containerHighlightDuration: number;
37
41
  moveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
38
42
  /** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
39
43
  runtimeUrl?: string;
@@ -72,6 +76,7 @@ export interface GuidesEventData {
72
76
 
73
77
  export interface UpdateEventData {
74
78
  el: HTMLElement;
79
+ parentEl: HTMLElement | null;
75
80
  ghostEl: HTMLElement;
76
81
  style: {
77
82
  width?: number;
package/src/util.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
+ import { removeClassName } from '@tmagic/utils';
18
19
 
19
20
  import { Mode, SELECTED_CLASS } from './const';
20
21
  import type { Offset } from './types';
@@ -63,16 +64,6 @@ export const getAbsolutePosition = (el: HTMLElement, { top, left }: Offset) => {
63
64
  return { left, top };
64
65
  };
65
66
 
66
- export const getHost = (targetUrl: string) => targetUrl.match(/\/\/([^/]+)/)?.[1];
67
-
68
- export const isSameDomain = (targetUrl = '', source = globalThis.location.host) => {
69
- const isHttpUrl = /^(http[s]?:)?\/\//.test(targetUrl);
70
-
71
- if (!isHttpUrl) return true;
72
-
73
- return getHost(targetUrl) === source;
74
- };
75
-
76
67
  export const isAbsolute = (style: CSSStyleDeclaration): boolean => style.position === 'absolute';
77
68
 
78
69
  export const isRelative = (style: CSSStyleDeclaration): boolean => style.position === 'relative';
@@ -123,21 +114,14 @@ export const getScrollParent = (element: HTMLElement, includeHidden = false): HT
123
114
  return null;
124
115
  };
125
116
 
126
- export const createDiv = ({ className, cssText }: { className: string; cssText: string }) => {
127
- const el = globalThis.document.createElement('div');
128
- el.className = className;
129
- el.style.cssText = cssText;
130
- return el;
131
- };
132
-
133
117
  export const removeSelectedClassName = (doc: Document) => {
134
118
  const oldEl = doc.querySelector(`.${SELECTED_CLASS}`);
135
119
 
136
120
  if (oldEl) {
137
- oldEl.classList.remove(SELECTED_CLASS);
138
- (oldEl.parentNode as HTMLDivElement)?.classList.remove(`${SELECTED_CLASS}-parent`);
121
+ removeClassName(oldEl, SELECTED_CLASS);
122
+ if (oldEl.parentNode) removeClassName(oldEl.parentNode as Element, `${SELECTED_CLASS}-parent`);
139
123
  doc.querySelectorAll(`.${SELECTED_CLASS}-parents`).forEach((item) => {
140
- item.classList.remove(`${SELECTED_CLASS}-parents`);
124
+ removeClassName(item, `${SELECTED_CLASS}-parents`);
141
125
  });
142
126
  }
143
127
  };
@@ -149,3 +133,14 @@ export const addSelectedClassName = (el: Element, doc: Document) => {
149
133
  item.classList.add(`${SELECTED_CLASS}-parents`);
150
134
  });
151
135
  };
136
+
137
+ export const calcValueByFontsize = (doc: Document, value: number) => {
138
+ const { fontSize } = doc.documentElement.style;
139
+
140
+ if (fontSize) {
141
+ const times = globalThis.parseFloat(fontSize) / 100;
142
+ return Number((value / times).toFixed(2));
143
+ }
144
+
145
+ return value;
146
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />