import { ref, computed, CSSProperties, PropType, reactive } from 'vue'; // Utils import { isHidden, unitToPx, getScrollTop, getZIndexStyle, createNamespace, } from '../utils'; // Composables import { useRect, useEventListener, useScrollParent } from '@vant/use'; import { useVisibilityChange } from '../composables/use-visibility-change'; const [createComponent, bem] = createNamespace('sticky'); export type StickyPosition = 'top' | 'bottom'; export default createComponent({ props: { zIndex: [Number, String], container: Element, offsetTop: { type: [Number, String], default: 0, }, offsetBottom: { type: [Number, String], default: 0, }, position: { type: String as PropType, default: 'top', }, }, emits: ['scroll'], setup(props, { emit, slots }) { const root = ref(); const scrollParent = useScrollParent(root); const state = reactive({ fixed: false, width: 0, // root width height: 0, // root height transform: 0, }); const offset = computed(() => unitToPx(props.position === 'top' ? props.offsetTop : props.offsetBottom) ); const rootStyle = computed(() => { const { fixed, height, width } = state; if (fixed) { return { width: `${width}px`, height: `${height}px`, }; } }); const stickyStyle = computed(() => { if (!state.fixed) { return; } const style: CSSProperties = { ...getZIndexStyle(props.zIndex), width: `${state.width}px`, height: `${state.height}px`, [props.position]: `${offset.value}px`, }; if (state.transform) { style.transform = `translate3d(0, ${state.transform}px, 0)`; } return style; }); const emitScroll = (scrollTop: number) => emit('scroll', { scrollTop, isFixed: state.fixed, }); const onScroll = () => { if (!root.value || isHidden(root)) { return; } const { container, position } = props; const rootRect = useRect(root); const scrollTop = getScrollTop(window); state.width = rootRect.width; state.height = rootRect.height; if (position === 'top') { // The sticky component should be kept inside the container element if (container) { const containerRect = useRect(container); const difference = containerRect.bottom - offset.value - state.height; state.fixed = offset.value > rootRect.top && containerRect.bottom > 0; state.transform = difference < 0 ? difference : 0; } else { state.fixed = offset.value > rootRect.top; } } else { const { clientHeight } = document.documentElement; if (container) { const containerRect = useRect(container); const difference = clientHeight - containerRect.top - offset.value - state.height; state.fixed = clientHeight - offset.value < rootRect.bottom && clientHeight > containerRect.top; state.transform = difference < 0 ? -difference : 0; } else { state.fixed = clientHeight - offset.value < rootRect.bottom; } } emitScroll(scrollTop); }; useEventListener('scroll', onScroll, { target: scrollParent }); useVisibilityChange(root, onScroll); return () => (
{slots.default?.()}
); }, });