import { CSSProperties, defineComponent, inject } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; import omit from 'omit.js'; import ResizeObserver from '../vc-resize-observer'; import BaseMixin from '../_util/BaseMixin'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; import { defaultConfigProvider } from '../config-provider'; import warning from '../_util/warning'; import { withInstall } from '../_util/type'; import { addObserveTarget, removeObserveTarget, getTargetRect, getFixedTop, getFixedBottom, } from './utils'; function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; } enum AffixStatus { None, Prepare, } export interface AffixState { affixStyle?: CSSProperties; placeholderStyle?: CSSProperties; status: AffixStatus; lastAffix: boolean; prevTarget: Window | HTMLElement | null; } // Affix const AffixProps = { /** * 距离窗口顶部达到指定偏移量后触发 */ offsetTop: PropTypes.number, offset: PropTypes.number, /** 距离窗口底部达到指定偏移量后触发 */ offsetBottom: PropTypes.number, /** 固定状态改变时触发的回调函数 */ // onChange?: (affixed?: boolean) => void; /** 设置 Affix 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 */ target: PropTypes.func.def(getDefaultTarget), prefixCls: PropTypes.string, onChange: PropTypes.func, onTestUpdatePosition: PropTypes.func, }; const Affix = defineComponent({ name: 'AAffix', mixins: [BaseMixin], props: AffixProps, emits: ['change', 'testUpdatePosition'], setup() { return { configProvider: inject('configProvider', defaultConfigProvider), }; }, data() { return { affixStyle: undefined, placeholderStyle: undefined, status: AffixStatus.None, lastAffix: false, prevTarget: null, timeout: null, }; }, watch: { target(val) { let newTarget = null; if (val) { newTarget = val() || null; } if (this.prevTarget !== newTarget) { removeObserveTarget(this); if (newTarget) { addObserveTarget(newTarget, this); // Mock Event object. this.updatePosition(); } this.prevTarget = newTarget; } }, offsetTop() { this.updatePosition(); }, offsetBottom() { this.updatePosition(); }, }, beforeMount() { this.updatePosition = throttleByAnimationFrame(this.updatePosition); this.lazyUpdatePosition = throttleByAnimationFrame(this.lazyUpdatePosition); }, mounted() { const { target } = this; if (target) { // [Legacy] Wait for parent component ref has its value. // We should use target as directly element instead of function which makes element check hard. this.timeout = setTimeout(() => { addObserveTarget(target(), this); // Mock Event object. this.updatePosition(); }); } }, updated() { this.measure(); }, beforeUnmount() { clearTimeout(this.timeout); removeObserveTarget(this); (this.updatePosition as any).cancel(); // https://github.com/ant-design/ant-design/issues/22683 (this.lazyUpdatePosition as any).cancel(); }, methods: { getOffsetTop() { const { offset, offsetBottom } = this; let { offsetTop } = this; if (typeof offsetTop === 'undefined') { offsetTop = offset; warning( typeof offset === 'undefined', 'Affix', '`offset` is deprecated. Please use `offsetTop` instead.', ); } if (offsetBottom === undefined && offsetTop === undefined) { offsetTop = 0; } return offsetTop; }, getOffsetBottom() { return this.offsetBottom; }, // =================== Measure =================== measure() { const { status, lastAffix } = this; const { target } = this; if ( status !== AffixStatus.Prepare || !this.$refs.fixedNode || !this.$refs.placeholderNode || !target ) { return; } const offsetTop = this.getOffsetTop(); const offsetBottom = this.getOffsetBottom(); const targetNode = target(); if (!targetNode) { return; } const newState = { status: AffixStatus.None, } as AffixState; const targetRect = getTargetRect(targetNode); const placeholderReact = getTargetRect(this.$refs.placeholderNode as HTMLElement); const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop); const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom); if (fixedTop !== undefined) { newState.affixStyle = { position: 'fixed', top: fixedTop, width: placeholderReact.width + 'px', height: placeholderReact.height + 'px', }; newState.placeholderStyle = { width: placeholderReact.width + 'px', height: placeholderReact.height + 'px', }; } else if (fixedBottom !== undefined) { newState.affixStyle = { position: 'fixed', bottom: fixedBottom, width: placeholderReact.width + 'px', height: placeholderReact.height + 'px', }; newState.placeholderStyle = { width: placeholderReact.width + 'px', height: placeholderReact.height + 'px', }; } newState.lastAffix = !!newState.affixStyle; if (lastAffix !== newState.lastAffix) { this.$emit('change', newState.lastAffix); } this.setState(newState); }, prepareMeasure() { this.setState({ status: AffixStatus.Prepare, affixStyle: undefined, placeholderStyle: undefined, }); this.$forceUpdate(); // Test if `updatePosition` called if (process.env.NODE_ENV === 'test') { this.$emit('testUpdatePosition'); } }, updatePosition() { this.prepareMeasure(); }, lazyUpdatePosition() { const { target } = this; const { affixStyle } = this; // Check position change before measure to make Safari smooth if (target && affixStyle) { const offsetTop = this.getOffsetTop(); const offsetBottom = this.getOffsetBottom(); const targetNode = target(); if (targetNode && this.$refs.placeholderNode) { const targetRect = getTargetRect(targetNode); const placeholderReact = getTargetRect(this.$refs.placeholderNode as HTMLElement); const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop); const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom); if ( (fixedTop !== undefined && affixStyle.top === fixedTop) || (fixedBottom !== undefined && affixStyle.bottom === fixedBottom) ) { return; } } } // Directly call prepare measure since it's already throttled. this.prepareMeasure(); }, }, render() { const { prefixCls, affixStyle, placeholderStyle, $slots, $props } = this; const getPrefixCls = this.configProvider.getPrefixCls; const className = classNames({ [getPrefixCls('affix', prefixCls)]: affixStyle, }); const props = omit($props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target']); return ( { this.updatePosition(); }} >
{$slots.default?.()}
); }, }); export default withInstall(Affix);