diff --git a/Update.json b/Update.json index d784cc30..362302d3 100644 --- a/Update.json +++ b/Update.json @@ -3850,6 +3850,17 @@ } ], "Notes": "
修复修改账号页面迁移后菜单与更新日志无法访问的问题,保留新版账号表单,在当前账号页面独立显示标签编辑,并保留 AC 代码导出功能。旧账号链接自动跳转到当前页面。恢复个人中心的身份信息、评分、标签、最后在线与已解决题目,修复请求失败后的空白信息及图例重叠。移除语言切换入口,经典界面和新版竞赛界面统一使用中文,隐藏账号表单中的语言选项并统一提交中文。修复最后在线时间显示为 HTML 源码的问题。重新绘制个人中心提交与正确数量的历史图表,兼容浅色、深色主题及移动端,默认清理设置不再隐藏图表。提交与正确数量在同一图表中以细线和半透明柱形叠加展示,减少密集曲线遮挡。恢复暂存更改中的 API 请求超时和错误日志。
" + }, + "3.8.4": { + "UpdateDate": 1791114523418, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1044, + "Description": "fix: keep navbar spacer visible under browser cosmetic filters" + } + ], + "Notes": "修复浏览器过滤规则隐藏导航栏占位元素、导致页面顶部内容被遮挡的问题。
" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index ab400e36..5f659d6e 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.8.3 +// @version 3.8.4 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -3442,13 +3442,23 @@ class NavbarStyler { } } - preserveStyles(element, properties) { + preserveStyles(element, properties, preserveOrder = false) { + const order = preserveOrder ? Array.from(element.style) : null; const original = properties.map(property => [property, element.style.getPropertyValue(property), element.style.getPropertyPriority(property)]); return () => { for (const [property, value, priority] of original) { if (value) element.style.setProperty(property, value, priority); else element.style.removeProperty(property); } + if (order) { + // Attribute selectors can depend on declaration order. Restore that + // order while retaining unrelated styles changed by the page. + const current = Array.from(element.style); + const names = [...order.filter(name => current.includes(name)), ...current.filter(name => !order.includes(name))]; + const values = names.map(name => [name, element.style.getPropertyValue(name), element.style.getPropertyPriority(name)]); + element.style.cssText = ''; + for (const [name, value, priority] of values) element.style.setProperty(name, value, priority); + } }; } @@ -3459,9 +3469,11 @@ class NavbarStyler { this.restoreNavbarStyles(); this.restoreOverlayStyles?.(); this.restoreSpacerStyles?.(); + this.restoreSpacerLayoutStyles?.(); this.overlay?.remove(); this.overlayStyle?.remove(); this.spacer?.remove(); + this.spacerStyle?.remove(); this.navbar = null; } @@ -3577,20 +3589,36 @@ class NavbarStyler { try { let spacer = document.getElementById('navbar-spacer'); let newHeight = this.navbar.offsetHeight + 24; + // Browser cosmetic filters can hide empty body children whose inline + // style starts with display:block; width:100%; height:. Keep static + // layout rules outside that attribute, including when Bootstrap is late. + if (!this.spacerStyle?.isConnected) { + this.spacerStyle = document.createElement('style'); + this.spacerStyle.textContent = '#navbar-spacer { display: block; width: 100%; }'; + (document.head || document.documentElement).appendChild(this.spacerStyle); + } if (!spacer) { // Legacy page handlers use body > div for the content container. // A block span reserves space without becoming their first match. spacer = document.createElement('span'); spacer.id = 'navbar-spacer'; spacer.setAttribute('aria-hidden', 'true'); - spacer.style.display = 'block'; - spacer.style.width = '100%'; document.body.insertBefore(spacer, document.body.firstChild); this.spacer = spacer; } else if (spacer !== this.spacer && spacer !== this.borrowedSpacer) { + this.restoreSpacerStyles?.(); + this.restoreSpacerLayoutStyles?.(); + this.restoreSpacerLayoutStyles = null; this.borrowedSpacer = spacer; this.restoreSpacerStyles = this.preserveStyles(spacer, ['height']); } + // Older initializers may leave the same fingerprint on a borrowed spacer. + // Normalize only matching nodes, then restore their layout on teardown. + if (spacer !== this.spacer && spacer.matches('body > [style^="display: block; width: 100%; height:"]:empty')) { + if (!this.restoreSpacerLayoutStyles) this.restoreSpacerLayoutStyles = this.preserveStyles(spacer, ['display', 'width'], true); + spacer.style.removeProperty('display'); + spacer.style.removeProperty('width'); + } // Update in place so an existing page-owned spacer is never replaced. if (spacer.style.height !== `${newHeight}px`) spacer.style.height = `${newHeight}px`; } catch (e) { diff --git a/package.json b/package.json index 5f8df3e5..69334302 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.8.3", + "version": "3.8.4", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { diff --git a/tests/navbar-styler.test.cjs b/tests/navbar-styler.test.cjs index 2cdb28fe..4a581ed7 100644 --- a/tests/navbar-styler.test.cjs +++ b/tests/navbar-styler.test.cjs @@ -264,3 +264,79 @@ test('navbar reserves content space before Bootstrap CSS loads and restores its await browser.close(); } }); + +test('browser cosmetic filtering does not hide the navbar spacer', {timeout: 60000}, async () => { + const start = 'let navbarStyler = null;'; + const end = '// Wrapped in an async IIFE'; + const navbarSource = source.slice(source.indexOf(start), source.indexOf(end)); + const browser = await chromium.launch({executablePath: process.env.XMOJ_CHROMIUM || undefined}); + const page = await browser.newPage(); + try { + // The reporter's video shows this browser-injected cosmetic rule. A block + // spacer whose inline style begins with these declarations is hidden. + await page.setContent(`