diff --git a/components/_util/getScroll.js b/components/_util/getScroll.js deleted file mode 100644 index 1b085e3e3..000000000 --- a/components/_util/getScroll.js +++ /dev/null @@ -1,17 +0,0 @@ -export default function getScroll(target, top) { - if (typeof window === 'undefined') { - return 0; - } - - const prop = top ? 'pageYOffset' : 'pageXOffset'; - const method = top ? 'scrollTop' : 'scrollLeft'; - const isWindow = target === window; - - let ret = isWindow ? target[prop] : target[method]; - // ie6,7,8 standard mode - if (isWindow && typeof ret !== 'number') { - ret = window.document.documentElement[method]; - } - - return ret; -} diff --git a/components/_util/getScroll.ts b/components/_util/getScroll.ts new file mode 100644 index 000000000..70b50141d --- /dev/null +++ b/components/_util/getScroll.ts @@ -0,0 +1,27 @@ +export function isWindow(obj: any) { + return obj !== null && obj !== undefined && obj === obj.window; +} + +export default function getScroll( + target: HTMLElement | Window | Document | null, + top: boolean, +): number { + if (typeof window === 'undefined') { + return 0; + } + const method = top ? 'scrollTop' : 'scrollLeft'; + let result = 0; + if (isWindow(target)) { + result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset']; + } else if (target instanceof Document) { + result = target.documentElement[method]; + } else if (target) { + result = (target as HTMLElement)[method]; + } + if (target && !isWindow(target) && typeof result !== 'number') { + result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement?.[ + method + ]; + } + return result; +} diff --git a/components/_util/scrollTo.ts b/components/_util/scrollTo.ts index 31237d248..f41c1b649 100644 --- a/components/_util/scrollTo.ts +++ b/components/_util/scrollTo.ts @@ -1,9 +1,10 @@ -import getScroll from './getScroll'; +import raf from './raf'; +import getScroll, { isWindow } from './getScroll'; import { easeInOutCubic } from './easings'; interface ScrollToOptions { /** Scroll container, default as window */ - getContainer?: () => HTMLElement | Window; + getContainer?: () => HTMLElement | Window | Document; /** Scroll end callback */ callback?: () => any; /** Animation duration, default as 450 */ @@ -12,7 +13,6 @@ interface ScrollToOptions { export default function scrollTo(y: number, options: ScrollToOptions = {}) { const { getContainer = () => window, callback, duration = 450 } = options; - const container = getContainer(); const scrollTop = getScroll(container, true); const startTime = Date.now(); @@ -21,16 +21,18 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) { const timestamp = Date.now(); const time = timestamp - startTime; const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration); - if (container === window) { - window.scrollTo(window.pageXOffset, nextScrollTop); + if (isWindow(container)) { + (container as Window).scrollTo(window.pageXOffset, nextScrollTop); + } else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') { + (container as HTMLDocument).documentElement.scrollTop = nextScrollTop; } else { (container as HTMLElement).scrollTop = nextScrollTop; } if (time < duration) { - requestAnimationFrame(frameFunc); + raf(frameFunc); } else if (typeof callback === 'function') { callback(); } }; - requestAnimationFrame(frameFunc); + raf(frameFunc); } diff --git a/components/back-top/index.tsx b/components/back-top/index.tsx index 2b18ad93b..c1e87948a 100644 --- a/components/back-top/index.tsx +++ b/components/back-top/index.tsx @@ -7,8 +7,13 @@ import { onBeforeUnmount, onMounted, reactive, + PropType, + ref, + watch, + onDeactivated, + computed, } from 'vue'; -import classNames from '../_util/classNames'; +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'; @@ -16,18 +21,12 @@ import { getTransitionProps, Transition } from '../_util/transition'; import { defaultConfigProvider } from '../config-provider'; import scrollTo from '../_util/scrollTo'; import { withInstall } from '../_util/type'; - -function getDefaultTarget() { - return window; -} +import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; export const backTopProps = { - // 滚动高度达到此参数值才出现 BackTop visibilityHeight: PropTypes.number.def(400), - // 回到顶部所需时间(ms) @4.4.0 duration: PropTypes.number.def(450), - // 设置需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 - target: PropTypes.func, + target: Function as PropType<() => HTMLElement | Window | Document>, prefixCls: PropTypes.string, onClick: PropTypes.func, // visible: PropTypes.looseBool, // Only for test. Don't use it. @@ -42,12 +41,15 @@ const BackTop = defineComponent({ 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, { @@ -57,51 +59,85 @@ const BackTop = defineComponent({ emit('click', e); }; - const handleScroll = () => { - const { visibilityHeight, target = getDefaultTarget } = props; - const scrollTop = getScroll(target(), true); + 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(() => { - const getTarget = props.target || getDefaultTarget; - state.scrollEvent = addEventListener(getTarget(), 'scroll', handleScroll); - handleScroll(); + bindScrollEvent(); }); }); onActivated(() => { nextTick(() => { - handleScroll(); + bindScrollEvent(); }); }); - onBeforeUnmount(() => { - if (state.scrollEvent) { - state.scrollEvent.remove(); - } + onDeactivated(() => { + scrollRemove(); }); - return () => { - const { prefixCls: customizePrefixCls } = props; + onBeforeUnmount(() => { + scrollRemove(); + }); - const getPrefixCls = configProvider.getPrefixCls; - const prefixCls = getPrefixCls('back-top', customizePrefixCls); - const classString = classNames(prefixCls, attrs.class); + const prefixCls = computed(() => configProvider.getPrefixCls('back-top', props.prefixCls)); + + return () => { const defaultElement = ( -