@tmagic/stage 1.8.0 → 1.8.2

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,5 +1,5 @@
1
1
  import { Mode, ZIndex } from "./const.js";
2
- import { getScrollParent, isFixedParent, scrollElementIntoView } from "./util.js";
2
+ import { getScrollParent, isDocumentVisible, isElementVisible, isFixedParent, scrollElementIntoView } from "./util.js";
3
3
  import Rule from "./Rule.js";
4
4
  import { createDiv, getDocument, injectStyle } from "@tmagic/core";
5
5
  //#region packages/stage/src/StageMask.ts
@@ -124,7 +124,13 @@ var StageMask = class extends Rule {
124
124
  setLayout(el) {
125
125
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
126
126
  }
127
+ /**
128
+ * 将元素滚动到画布可视区域内
129
+ * @description 页面不可见时不做滚动:此时滚动既无视觉效果,又会记录错误的滚动偏移
130
+ * @param el 目标元素
131
+ */
127
132
  scrollIntoView(el) {
133
+ if (!this.isPageVisible()) return;
128
134
  if (!this.page || el.getBoundingClientRect().left >= this.page.scrollWidth) return;
129
135
  const scrollParent = getScrollParent(el);
130
136
  if (scrollParent && scrollParent !== this.pageScrollParent) {
@@ -163,6 +169,7 @@ var StageMask = class extends Rule {
163
169
  if (typeof IntersectionObserver !== "undefined") this.intersectionObserver = new IntersectionObserver((entries) => {
164
170
  entries.forEach((entry) => {
165
171
  const { target, intersectionRatio } = entry;
172
+ if (!this.isPageVisible()) return;
166
173
  if (intersectionRatio <= 0) this.scrollIntoView(target);
167
174
  this.intersectionObserver?.unobserve(target);
168
175
  });
@@ -216,6 +223,17 @@ var StageMask = class extends Rule {
216
223
  this.content.dispatchEvent(event);
217
224
  }
218
225
  /**
226
+ * 页面是否可见
227
+ * @description 页面未挂载、或画布容器被隐藏(如编辑器切到其他面板)时页面不可见;
228
+ * 蒙层与页面所在的 iframe 挂载在同一容器中,容器隐藏时页面内部元素仍可能是"可见"的,因此额外判断蒙层;
229
+ * 浏览器 tab 不可见(切到其他标签页、窗口最小化)时同样视为不可见
230
+ */
231
+ isPageVisible() {
232
+ if (!isDocumentVisible(this.page?.ownerDocument) || !isDocumentVisible(this.wrapper.ownerDocument)) return false;
233
+ if (!isElementVisible(this.page)) return false;
234
+ return !this.wrapper.isConnected || isElementVisible(this.wrapper);
235
+ }
236
+ /**
219
237
  * 从当前页面同步蒙层宽高,并修正滚动偏移
220
238
  */
221
239
  syncPageSize() {
package/dist/es/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AbleActionEventType, CONTAINER_HIGHLIGHT_CLASS_NAME, ContainerHighlightType, DEFAULT_ZOOM, DRAG_EL_ID_PREFIX, FLASH_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, HIGHLIGHT_EL_ID_PREFIX, Mode, MouseButton, PAGE_CLASS, RenderType, SELECTED_CLASS, SelectStatus, StageDragStatus, ZIndex } from "./const.js";
2
- import { addSelectedClassName, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up } from "./util.js";
2
+ import { addSelectedClassName, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isDocumentVisible, isElementVisible, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up } from "./util.js";
3
3
  import MoveableActionsAble_default from "./MoveableActionsAble.js";
4
4
  import StageDragResize from "./StageDragResize.js";
5
5
  import StageMask from "./StageMask.js";
@@ -9,4 +9,4 @@ export * from "moveable";
9
9
  //#region packages/stage/src/index.ts
10
10
  var src_default = StageCore;
11
11
  //#endregion
12
- export { AbleActionEventType, CONTAINER_HIGHLIGHT_CLASS_NAME, ContainerHighlightType, DEFAULT_ZOOM, DRAG_EL_ID_PREFIX, FLASH_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, HIGHLIGHT_EL_ID_PREFIX, Mode, MouseButton, MoveableActionsAble_default as MoveableActionsAble, PAGE_CLASS, RenderType, SELECTED_CLASS, SelectStatus, StageDragResize, StageDragStatus, StageMask, StageRender, ZIndex, addSelectedClassName, src_default as default, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up };
12
+ export { AbleActionEventType, CONTAINER_HIGHLIGHT_CLASS_NAME, ContainerHighlightType, DEFAULT_ZOOM, DRAG_EL_ID_PREFIX, FLASH_EL_ID_PREFIX, GHOST_EL_ID_PREFIX, GuidesType, HIGHLIGHT_EL_ID_PREFIX, Mode, MouseButton, MoveableActionsAble_default as MoveableActionsAble, PAGE_CLASS, RenderType, SELECTED_CLASS, SelectStatus, StageDragResize, StageDragStatus, StageMask, StageRender, ZIndex, addSelectedClassName, src_default as default, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isDocumentVisible, isElementVisible, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up };
package/dist/es/util.js CHANGED
@@ -92,6 +92,28 @@ var getScrollParent = (element, includeHidden = false) => {
92
92
  return null;
93
93
  };
94
94
  /**
95
+ * 判断文档是否可见
96
+ * @description 浏览器 tab 切到后台、窗口最小化时文档 visibilityState 为 hidden;
97
+ * 只将 hidden 视为不可见,避免 prerender 等中间态被误判
98
+ * @param doc 目标文档
99
+ */
100
+ var isDocumentVisible = (doc) => doc?.visibilityState !== "hidden";
101
+ /**
102
+ * 判断元素是否可见
103
+ * @description 元素未挂载到文档、或其自身/祖先被 display:none、visibility:hidden 隐藏时视为不可见;
104
+ * 优先使用浏览器提供的 checkVisibility,不支持时退化为沿祖先链检查计算样式
105
+ * @param el 目标元素
106
+ */
107
+ var isElementVisible = (el) => {
108
+ if (!el?.isConnected) return false;
109
+ if (typeof el.checkVisibility === "function") return el.checkVisibility({ checkVisibilityCSS: true });
110
+ for (let node = el; node; node = node.parentElement) {
111
+ const { display, visibility } = getComputedStyle(node);
112
+ if (display === "none" || visibility === "hidden") return false;
113
+ }
114
+ return true;
115
+ };
116
+ /**
95
117
  * 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
96
118
  * @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,
97
119
  * 避免编辑器画布外层容器(如 stage 外层使用 transform 模拟滚动的容器)被浏览器自动滚动导致整个画布位移
@@ -216,4 +238,4 @@ var getBorderWidth = (el) => {
216
238
  };
217
239
  };
218
240
  //#endregion
219
- export { addSelectedClassName, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up };
241
+ export { addSelectedClassName, down, getAbsolutePosition, getBorderWidth, getMarginValue, getMode, getOffset, getScrollParent, getTargetElStyle, isAbsolute, isDocumentVisible, isElementVisible, isFixed, isFixedParent, isMoveableButton, isRelative, isStatic, removeSelectedClassName, scrollElementIntoView, up };
@@ -2386,6 +2386,28 @@
2386
2386
  return null;
2387
2387
  };
2388
2388
  /**
2389
+ * 判断文档是否可见
2390
+ * @description 浏览器 tab 切到后台、窗口最小化时文档 visibilityState 为 hidden;
2391
+ * 只将 hidden 视为不可见,避免 prerender 等中间态被误判
2392
+ * @param doc 目标文档
2393
+ */
2394
+ var isDocumentVisible = (doc) => doc?.visibilityState !== "hidden";
2395
+ /**
2396
+ * 判断元素是否可见
2397
+ * @description 元素未挂载到文档、或其自身/祖先被 display:none、visibility:hidden 隐藏时视为不可见;
2398
+ * 优先使用浏览器提供的 checkVisibility,不支持时退化为沿祖先链检查计算样式
2399
+ * @param el 目标元素
2400
+ */
2401
+ var isElementVisible = (el) => {
2402
+ if (!el?.isConnected) return false;
2403
+ if (typeof el.checkVisibility === "function") return el.checkVisibility({ checkVisibilityCSS: true });
2404
+ for (let node = el; node; node = node.parentElement) {
2405
+ const { display, visibility } = getComputedStyle(node);
2406
+ if (display === "none" || visibility === "hidden") return false;
2407
+ }
2408
+ return true;
2409
+ };
2410
+ /**
2389
2411
  * 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
2390
2412
  * @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,
2391
2413
  * 避免编辑器画布外层容器(如 stage 外层使用 transform 模拟滚动的容器)被浏览器自动滚动导致整个画布位移
@@ -4422,7 +4444,13 @@
4422
4444
  setLayout(el) {
4423
4445
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
4424
4446
  }
4447
+ /**
4448
+ * 将元素滚动到画布可视区域内
4449
+ * @description 页面不可见时不做滚动:此时滚动既无视觉效果,又会记录错误的滚动偏移
4450
+ * @param el 目标元素
4451
+ */
4425
4452
  scrollIntoView(el) {
4453
+ if (!this.isPageVisible()) return;
4426
4454
  if (!this.page || el.getBoundingClientRect().left >= this.page.scrollWidth) return;
4427
4455
  const scrollParent = getScrollParent(el);
4428
4456
  if (scrollParent && scrollParent !== this.pageScrollParent) {
@@ -4461,6 +4489,7 @@
4461
4489
  if (typeof IntersectionObserver !== "undefined") this.intersectionObserver = new IntersectionObserver((entries) => {
4462
4490
  entries.forEach((entry) => {
4463
4491
  const { target, intersectionRatio } = entry;
4492
+ if (!this.isPageVisible()) return;
4464
4493
  if (intersectionRatio <= 0) this.scrollIntoView(target);
4465
4494
  this.intersectionObserver?.unobserve(target);
4466
4495
  });
@@ -4514,6 +4543,17 @@
4514
4543
  this.content.dispatchEvent(event);
4515
4544
  }
4516
4545
  /**
4546
+ * 页面是否可见
4547
+ * @description 页面未挂载、或画布容器被隐藏(如编辑器切到其他面板)时页面不可见;
4548
+ * 蒙层与页面所在的 iframe 挂载在同一容器中,容器隐藏时页面内部元素仍可能是"可见"的,因此额外判断蒙层;
4549
+ * 浏览器 tab 不可见(切到其他标签页、窗口最小化)时同样视为不可见
4550
+ */
4551
+ isPageVisible() {
4552
+ if (!isDocumentVisible(this.page?.ownerDocument) || !isDocumentVisible(this.wrapper.ownerDocument)) return false;
4553
+ if (!isElementVisible(this.page)) return false;
4554
+ return !this.wrapper.isConnected || isElementVisible(this.wrapper);
4555
+ }
4556
+ /**
4517
4557
  * 从当前页面同步蒙层宽高,并修正滚动偏移
4518
4558
  */
4519
4559
  syncPageSize() {
@@ -5178,6 +5218,8 @@
5178
5218
  exports.getScrollParent = getScrollParent;
5179
5219
  exports.getTargetElStyle = getTargetElStyle;
5180
5220
  exports.isAbsolute = isAbsolute;
5221
+ exports.isDocumentVisible = isDocumentVisible;
5222
+ exports.isElementVisible = isElementVisible;
5181
5223
  exports.isFixed = isFixed;
5182
5224
  exports.isFixedParent = isFixedParent;
5183
5225
  exports.isMoveableButton = isMoveableButton;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0",
2
+ "version": "1.8.2",
3
3
  "name": "@tmagic/stage",
4
4
  "type": "module",
5
5
  "sideEffects": false,
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "typescript": "^6.0.3",
47
- "@tmagic/core": "1.8.0"
47
+ "@tmagic/core": "1.8.2"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "typescript": {
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, scrollElementIntoView } from './util';
24
+ import { getScrollParent, isDocumentVisible, isElementVisible, isFixedParent, scrollElementIntoView } from './util';
25
25
 
26
26
  const wrapperClassName = 'editor-mask-wrapper';
27
27
 
@@ -176,7 +176,14 @@ export default class StageMask extends Rule {
176
176
  this.setMode(isFixedParent(el) ? Mode.FIXED : Mode.ABSOLUTE);
177
177
  }
178
178
 
179
+ /**
180
+ * 将元素滚动到画布可视区域内
181
+ * @description 页面不可见时不做滚动:此时滚动既无视觉效果,又会记录错误的滚动偏移
182
+ * @param el 目标元素
183
+ */
179
184
  public scrollIntoView(el: Element): void {
185
+ if (!this.isPageVisible()) return;
186
+
180
187
  // 不可以有横向滚动
181
188
  if (!this.page || el.getBoundingClientRect().left >= this.page.scrollWidth) return;
182
189
 
@@ -235,6 +242,10 @@ export default class StageMask extends Rule {
235
242
  (entries) => {
236
243
  entries.forEach((entry) => {
237
244
  const { target, intersectionRatio } = entry;
245
+
246
+ // 页面不可见时不滚动,同时保留监听,等页面恢复可见后由 IntersectionObserver 重新回调
247
+ if (!this.isPageVisible()) return;
248
+
238
249
  if (intersectionRatio <= 0) {
239
250
  this.scrollIntoView(target);
240
251
  }
@@ -310,6 +321,20 @@ export default class StageMask extends Rule {
310
321
  this.content.dispatchEvent(event);
311
322
  }
312
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
+
313
338
  /**
314
339
  * 从当前页面同步蒙层宽高,并修正滚动偏移
315
340
  */
package/src/util.ts CHANGED
@@ -141,6 +141,35 @@ 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
+
144
173
  /**
145
174
  * 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
146
175
  * @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,
package/types/index.d.ts CHANGED
@@ -727,6 +727,11 @@ declare class StageMask extends Rule {
727
727
  */
728
728
  mount(el: HTMLDivElement): void;
729
729
  setLayout(el: HTMLElement): void;
730
+ /**
731
+ * 将元素滚动到画布可视区域内
732
+ * @description 页面不可见时不做滚动:此时滚动既无视觉效果,又会记录错误的滚动偏移
733
+ * @param el 目标元素
734
+ */
730
735
  scrollIntoView(el: Element): void;
731
736
  /**
732
737
  * 销毁实例
@@ -744,6 +749,13 @@ declare class StageMask extends Rule {
744
749
  private initObserverWrapper;
745
750
  private scroll;
746
751
  private scrollTo;
752
+ /**
753
+ * 页面是否可见
754
+ * @description 页面未挂载、或画布容器被隐藏(如编辑器切到其他面板)时页面不可见;
755
+ * 蒙层与页面所在的 iframe 挂载在同一容器中,容器隐藏时页面内部元素仍可能是"可见"的,因此额外判断蒙层;
756
+ * 浏览器 tab 不可见(切到其他标签页、窗口最小化)时同样视为不可见
757
+ */
758
+ private isPageVisible;
747
759
  /**
748
760
  * 从当前页面同步蒙层宽高,并修正滚动偏移
749
761
  */
@@ -1123,6 +1135,20 @@ export declare const isFixed: (style: {
1123
1135
  export declare const isFixedParent: (el: Element) => boolean;
1124
1136
  export declare const getMode: (el: Element) => Mode;
1125
1137
  export declare const getScrollParent: (element: HTMLElement, includeHidden?: boolean) => HTMLElement | null;
1138
+ /**
1139
+ * 判断文档是否可见
1140
+ * @description 浏览器 tab 切到后台、窗口最小化时文档 visibilityState 为 hidden;
1141
+ * 只将 hidden 视为不可见,避免 prerender 等中间态被误判
1142
+ * @param doc 目标文档
1143
+ */
1144
+ export declare const isDocumentVisible: (doc: Document | null | undefined) => boolean;
1145
+ /**
1146
+ * 判断元素是否可见
1147
+ * @description 元素未挂载到文档、或其自身/祖先被 display:none、visibility:hidden 隐藏时视为不可见;
1148
+ * 优先使用浏览器提供的 checkVisibility,不支持时退化为沿祖先链检查计算样式
1149
+ * @param el 目标元素
1150
+ */
1151
+ export declare const isElementVisible: (el: Element | null | undefined) => boolean;
1126
1152
  /**
1127
1153
  * 将元素滚动到指定滚动容器的可视区域内(仅垂直方向,滚动最小距离)
1128
1154
  * @description 与原生 scrollIntoView 不同,只滚动指定的容器自身,不会连带滚动外层祖先滚动容器,