141 lines
3.7 KiB
TypeScript
141 lines
3.7 KiB
TypeScript
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<StickyPosition>,
|
|
default: 'top',
|
|
},
|
|
},
|
|
|
|
emits: ['scroll'],
|
|
|
|
setup(props, { emit, slots }) {
|
|
const root = ref<HTMLElement>();
|
|
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<CSSProperties | undefined>(() => {
|
|
const { fixed, height, width } = state;
|
|
if (fixed) {
|
|
return {
|
|
width: `${width}px`,
|
|
height: `${height}px`,
|
|
};
|
|
}
|
|
});
|
|
|
|
const stickyStyle = computed<CSSProperties | undefined>(() => {
|
|
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 () => (
|
|
<div ref={root} style={rootStyle.value}>
|
|
<div class={bem({ fixed: state.fixed })} style={stickyStyle.value}>
|
|
{slots.default?.()}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
});
|