import type { PropType } from 'vue'; import { defineComponent, reactive } from 'vue'; import classNames from '../_util/classNames'; import createRef from '../_util/createRef'; import raf from '../_util/raf'; import supportsPassive from '../_util/supportsPassive'; import PropTypes from '../_util/vue-types'; const MIN_SIZE = 20; interface ScrollBarState { dragging: boolean; pageY: number | null; startTop: number | null; visible: boolean; } function getPageY(e: MouseEvent | TouchEvent) { return 'touches' in e ? e.touches[0].pageY : e.pageY; } export default defineComponent({ name: 'ScrollBar', inheritAttrs: false, props: { prefixCls: PropTypes.string, scrollTop: PropTypes.number, scrollHeight: PropTypes.number, height: PropTypes.number, count: PropTypes.number, onScroll: { type: Function as PropType<(scrollTop: number) => void>, }, onStartMove: { type: Function as PropType<() => void>, }, onStopMove: { type: Function as PropType<() => void>, }, }, setup() { return { moveRaf: null, scrollbarRef: createRef(), thumbRef: createRef(), visibleTimeout: null, state: reactive({ dragging: false, pageY: null, startTop: null, visible: false, }), }; }, watch: { scrollTop: { handler() { this.delayHidden(); }, flush: 'post', }, }, mounted() { this.scrollbarRef.current?.addEventListener( 'touchstart', this.onScrollbarTouchStart, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); this.thumbRef.current?.addEventListener( 'touchstart', this.onMouseDown, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); }, beforeUnmount() { this.removeEvents(); clearTimeout(this.visibleTimeout); }, methods: { delayHidden() { clearTimeout(this.visibleTimeout); this.state.visible = true; this.visibleTimeout = setTimeout(() => { this.state.visible = false; }, 2000); }, onScrollbarTouchStart(e: TouchEvent) { e.preventDefault(); }, onContainerMouseDown(e: MouseEvent) { e.stopPropagation(); e.preventDefault(); }, // ======================= Clean ======================= patchEvents() { window.addEventListener('mousemove', this.onMouseMove); window.addEventListener('mouseup', this.onMouseUp); this.thumbRef.current.addEventListener( 'touchmove', this.onMouseMove, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); this.thumbRef.current.addEventListener('touchend', this.onMouseUp); }, removeEvents() { window.removeEventListener('mousemove', this.onMouseMove); window.removeEventListener('mouseup', this.onMouseUp); this.scrollbarRef.current.removeEventListener( 'touchstart', this.onScrollbarTouchStart, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); this.thumbRef.current.removeEventListener( 'touchstart', this.onMouseDown, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); this.thumbRef.current.removeEventListener( 'touchmove', this.onMouseMove, supportsPassive ? ({ passive: false } as EventListenerOptions) : false, ); this.thumbRef.current.removeEventListener('touchend', this.onMouseUp); raf.cancel(this.moveRaf); }, // ======================= Thumb ======================= onMouseDown(e: MouseEvent | TouchEvent) { const { onStartMove } = this.$props; Object.assign(this.state, { dragging: true, pageY: getPageY(e), startTop: this.getTop(), }); onStartMove(); this.patchEvents(); e.stopPropagation(); e.preventDefault(); }, onMouseMove(e: MouseEvent | TouchEvent) { const { dragging, pageY, startTop } = this.state; const { onScroll } = this.$props; raf.cancel(this.moveRaf); if (dragging) { const offsetY = getPageY(e) - pageY; const newTop = startTop + offsetY; const enableScrollRange = this.getEnableScrollRange(); const enableHeightRange = this.getEnableHeightRange(); const ptg = enableHeightRange ? newTop / enableHeightRange : 0; const newScrollTop = Math.ceil(ptg * enableScrollRange); this.moveRaf = raf(() => { onScroll(newScrollTop); }); } }, onMouseUp() { const { onStopMove } = this.$props; this.state.dragging = false; onStopMove(); this.removeEvents(); }, // ===================== Calculate ===================== getSpinHeight() { const { height, count } = this.$props; let baseHeight = (height / count) * 10; baseHeight = Math.max(baseHeight, MIN_SIZE); baseHeight = Math.min(baseHeight, height / 2); return Math.floor(baseHeight); }, getEnableScrollRange() { const { scrollHeight, height } = this.$props; return scrollHeight - height || 0; }, getEnableHeightRange() { const { height } = this.$props; const spinHeight = this.getSpinHeight(); return height - spinHeight || 0; }, getTop() { const { scrollTop } = this.$props; const enableScrollRange = this.getEnableScrollRange(); const enableHeightRange = this.getEnableHeightRange(); if (scrollTop === 0 || enableScrollRange === 0) { return 0; } const ptg = scrollTop / enableScrollRange; return ptg * enableHeightRange; }, // Not show scrollbar when height is large than scrollHeight showScroll() { const { height, scrollHeight } = this.$props; return scrollHeight > height; }, }, render() { // eslint-disable-next-line no-unused-vars const { dragging, visible } = this.state; const { prefixCls } = this.$props; const spinHeight = this.getSpinHeight() + 'px'; const top = this.getTop() + 'px'; const canScroll = this.showScroll(); const mergedVisible = canScroll && visible; return (
); }, });