vant/src/sticky/index.tsx
2020-11-16 11:41:46 +08:00

125 lines
3.0 KiB
TypeScript

import { ref, reactive, computed, CSSProperties } from 'vue';
// Utils
import {
isHidden,
unitToPx,
getScrollTop,
getElementTop,
createNamespace,
} from '../utils';
// Composition
import { useScrollParent, useEventListener } from '@vant/use';
import { useVisibilityChange } from '../composables/use-visibility-change';
const [createComponent, bem] = createNamespace('sticky');
export default createComponent({
props: {
zIndex: [Number, String],
container: null,
offsetTop: {
type: [Number, String],
default: 0,
},
},
emits: ['scroll'],
setup(props, { emit, slots }) {
const root = ref<HTMLElement>();
const scrollParent = useScrollParent(root);
const state = reactive({
fixed: false,
height: 0,
transform: 0,
});
const offsetTop = computed(() => unitToPx(props.offsetTop));
const style = computed<CSSProperties | undefined>(() => {
if (!state.fixed) {
return;
}
const top = offsetTop.value ? `${offsetTop.value}px` : undefined;
const transform = state.transform
? `translate3d(0, ${state.transform}px, 0)`
: undefined;
return {
top,
zIndex: props.zIndex !== undefined ? +props.zIndex : undefined,
transform,
};
});
const emitScrollEvent = (scrollTop: number) => {
emit('scroll', {
scrollTop,
isFixed: state.fixed,
});
};
const onScroll = () => {
if (!root.value || isHidden(root)) {
return;
}
state.height = root.value.offsetHeight;
const { container } = props;
const scrollTop = getScrollTop(window);
const topToPageTop = getElementTop(root.value);
// The sticky component should be kept inside the container element
if (container) {
const bottomToPageTop = topToPageTop + container.offsetHeight;
if (scrollTop + offsetTop.value + state.height > bottomToPageTop) {
const distanceToBottom = state.height + scrollTop - bottomToPageTop;
if (distanceToBottom < state.height) {
state.fixed = true;
state.transform = -(distanceToBottom + offsetTop.value);
} else {
state.fixed = false;
}
emitScrollEvent(scrollTop);
return;
}
}
if (scrollTop + offsetTop.value > topToPageTop) {
state.fixed = true;
state.transform = 0;
} else {
state.fixed = false;
}
emitScrollEvent(scrollTop);
};
useEventListener('scroll', onScroll, { target: scrollParent });
useVisibilityChange(root, onScroll);
return () => {
const { fixed, height } = state;
const rootStyle: CSSProperties = {
height: fixed ? `${height}px` : undefined,
};
return (
<div ref={root} style={rootStyle}>
<div class={bem({ fixed })} style={style.value}>
{slots.default?.()}
</div>
</div>
);
};
},
});