/** * 拦截文章正文/目录里的 hash 锚点点击,阻止 Dioxus 接管导致整页刷新。 * * 背景:Dioxus 0.7 的 NativeInterpreter 通过事件委托在根节点上监听 click。 * 只要 app 任意位置用了 onclick(本项目 post_nav_links.rs 有),Dioxus 就启用 * 全局 click 委托。点击 dangerous_inner_html 注入的原生 时: * 1. click 冒泡到根 → handleEvent 同步 POST /__events * 2. 目标 没有 Dioxus onclick → response 不含 preventDefault * 3. 兜底分支 handleClickNavigate: event.preventDefault() + browser_open IPC * 4. browser_open 走 History::external → window.location.set_href("#id") → 整页刷新 * * 结果:原生 fragment-scroll 被 preventDefault 拦掉,IPC 又把 hash 当外部 URL * 整页加载,表现为「点击锚点刷新文章页且不滚动」。WASM 未下载完时是纯 SSR HTML, * 走浏览器原生 fragment-scroll 正常——所以问题只在 hydration 后出现。 * * 修复:在 capture 阶段(早于 Dioxus 的 bubble 委托监听器)拦截指向同页 hash 的 * 点击,stopPropagation 阻止事件到达 Dioxus 的委托监听器,自行滚动到标题 * (scrollToHeading 手动扣除 header 高度)并用 history.replaceState 更新 URL hash * (不触发 popstate/hashchange,避免抖动)。 */ import { scrollToHeading } from './scroll-to-heading'; let installed = false; /** * 安装全局 hash 锚点点击拦截器。幂等:重复调用安全。 * * 用 capture 阶段 + 在 window 上注册,保证在 Dioxus 的根监听器之前执行; * stopPropagation 阻止事件继续传播,Dioxus 的 handleEvent/handleClickNavigate * 不会收到这个事件,browser_open IPC 不触发,整页刷新被避免。 */ export function initAnchorClick(): void { if (installed) return; installed = true; window.addEventListener( 'click', (event) => { // 只处理无修饰键的左键点击(中键/ctrl/cmd 点击交给浏览器新窗口行为)。 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { return; } const target = event.target; if (!(target instanceof Element)) return; // 从点击点向上找最近的 (点击可能落在 内的子元素上)。 const anchor = target.closest('a'); if (!anchor) return; const href = anchor.getAttribute('href'); // 只接管同页 hash 锚点:href 以 # 开头且非空(排除纯 "#" 和 "#top" 之外的真锚点)。 // 外链、路径链接、空 hash 都交给浏览器/Dioxus 原行为。 if (!href?.startsWith('#') || href === '#') return; const id = decodeURIComponent(href.slice(1)); const el = document.getElementById(id); if (!el) return; // 目标不存在:放行,交还原生行为(不滚动但不刷新) // 阻止事件继续传播到 Dioxus 的委托监听器 → 避免 handleClickNavigate。 event.stopPropagation(); event.preventDefault(); // 平滑滚动到目标标题,手动扣除 sticky header 高度(见 scroll-to-heading.ts)。 scrollToHeading(el, true); // 更新 URL hash 但不产生历史记录抖动:replaceState 不触发 popstate/hashchange。 // 这样地址栏显示当前章节,刷新页面也能 scrollToHash 回到原位。 if (window.location.hash !== href) { history.replaceState(null, '', href); } }, true, // capture:在 Dioxus 的 bubble 监听器之前执行 ); }