import { h } from '@ui-arch/frame'; import type { AffixProps } from './types.js'; /*** 固钉:内容滚动越过阈值后转 fixed 贴住视口顶部,外层保留占位高度避免布局跳动 ***/ export function affix(props: AffixProps, ...children: unknown[]): HTMLElement { // —— 状态集中声明 —— const offset = props.offset ?? 0; // 触发固定的阈值 const scroller = props.target ?? window; // 滚动容器 const inner = h('div', { class: 'ua-affix-inner' }, ...children) as HTMLElement; // 真正会被固定的内容 const wrap = h('div', { class: 'ua-affix' }, inner) as HTMLElement; // 占位容器,固定时撑住原位 let fixed = false; // 当前是否已固定,避免每次滚动都重写样式 /*** 同步固定态:越过阈值即 fixed + 撑占位高度,回到阈值以上就还原 ***/ const sync = (): void => { const box = wrap.getBoundingClientRect(); // 占位容器几何:left / width 决定固定后的横向位置 const should = box.top <= offset; // 已滚到阈值之上 if (should === fixed) { if (fixed) { // 固定中:宽度与横向位置跟随容器(窗口缩放 / 父容器变宽) inner.style.width = `${box.width}px`; inner.style.left = `${box.left}px`; } return; // 卫语句:状态未变不重写其余样式 } fixed = should; if (fixed) { wrap.style.height = `${inner.offsetHeight}px`; // 先撑住占位,再让 inner 脱离文档流 inner.classList.add('is-fixed'); inner.style.top = `${offset}px`; inner.style.left = `${box.left}px`; inner.style.width = `${box.width}px`; } else { inner.classList.remove('is-fixed'); inner.style.top = ''; inner.style.width = ''; wrap.style.height = ''; } }; scroller.addEventListener('scroll', sync, { passive: true }); // 被动监听,不阻塞滚动 window.addEventListener('resize', sync); // 视口变化会改判定与宽度 sync(); // 首屏同步一次:刷新时若已滚动到位,直接进入固定态 return wrap; }