ant-design-vue/components/back-top/index.tsx

148 lines
3.9 KiB
Vue

import type { ExtractPropTypes, PropType } from 'vue';
import {
defineComponent,
inject,
nextTick,
onActivated,
onBeforeUnmount,
onMounted,
reactive,
ref,
watch,
onDeactivated,
computed,
} from 'vue';
import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined';
import PropTypes from '../_util/vue-types';
import addEventListener from '../vc-util/Dom/addEventListener';
import getScroll from '../_util/getScroll';
import { getTransitionProps, Transition } from '../_util/transition';
import { defaultConfigProvider } from '../config-provider';
import scrollTo from '../_util/scrollTo';
import { withInstall } from '../_util/type';
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
export const backTopProps = {
visibilityHeight: PropTypes.number.def(400),
duration: PropTypes.number.def(450),
target: Function as PropType<() => HTMLElement | Window | Document>,
prefixCls: PropTypes.string,
onClick: PropTypes.func,
// visible: PropTypes.looseBool, // Only for test. Don't use it.
};
export type BackTopProps = Partial<ExtractPropTypes<typeof backTopProps>>;
const BackTop = defineComponent({
name: 'ABackTop',
inheritAttrs: false,
props: backTopProps,
emits: ['click'],
setup(props, { slots, attrs, emit }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const domRef = ref();
const state = reactive({
visible: false,
scrollEvent: null,
});
const getDefaultTarget = () =>
domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window;
const scrollToTop = (e: Event) => {
const { target = getDefaultTarget, duration } = props;
scrollTo(0, {
getContainer: target,
duration,
});
emit('click', e);
};
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
const { visibilityHeight } = props;
const scrollTop = getScroll(e.target, true);
state.visible = scrollTop > visibilityHeight;
});
const bindScrollEvent = () => {
const { target } = props;
const getTarget = target || getDefaultTarget;
const container = getTarget();
state.scrollEvent = addEventListener(container, 'scroll', (e: Event) => {
handleScroll(e);
});
handleScroll({
target: container,
});
};
const scrollRemove = () => {
if (state.scrollEvent) {
state.scrollEvent.remove();
}
(handleScroll as any).cancel();
};
watch(
() => props.target,
() => {
scrollRemove();
nextTick(() => {
bindScrollEvent();
});
},
);
onMounted(() => {
nextTick(() => {
bindScrollEvent();
});
});
onActivated(() => {
nextTick(() => {
bindScrollEvent();
});
});
onDeactivated(() => {
scrollRemove();
});
onBeforeUnmount(() => {
scrollRemove();
});
const prefixCls = computed(() => configProvider.getPrefixCls('back-top', props.prefixCls));
return () => {
const defaultElement = (
<div class={`${prefixCls.value}-content`}>
<div class={`${prefixCls.value}-icon`}>
<VerticalAlignTopOutlined />
</div>
</div>
);
const divProps = {
...attrs,
onClick: scrollToTop,
class: {
[`${prefixCls.value}`]: true,
[`${attrs.class}`]: attrs.class,
[`${prefixCls.value}-rtl`]: configProvider.direction === 'rtl',
},
};
const backTopBtn = state.visible ? (
<div {...divProps} ref={domRef}>
{slots.default?.() || defaultElement}
</div>
) : null;
const transitionProps = getTransitionProps('fade');
return <Transition {...transitionProps}>{backTopBtn}</Transition>;
};
},
});
export default withInstall(BackTop);