@tmagic/stage 1.8.0-manmanyu.8 → 1.8.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.
- package/dist/es/ActionManager.js +14 -6
- package/dist/es/MoveableOptionsManager.js +4 -2
- package/dist/es/StageCore.js +14 -2
- package/dist/es/StageDragResize.js +12 -9
- package/dist/es/StageFlashHighlight.js +1 -1
- package/dist/es/StageMask.js +39 -9
- package/dist/es/index.js +2 -2
- package/dist/es/util.js +28 -1
- package/dist/tmagic-stage.umd.cjs +264 -149
- package/package.json +5 -5
- package/src/ActionManager.ts +20 -8
- package/src/StageCore.ts +23 -2
- package/src/StageMask.ts +55 -10
- package/src/types.ts +14 -1
- package/src/util.ts +43 -1
- package/types/index.d.ts +124 -97
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0
|
|
2
|
+
"version": "1.8.0",
|
|
3
3
|
"name": "@tmagic/stage",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -30,21 +30,21 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@scena/guides": "^0.29.2",
|
|
33
|
+
"@zumer/snapdom": "^2.23.2",
|
|
33
34
|
"events": "^3.3.0",
|
|
34
|
-
"@zumer/snapdom": "^2.8.0",
|
|
35
35
|
"keycon": "^1.4.0",
|
|
36
|
-
"lodash-es": "^4.
|
|
36
|
+
"lodash-es": "^4.18.1",
|
|
37
37
|
"moveable": "^0.53.0",
|
|
38
38
|
"moveable-helper": "^0.4.0",
|
|
39
39
|
"scenejs": "^1.10.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/events": "^3.0.3",
|
|
43
|
-
"@types/lodash-es": "^4.17.
|
|
43
|
+
"@types/lodash-es": "^4.17.12"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
|
-
"@tmagic/core": "1.8.0
|
|
47
|
+
"@tmagic/core": "1.8.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"typescript": {
|
package/src/ActionManager.ts
CHANGED
|
@@ -84,6 +84,8 @@ export default class ActionManager extends EventEmitter {
|
|
|
84
84
|
private containerHighlightDuration: number;
|
|
85
85
|
/** 将组件加入容器的操作方式 */
|
|
86
86
|
private containerHighlightType?: ContainerHighlightType;
|
|
87
|
+
/** 是否仅在新增组件时才启用将组件加入容器 */
|
|
88
|
+
private containerHighlightAddOnly = false;
|
|
87
89
|
private isAltKeydown = false;
|
|
88
90
|
private getTargetElement: GetTargetElement;
|
|
89
91
|
private getElementsFromPoint: GetElementsFromPoint;
|
|
@@ -124,6 +126,7 @@ export default class ActionManager extends EventEmitter {
|
|
|
124
126
|
this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME;
|
|
125
127
|
this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration;
|
|
126
128
|
this.containerHighlightType = config.containerHighlightType;
|
|
129
|
+
this.containerHighlightAddOnly = config.containerHighlightAddOnly ?? false;
|
|
127
130
|
this.disabledMultiSelect = config.disabledMultiSelect ?? false;
|
|
128
131
|
this.alwaysMultiSelect = config.alwaysMultiSelect ?? false;
|
|
129
132
|
this.getTargetElement = config.getTargetElement;
|
|
@@ -428,10 +431,15 @@ export default class ActionManager extends EventEmitter {
|
|
|
428
431
|
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
|
429
432
|
* @param event 鼠标事件
|
|
430
433
|
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
|
434
|
+
* @param isAdd 当前操作是否为新增组件(从组件列表拖入),开启 containerHighlightAddOnly 时只有新增才会标记
|
|
431
435
|
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
|
432
436
|
*/
|
|
433
|
-
public delayedMarkContainer(
|
|
434
|
-
|
|
437
|
+
public delayedMarkContainer(
|
|
438
|
+
event: MouseEvent,
|
|
439
|
+
excludeElList: Element[] = [],
|
|
440
|
+
isAdd = false,
|
|
441
|
+
): NodeJS.Timeout | undefined {
|
|
442
|
+
if (this.canAddToContainer(isAdd)) {
|
|
435
443
|
return globalThis.setTimeout(() => {
|
|
436
444
|
this.addContainerHighlightClassName(event, excludeElList);
|
|
437
445
|
}, this.containerHighlightDuration);
|
|
@@ -609,9 +617,13 @@ export default class ActionManager extends EventEmitter {
|
|
|
609
617
|
}
|
|
610
618
|
|
|
611
619
|
/**
|
|
612
|
-
* 当前状态下能否将组件加入容器,默认是鼠标悬停一段时间加入,alt模式则是按住alt
|
|
620
|
+
* 当前状态下能否将组件加入容器,默认是鼠标悬停一段时间加入,alt模式则是按住alt+鼠标悬停一段时间加入;
|
|
621
|
+
* 开启containerHighlightAddOnly时,画布中拖动已有组件不能加入容器
|
|
622
|
+
* @param isAdd 当前操作是否为新增组件
|
|
613
623
|
*/
|
|
614
|
-
private canAddToContainer(): boolean {
|
|
624
|
+
private canAddToContainer(isAdd = false): boolean {
|
|
625
|
+
if (this.containerHighlightAddOnly && !isAdd) return false;
|
|
626
|
+
|
|
615
627
|
return (
|
|
616
628
|
this.containerHighlightType === ContainerHighlightType.DEFAULT ||
|
|
617
629
|
(this.containerHighlightType === ContainerHighlightType.ALT && this.isAltKeydown)
|
|
@@ -624,10 +636,10 @@ export default class ActionManager extends EventEmitter {
|
|
|
624
636
|
*/
|
|
625
637
|
private markContainerEnd(): HTMLElement | null {
|
|
626
638
|
const doc = this.getRenderDocument();
|
|
627
|
-
if (doc
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return null;
|
|
639
|
+
if (!doc) return null;
|
|
640
|
+
|
|
641
|
+
const markedContainer = removeClassNameByClassName(doc, this.containerHighlightClassName);
|
|
642
|
+
return this.canAddToContainer() ? markedContainer : null;
|
|
631
643
|
}
|
|
632
644
|
|
|
633
645
|
private initMouseEvent(): void {
|
package/src/StageCore.ts
CHANGED
|
@@ -263,10 +263,15 @@ export default class StageCore extends EventEmitter {
|
|
|
263
263
|
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
|
264
264
|
* @param event 鼠标事件
|
|
265
265
|
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
|
266
|
+
* @param isAdd 当前操作是否为新增组件(从组件列表拖入),开启 containerHighlightAddOnly 时只有新增才会标记
|
|
266
267
|
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
|
267
268
|
*/
|
|
268
|
-
public delayedMarkContainer(
|
|
269
|
-
|
|
269
|
+
public delayedMarkContainer(
|
|
270
|
+
event: MouseEvent,
|
|
271
|
+
excludeElList: Element[] = [],
|
|
272
|
+
isAdd = false,
|
|
273
|
+
): NodeJS.Timeout | undefined {
|
|
274
|
+
return this.actionManager?.delayedMarkContainer(event, excludeElList, isAdd);
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
public getMoveableOption<K extends keyof MoveableOptions>(key: K): MoveableOptions[K] | undefined {
|
|
@@ -363,11 +368,23 @@ export default class StageCore extends EventEmitter {
|
|
|
363
368
|
}
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
/**
|
|
372
|
+
* page-el-update 后,若当前选中节点仍在新页面文档中且需要自动滚动,则重新触发 scrollIntoView
|
|
373
|
+
*/
|
|
374
|
+
private scrollSelectedIntoViewAfterPageUpdate(): void {
|
|
375
|
+
const selected = this.actionManager?.getSelectedEl();
|
|
376
|
+
if (!selected?.isConnected) return;
|
|
377
|
+
if (!(this.autoScrollIntoView || selected.dataset.autoScrollIntoView)) return;
|
|
378
|
+
|
|
379
|
+
this.mask?.observerIntersection(selected);
|
|
380
|
+
}
|
|
381
|
+
|
|
366
382
|
private getActionManagerConfig(config: StageCoreConfig): ActionManagerConfig {
|
|
367
383
|
const actionManagerConfig: ActionManagerConfig = {
|
|
368
384
|
containerHighlightClassName: config.containerHighlightClassName,
|
|
369
385
|
containerHighlightDuration: config.containerHighlightDuration,
|
|
370
386
|
containerHighlightType: config.containerHighlightType,
|
|
387
|
+
containerHighlightAddOnly: config.containerHighlightAddOnly,
|
|
371
388
|
moveableOptions: config.moveableOptions,
|
|
372
389
|
container: this.mask!.content,
|
|
373
390
|
disabledDragStart: config.disabledDragStart,
|
|
@@ -394,6 +411,10 @@ export default class StageCore extends EventEmitter {
|
|
|
394
411
|
this.mask?.observe(el);
|
|
395
412
|
this.observePageResize(el);
|
|
396
413
|
|
|
414
|
+
// observe 切页时会重置滚动;若 select + scrollIntoView 已先完成,这里对仍挂载的选中节点
|
|
415
|
+
// 重新触发一次,避免滚动被打回顶部
|
|
416
|
+
this.scrollSelectedIntoViewAfterPageUpdate();
|
|
417
|
+
|
|
397
418
|
this.emit('page-el-update', el);
|
|
398
419
|
});
|
|
399
420
|
}
|
package/src/StageMask.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { createDiv, getDocument, injectStyle } from '@tmagic/core';
|
|
|
21
21
|
import { Mode, ZIndex } from './const';
|
|
22
22
|
import Rule from './Rule';
|
|
23
23
|
import type { MaskEvents, RuleOptions } from './types';
|
|
24
|
-
import { getScrollParent, isFixedParent } from './util';
|
|
24
|
+
import { getScrollParent, isFixedParent, scrollElementIntoView } from './util';
|
|
25
25
|
|
|
26
26
|
const wrapperClassName = 'editor-mask-wrapper';
|
|
27
27
|
|
|
@@ -108,28 +108,50 @@ export default class StageMask extends Rule {
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* 初始化视窗和蒙层监听,监听元素是否在视窗区域、监听mask蒙层所在的wrapper大小变化
|
|
111
|
-
* @description
|
|
111
|
+
* @description 初始化视窗和蒙层监听;切换到新页面 DOM 时重置滚动
|
|
112
112
|
* @param page 页面Dom节点
|
|
113
113
|
*/
|
|
114
114
|
public observe(page: HTMLElement): void {
|
|
115
115
|
if (!page) return;
|
|
116
116
|
|
|
117
|
+
const pageChanged = this.page !== page;
|
|
118
|
+
|
|
117
119
|
this.page = page;
|
|
118
120
|
this.initObserverIntersection();
|
|
119
121
|
this.initObserverWrapper();
|
|
122
|
+
|
|
123
|
+
if (pageChanged) {
|
|
124
|
+
// runtime 按 pageId 重挂载后,滚动容器(多为 documentElement)可能仍残留上一页偏移;
|
|
125
|
+
// 新页默认从顶部展示:重置页面与 mask 滚动,并立刻 scroll() 刷新 transform/标尺。
|
|
126
|
+
// syncPageSize 在宽高为 0 时会提前返回,不能依赖它刷新视觉状态。
|
|
127
|
+
// 若 select 已先 scrollIntoView,由 StageCore 在 page-el-update 后重新触发一次补回。
|
|
128
|
+
this.pageScrollParent?.scrollTo({ top: 0, left: 0 });
|
|
129
|
+
this.scrollTop = 0;
|
|
130
|
+
this.scrollLeft = 0;
|
|
131
|
+
this.scroll();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 页面切换后立即同步一次;ResizeObserver 首次回调可能仍上报旧页/0 尺寸
|
|
135
|
+
this.syncPageSize();
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
/**
|
|
123
|
-
* 处理页面大小变更,同步页面和mask大小
|
|
139
|
+
* 处理页面大小变更,同步页面和 mask 大小
|
|
140
|
+
* @description 仅处理当前 page 且仍挂载在文档中的 entry;非当前页、已卸载或宽高为 0 的回调会被忽略
|
|
124
141
|
* @param entries ResizeObserverEntry,获取页面最新大小
|
|
125
142
|
*/
|
|
126
143
|
public pageResize(entries: ResizeObserverEntry[]): void {
|
|
127
|
-
|
|
128
|
-
const { clientHeight, clientWidth } = entry.target;
|
|
129
|
-
this.setHeight(clientHeight);
|
|
130
|
-
this.setWidth(clientWidth);
|
|
144
|
+
if (!this.page || !entries.length) return;
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
// 取当前 page 对应 entry,避免 batch 中首项是旧页时整批被丢弃
|
|
147
|
+
const entry = entries.find((item) => item.target === this.page);
|
|
148
|
+
if (!entry) return;
|
|
149
|
+
|
|
150
|
+
const { target } = entry;
|
|
151
|
+
// 旧页卸载后仍可能回调,已断开连接的节点不再同步尺寸
|
|
152
|
+
if (!target.isConnected) return;
|
|
153
|
+
|
|
154
|
+
this.applyPageSize(target.clientWidth, target.clientHeight);
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
/**
|
|
@@ -165,10 +187,12 @@ export default class StageMask extends Rule {
|
|
|
165
187
|
return;
|
|
166
188
|
}
|
|
167
189
|
|
|
168
|
-
el.scrollIntoView();
|
|
169
|
-
|
|
170
190
|
if (!this.pageScrollParent) return;
|
|
171
191
|
|
|
192
|
+
// 不使用原生 scrollIntoView,避免编辑器画布外层滚动容器被浏览器连带滚动导致整个 stage 位移,
|
|
193
|
+
// 只滚动页面所在的滚动容器
|
|
194
|
+
scrollElementIntoView(el, this.pageScrollParent);
|
|
195
|
+
|
|
172
196
|
this.scrollLeft = this.pageScrollParent.scrollLeft;
|
|
173
197
|
this.scrollTop = this.pageScrollParent.scrollTop;
|
|
174
198
|
|
|
@@ -184,6 +208,7 @@ export default class StageMask extends Rule {
|
|
|
184
208
|
this.content?.remove();
|
|
185
209
|
this.page = null;
|
|
186
210
|
this.pageScrollParent = null;
|
|
211
|
+
this.intersectionObserver?.disconnect();
|
|
187
212
|
this.wrapperResizeObserver?.disconnect();
|
|
188
213
|
}
|
|
189
214
|
|
|
@@ -285,6 +310,26 @@ export default class StageMask extends Rule {
|
|
|
285
310
|
this.content.dispatchEvent(event);
|
|
286
311
|
}
|
|
287
312
|
|
|
313
|
+
/**
|
|
314
|
+
* 从当前页面同步蒙层宽高,并修正滚动偏移
|
|
315
|
+
*/
|
|
316
|
+
private syncPageSize(): void {
|
|
317
|
+
if (!this.page?.isConnected) return;
|
|
318
|
+
|
|
319
|
+
this.applyPageSize(this.page.clientWidth, this.page.clientHeight);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 应用页面尺寸到蒙层;宽高为 0 时忽略,避免切换/布局过程中把 editor-mask 置空
|
|
324
|
+
*/
|
|
325
|
+
private applyPageSize(clientWidth: number, clientHeight: number): void {
|
|
326
|
+
if (clientWidth <= 0 || clientHeight <= 0) return;
|
|
327
|
+
|
|
328
|
+
this.setHeight(clientHeight);
|
|
329
|
+
this.setWidth(clientWidth);
|
|
330
|
+
this.scroll();
|
|
331
|
+
}
|
|
332
|
+
|
|
288
333
|
/**
|
|
289
334
|
* 设置蒙层高度
|
|
290
335
|
* @param height 高度
|
package/src/types.ts
CHANGED
|
@@ -53,7 +53,11 @@ export type GetTargetElement = (id: Id) => HTMLElement | null;
|
|
|
53
53
|
/** render提供的接口,通过坐标获得坐标下所有HTML元素数组 */
|
|
54
54
|
export type GetElementsFromPoint = (point: Point) => HTMLElement[];
|
|
55
55
|
export type GetRenderDocument = () => Document | undefined;
|
|
56
|
-
export type DelayedMarkContainer = (
|
|
56
|
+
export type DelayedMarkContainer = (
|
|
57
|
+
event: MouseEvent,
|
|
58
|
+
exclude?: Element[],
|
|
59
|
+
isAdd?: boolean,
|
|
60
|
+
) => NodeJS.Timeout | undefined;
|
|
57
61
|
export type MarkContainerEnd = () => HTMLElement | null;
|
|
58
62
|
export type GetRootContainer = () => HTMLDivElement | undefined;
|
|
59
63
|
|
|
@@ -74,6 +78,11 @@ export interface StageCoreConfig {
|
|
|
74
78
|
containerHighlightClassName?: string;
|
|
75
79
|
containerHighlightDuration?: number;
|
|
76
80
|
containerHighlightType?: ContainerHighlightType;
|
|
81
|
+
/**
|
|
82
|
+
* 是否仅在新增组件(从组件列表拖入新组件)时才启用将组件拖入容器,
|
|
83
|
+
* 开启后在画布中拖动已有组件不会识别容器,默认 false
|
|
84
|
+
*/
|
|
85
|
+
containerHighlightAddOnly?: boolean;
|
|
77
86
|
moveableOptions?: CustomizeMoveableOptions;
|
|
78
87
|
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
|
79
88
|
runtimeUrl?: string;
|
|
@@ -102,6 +111,8 @@ export interface ActionManagerConfig {
|
|
|
102
111
|
containerHighlightClassName?: string;
|
|
103
112
|
containerHighlightDuration?: number;
|
|
104
113
|
containerHighlightType?: ContainerHighlightType;
|
|
114
|
+
/** 见 StageCoreConfig.containerHighlightAddOnly */
|
|
115
|
+
containerHighlightAddOnly?: boolean;
|
|
105
116
|
moveableOptions?: CustomizeMoveableOptions;
|
|
106
117
|
disabledDragStart?: boolean;
|
|
107
118
|
disabledMultiSelect?: boolean;
|
|
@@ -224,6 +235,8 @@ export interface UpdateData {
|
|
|
224
235
|
parent?: MContainer;
|
|
225
236
|
parentId?: Id;
|
|
226
237
|
root: MApp;
|
|
238
|
+
/** 插入到父节点 items 中的目标下标;runtime 优先按此同步,避免依赖选中态推算顺序 */
|
|
239
|
+
index?: number;
|
|
227
240
|
}
|
|
228
241
|
|
|
229
242
|
export interface RemoveData {
|
package/src/util.ts
CHANGED
|
@@ -126,7 +126,7 @@ export const getScrollParent = (element: HTMLElement, includeHidden = false): HT
|
|
|
126
126
|
|
|
127
127
|
if (isFixed(style)) return null;
|
|
128
128
|
|
|
129
|
-
for (let parent = element; parent.parentElement;
|
|
129
|
+
for (let parent = element; parent.parentElement;) {
|
|
130
130
|
parent = parent.parentElement;
|
|
131
131
|
|
|
132
132
|
if (parent.tagName === 'HTML') return parent;
|
|
@@ -141,6 +141,48 @@ export const getScrollParent = (element: HTMLElement, includeHidden = false): HT
|
|
|
141
141
|
return null;
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
|
|
146
|
+
* @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,
|
|
147
|
+
* 避免编辑器画布外层容器(如 stage 外层使用 transform 模拟滚动的容器)被浏览器自动滚动导致整个画布位移
|
|
148
|
+
* @param el 目标元素
|
|
149
|
+
* @param container 滚动容器
|
|
150
|
+
*/
|
|
151
|
+
export const scrollElementIntoView = (el: Element, container: HTMLElement): void => {
|
|
152
|
+
const { ownerDocument } = container;
|
|
153
|
+
const win = ownerDocument?.defaultView;
|
|
154
|
+
if (!ownerDocument || !win) return;
|
|
155
|
+
|
|
156
|
+
const elRect = el.getBoundingClientRect();
|
|
157
|
+
|
|
158
|
+
let viewTop = 0;
|
|
159
|
+
let viewHeight = 0;
|
|
160
|
+
|
|
161
|
+
if (container === ownerDocument.documentElement || container === ownerDocument.body) {
|
|
162
|
+
// 文档滚动元素的可视区域为窗口视口
|
|
163
|
+
viewTop = 0;
|
|
164
|
+
viewHeight = win.innerHeight;
|
|
165
|
+
} else {
|
|
166
|
+
const containerRect = container.getBoundingClientRect();
|
|
167
|
+
viewTop = containerRect.top + container.clientTop;
|
|
168
|
+
viewHeight = container.clientHeight;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const viewBottom = viewTop + viewHeight;
|
|
172
|
+
|
|
173
|
+
let delta = 0;
|
|
174
|
+
if (elRect.top < viewTop) {
|
|
175
|
+
delta = elRect.top - viewTop;
|
|
176
|
+
} else if (elRect.bottom > viewBottom) {
|
|
177
|
+
// 元素高度超过可视区域高度时,优先保证元素顶部可见
|
|
178
|
+
delta = Math.min(elRect.bottom - viewBottom, elRect.top - viewTop);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (delta !== 0) {
|
|
182
|
+
container.scrollTop += delta;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
144
186
|
export const removeSelectedClassName = (doc: Document) => {
|
|
145
187
|
const oldEl = doc.querySelector(`.${SELECTED_CLASS}`);
|
|
146
188
|
|