46 lines
2.3 KiB
TypeScript
46 lines
2.3 KiB
TypeScript
import { h } from '@ui-arch/frame';
|
|
import type { AffixProps } from './types.js';
|
|
import { watchViewport } from './watch.js';
|
|
|
|
/*** 固钉:内容滚动越过阈值后转 fixed 贴住视口边缘(top / bottom),外层保留占位高度避免布局跳动 ***/
|
|
export function affix(props: AffixProps, ...children: unknown[]): HTMLElement {
|
|
// —— 状态集中声明 ——
|
|
const offset = props.offset ?? 0; // 触发固定的阈值
|
|
const atBottom = props.position === 'bottom'; // 贴底还是贴顶(底部适合操作条)
|
|
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 = atBottom ? box.bottom >= window.innerHeight - offset : 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');
|
|
if (atBottom) inner.style.bottom = `${offset}px`; // 贴底:固定边距写 bottom,top 保持空
|
|
else 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.bottom = '';
|
|
inner.style.width = '';
|
|
wrap.style.height = '';
|
|
}
|
|
};
|
|
|
|
watchViewport(wrap, scroller, sync); // 滚动 / 缩放后重算,节点脱离文档即自摘监听
|
|
return wrap;
|
|
}
|