diff --git a/packages/stage/src/StageMask.ts b/packages/stage/src/StageMask.ts index 04cc7f92..0cf33216 100644 --- a/packages/stage/src/StageMask.ts +++ b/packages/stage/src/StageMask.ts @@ -117,19 +117,27 @@ export default class StageMask extends Rule { this.page = page; this.initObserverIntersection(); this.initObserverWrapper(); + // 页面切换后立即同步一次;ResizeObserver 首次回调可能仍上报旧页/0 尺寸 + this.syncPageSize(); } /** - * 处理页面大小变更,同步页面和mask大小 + * 处理页面大小变更,同步页面和 mask 大小 + * @description 仅处理当前 page 且仍挂载在文档中的 entry;非当前页、已卸载或宽高为 0 的回调会被忽略 * @param entries ResizeObserverEntry,获取页面最新大小 */ public pageResize(entries: ResizeObserverEntry[]): void { - const [entry] = entries; - const { clientHeight, clientWidth } = entry.target; - this.setHeight(clientHeight); - this.setWidth(clientWidth); + if (!this.page || !entries.length) return; - this.scroll(); + // 取当前 page 对应 entry,避免 batch 中首项是旧页时整批被丢弃 + const entry = entries.find((item) => item.target === this.page); + if (!entry) return; + + const { target } = entry; + // 旧页卸载后仍可能回调,已断开连接的节点不再同步尺寸 + if (!target.isConnected) return; + + this.applyPageSize(target.clientWidth, target.clientHeight); } /** @@ -186,6 +194,7 @@ export default class StageMask extends Rule { this.content?.remove(); this.page = null; this.pageScrollParent = null; + this.intersectionObserver?.disconnect(); this.wrapperResizeObserver?.disconnect(); } @@ -287,6 +296,26 @@ export default class StageMask extends Rule { this.content.dispatchEvent(event); } + /** + * 从当前页面同步蒙层宽高,并修正滚动偏移 + */ + private syncPageSize(): void { + if (!this.page?.isConnected) return; + + this.applyPageSize(this.page.clientWidth, this.page.clientHeight); + } + + /** + * 应用页面尺寸到蒙层;宽高为 0 时忽略,避免切换/布局过程中把 editor-mask 置空 + */ + private applyPageSize(clientWidth: number, clientHeight: number): void { + if (clientWidth <= 0 || clientHeight <= 0) return; + + this.setHeight(clientHeight); + this.setWidth(clientWidth); + this.scroll(); + } + /** * 设置蒙层高度 * @param height 高度 diff --git a/packages/stage/tests/unit/StageMask.spec.ts b/packages/stage/tests/unit/StageMask.spec.ts index f48931b0..e5fc653c 100644 --- a/packages/stage/tests/unit/StageMask.spec.ts +++ b/packages/stage/tests/unit/StageMask.spec.ts @@ -79,11 +79,124 @@ describe('StageMask', () => { const page = globalThis.document.createElement('div'); Object.defineProperty(page, 'clientWidth', { value: 400, configurable: true }); Object.defineProperty(page, 'clientHeight', { value: 300, configurable: true }); + globalThis.document.body.appendChild(page); mask.observe(page); mask.pageResize([makeResizeEntry(page)]); expect(mask.width).toBe(400); expect(mask.height).toBe(300); + expect(mask.content.style.width).toBe('400px'); + expect(mask.content.style.height).toBe('300px'); + }); + + test('observe 立即同步尺寸,并从长页切到短页时修正滚动偏移', () => { + mask = new StageMask({ disabledRule: true }); + const tallPage = globalThis.document.createElement('div'); + Object.defineProperty(tallPage, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(tallPage, 'clientHeight', { value: 1000, configurable: true }); + globalThis.document.body.appendChild(tallPage); + mask.observe(tallPage); + mask.wrapperWidth = 400; + mask.wrapperHeight = 300; + (mask as any).setMaxScrollLeft(); + (mask as any).setMaxScrollTop(); + mask.scrollTop = 500; + (mask as any).scroll(); + expect(mask.scrollTop).toBe(500); + + const shortPage = globalThis.document.createElement('div'); + Object.defineProperty(shortPage, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(shortPage, 'clientHeight', { value: 400, configurable: true }); + globalThis.document.body.appendChild(shortPage); + mask.observe(shortPage); + + expect(mask.width).toBe(400); + expect(mask.height).toBe(400); + expect(mask.maxScrollTop).toBe(100); + expect(mask.scrollTop).toBe(100); + expect(mask.content.style.transform).toBe('translate3d(0px, -100px, 0)'); + }); + + test('pageResize 忽略非当前页、已卸载页与 0 尺寸,避免切换页面后 editor-mask 变为 0', () => { + mask = new StageMask({ disabledRule: true }); + const oldPage = globalThis.document.createElement('div'); + Object.defineProperty(oldPage, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(oldPage, 'clientHeight', { value: 300, configurable: true }); + globalThis.document.body.appendChild(oldPage); + mask.observe(oldPage); + expect(mask.width).toBe(400); + expect(mask.height).toBe(300); + + const newPage = globalThis.document.createElement('div'); + Object.defineProperty(newPage, 'clientWidth', { value: 500, configurable: true }); + Object.defineProperty(newPage, 'clientHeight', { value: 600, configurable: true }); + globalThis.document.body.appendChild(newPage); + mask.observe(newPage); + expect(mask.width).toBe(500); + expect(mask.height).toBe(600); + expect(mask.content.style.width).toBe('500px'); + expect(mask.content.style.height).toBe('600px'); + + // 仍连接但不是当前 page:忽略 + Object.defineProperty(oldPage, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(oldPage, 'clientHeight', { value: 100, configurable: true }); + mask.pageResize([makeResizeEntry(oldPage)]); + expect(mask.width).toBe(500); + expect(mask.height).toBe(600); + + // batch 首项是旧页、后续是当前页:仍应应用当前页尺寸 + Object.defineProperty(newPage, 'clientWidth', { value: 520, configurable: true }); + Object.defineProperty(newPage, 'clientHeight', { value: 620, configurable: true }); + mask.pageResize([makeResizeEntry(oldPage), makeResizeEntry(newPage)]); + expect(mask.width).toBe(520); + expect(mask.height).toBe(620); + + // 旧页面卸载后 ResizeObserver 可能仍回调,不应覆盖当前蒙层尺寸 + Object.defineProperty(oldPage, 'clientWidth', { value: 0, configurable: true }); + Object.defineProperty(oldPage, 'clientHeight', { value: 0, configurable: true }); + oldPage.remove(); + mask.pageResize([makeResizeEntry(oldPage)]); + expect(mask.width).toBe(520); + expect(mask.height).toBe(620); + + // 当前 page 已断开连接:忽略 + newPage.remove(); + Object.defineProperty(newPage, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(newPage, 'clientHeight', { value: 100, configurable: true }); + mask.pageResize([makeResizeEntry(newPage)]); + expect(mask.width).toBe(520); + expect(mask.height).toBe(620); + expect(mask.content.style.width).toBe('520px'); + expect(mask.content.style.height).toBe('620px'); + + // 重新挂载后短暂为 0 时也应忽略 + globalThis.document.body.appendChild(newPage); + Object.defineProperty(newPage, 'clientWidth', { value: 0, configurable: true }); + Object.defineProperty(newPage, 'clientHeight', { value: 0, configurable: true }); + mask.pageResize([makeResizeEntry(newPage)]); + expect(mask.width).toBe(520); + expect(mask.height).toBe(620); + expect(mask.content.style.width).toBe('520px'); + expect(mask.content.style.height).toBe('620px'); + }); + + test('observe 时 page 宽高为 0 不更新蒙层尺寸', () => { + mask = new StageMask({ disabledRule: true }); + const firstPage = globalThis.document.createElement('div'); + Object.defineProperty(firstPage, 'clientWidth', { value: 400, configurable: true }); + Object.defineProperty(firstPage, 'clientHeight', { value: 300, configurable: true }); + globalThis.document.body.appendChild(firstPage); + mask.observe(firstPage); + + const emptyPage = globalThis.document.createElement('div'); + Object.defineProperty(emptyPage, 'clientWidth', { value: 0, configurable: true }); + Object.defineProperty(emptyPage, 'clientHeight', { value: 0, configurable: true }); + globalThis.document.body.appendChild(emptyPage); + mask.observe(emptyPage); + + expect(mask.page).toBe(emptyPage); + expect(mask.width).toBe(400); + expect(mask.height).toBe(300); }); test('wheel 事件更新 scroll 并 emit scroll', () => { @@ -91,6 +204,7 @@ describe('StageMask', () => { const page = globalThis.document.createElement('div'); Object.defineProperty(page, 'clientWidth', { value: 1000, configurable: true }); Object.defineProperty(page, 'clientHeight', { value: 1000, configurable: true }); + globalThis.document.body.appendChild(page); mask.observe(page); mask.pageResize([makeResizeEntry(page)]); mask.wrapperWidth = 400;