@tmagic/stage 1.8.0-manmanyu.9 → 1.8.1
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 +57 -9
- package/dist/es/index.js +2 -2
- package/dist/es/util.js +50 -1
- package/dist/tmagic-stage.umd.cjs +306 -149
- package/package.json +5 -5
- package/src/ActionManager.ts +20 -8
- package/src/StageCore.ts +23 -2
- package/src/StageMask.ts +80 -10
- package/src/types.ts +14 -1
- package/src/util.ts +72 -1
- package/types/index.d.ts +150 -97
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.
|
|
2
|
+
"version": "1.8.1",
|
|
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.
|
|
47
|
+
"@tmagic/core": "1.8.1"
|
|
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, isDocumentVisible, isElementVisible, 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
|
/**
|
|
@@ -154,7 +176,14 @@ export default class StageMask extends Rule {
|
|
|
154
176
|
this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
|
|
155
177
|
}
|
|
156
178
|
|
|
179
|
+
/**
|
|
180
|
+
* 将元素滚动到画布可视区域内
|
|
181
|
+
* @description 页面不可见时不做滚动:此时滚动既无视觉效果,又会记录错误的滚动偏移
|
|
182
|
+
* @param el 目标元素
|
|
183
|
+
*/
|
|
157
184
|
public scrollIntoView(el: Element): void {
|
|
185
|
+
if (!this.isPageVisible()) return;
|
|
186
|
+
|
|
158
187
|
// 不可以有横向滚动
|
|
159
188
|
if (!this.page || el.getBoundingClientRect().left >= this.page.scrollWidth) return;
|
|
160
189
|
|
|
@@ -165,10 +194,12 @@ export default class StageMask extends Rule {
|
|
|
165
194
|
return;
|
|
166
195
|
}
|
|
167
196
|
|
|
168
|
-
el.scrollIntoView();
|
|
169
|
-
|
|
170
197
|
if (!this.pageScrollParent) return;
|
|
171
198
|
|
|
199
|
+
// 不使用原生 scrollIntoView,避免编辑器画布外层滚动容器被浏览器连带滚动导致整个 stage 位移,
|
|
200
|
+
// 只滚动页面所在的滚动容器
|
|
201
|
+
scrollElementIntoView(el, this.pageScrollParent);
|
|
202
|
+
|
|
172
203
|
this.scrollLeft = this.pageScrollParent.scrollLeft;
|
|
173
204
|
this.scrollTop = this.pageScrollParent.scrollTop;
|
|
174
205
|
|
|
@@ -184,6 +215,7 @@ export default class StageMask extends Rule {
|
|
|
184
215
|
this.content?.remove();
|
|
185
216
|
this.page = null;
|
|
186
217
|
this.pageScrollParent = null;
|
|
218
|
+
this.intersectionObserver?.disconnect();
|
|
187
219
|
this.wrapperResizeObserver?.disconnect();
|
|
188
220
|
}
|
|
189
221
|
|
|
@@ -210,6 +242,10 @@ export default class StageMask extends Rule {
|
|
|
210
242
|
(entries) => {
|
|
211
243
|
entries.forEach((entry) => {
|
|
212
244
|
const { target, intersectionRatio } = entry;
|
|
245
|
+
|
|
246
|
+
// 页面不可见时不滚动,同时保留监听,等页面恢复可见后由 IntersectionObserver 重新回调
|
|
247
|
+
if (!this.isPageVisible()) return;
|
|
248
|
+
|
|
213
249
|
if (intersectionRatio <= 0) {
|
|
214
250
|
this.scrollIntoView(target);
|
|
215
251
|
}
|
|
@@ -285,6 +321,40 @@ export default class StageMask extends Rule {
|
|
|
285
321
|
this.content.dispatchEvent(event);
|
|
286
322
|
}
|
|
287
323
|
|
|
324
|
+
/**
|
|
325
|
+
* 页面是否可见
|
|
326
|
+
* @description 页面未挂载、或画布容器被隐藏(如编辑器切到其他面板)时页面不可见;
|
|
327
|
+
* 蒙层与页面所在的 iframe 挂载在同一容器中,容器隐藏时页面内部元素仍可能是"可见"的,因此额外判断蒙层;
|
|
328
|
+
* 浏览器 tab 不可见(切到其他标签页、窗口最小化)时同样视为不可见
|
|
329
|
+
*/
|
|
330
|
+
private isPageVisible(): boolean {
|
|
331
|
+
if (!isDocumentVisible(this.page?.ownerDocument) || !isDocumentVisible(this.wrapper.ownerDocument)) return false;
|
|
332
|
+
|
|
333
|
+
if (!isElementVisible(this.page)) return false;
|
|
334
|
+
|
|
335
|
+
return !this.wrapper.isConnected || isElementVisible(this.wrapper);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 从当前页面同步蒙层宽高,并修正滚动偏移
|
|
340
|
+
*/
|
|
341
|
+
private syncPageSize(): void {
|
|
342
|
+
if (!this.page?.isConnected) return;
|
|
343
|
+
|
|
344
|
+
this.applyPageSize(this.page.clientWidth, this.page.clientHeight);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 应用页面尺寸到蒙层;宽高为 0 时忽略,避免切换/布局过程中把 editor-mask 置空
|
|
349
|
+
*/
|
|
350
|
+
private applyPageSize(clientWidth: number, clientHeight: number): void {
|
|
351
|
+
if (clientWidth <= 0 || clientHeight <= 0) return;
|
|
352
|
+
|
|
353
|
+
this.setHeight(clientHeight);
|
|
354
|
+
this.setWidth(clientWidth);
|
|
355
|
+
this.scroll();
|
|
356
|
+
}
|
|
357
|
+
|
|
288
358
|
/**
|
|
289
359
|
* 设置蒙层高度
|
|
290
360
|
* @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,77 @@ export const getScrollParent = (element: HTMLElement, includeHidden = false): HT
|
|
|
141
141
|
return null;
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 判断文档是否可见
|
|
146
|
+
* @description 浏览器 tab 切到后台、窗口最小化时文档 visibilityState 为 hidden;
|
|
147
|
+
* 只将 hidden 视为不可见,避免 prerender 等中间态被误判
|
|
148
|
+
* @param doc 目标文档
|
|
149
|
+
*/
|
|
150
|
+
export const isDocumentVisible = (doc: Document | null | undefined): boolean => doc?.visibilityState !== 'hidden';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 判断元素是否可见
|
|
154
|
+
* @description 元素未挂载到文档、或其自身/祖先被 display:none、visibility:hidden 隐藏时视为不可见;
|
|
155
|
+
* 优先使用浏览器提供的 checkVisibility,不支持时退化为沿祖先链检查计算样式
|
|
156
|
+
* @param el 目标元素
|
|
157
|
+
*/
|
|
158
|
+
export const isElementVisible = (el: Element | null | undefined): boolean => {
|
|
159
|
+
if (!el?.isConnected) return false;
|
|
160
|
+
|
|
161
|
+
if (typeof el.checkVisibility === 'function') {
|
|
162
|
+
return el.checkVisibility({ checkVisibilityCSS: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (let node: Element | null = el; node; node = node.parentElement) {
|
|
166
|
+
const { display, visibility } = getComputedStyle(node);
|
|
167
|
+
if (display === 'none' || visibility === 'hidden') return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return true;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
|
|
175
|
+
* @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,
|
|
176
|
+
* 避免编辑器画布外层容器(如 stage 外层使用 transform 模拟滚动的容器)被浏览器自动滚动导致整个画布位移
|
|
177
|
+
* @param el 目标元素
|
|
178
|
+
* @param container 滚动容器
|
|
179
|
+
*/
|
|
180
|
+
export const scrollElementIntoView = (el: Element, container: HTMLElement): void => {
|
|
181
|
+
const { ownerDocument } = container;
|
|
182
|
+
const win = ownerDocument?.defaultView;
|
|
183
|
+
if (!ownerDocument || !win) return;
|
|
184
|
+
|
|
185
|
+
const elRect = el.getBoundingClientRect();
|
|
186
|
+
|
|
187
|
+
let viewTop = 0;
|
|
188
|
+
let viewHeight = 0;
|
|
189
|
+
|
|
190
|
+
if (container === ownerDocument.documentElement || container === ownerDocument.body) {
|
|
191
|
+
// 文档滚动元素的可视区域为窗口视口
|
|
192
|
+
viewTop = 0;
|
|
193
|
+
viewHeight = win.innerHeight;
|
|
194
|
+
} else {
|
|
195
|
+
const containerRect = container.getBoundingClientRect();
|
|
196
|
+
viewTop = containerRect.top + container.clientTop;
|
|
197
|
+
viewHeight = container.clientHeight;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const viewBottom = viewTop + viewHeight;
|
|
201
|
+
|
|
202
|
+
let delta = 0;
|
|
203
|
+
if (elRect.top < viewTop) {
|
|
204
|
+
delta = elRect.top - viewTop;
|
|
205
|
+
} else if (elRect.bottom > viewBottom) {
|
|
206
|
+
// 元素高度超过可视区域高度时,优先保证元素顶部可见
|
|
207
|
+
delta = Math.min(elRect.bottom - viewBottom, elRect.top - viewTop);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (delta !== 0) {
|
|
211
|
+
container.scrollTop += delta;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
144
215
|
export const removeSelectedClassName = (doc: Document) => {
|
|
145
216
|
const oldEl = doc.querySelector(`.${SELECTED_CLASS}`);
|
|
146
217
|
|