🌈 An enterprise-class UI components based on Ant Design and Vue. 🐜
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

245 lines
6.8 KiB

import type { Ref } from 'vue';
import {
nextTick,
onActivated,
watchEffect,
defineComponent,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
watch,
} from 'vue';
import addEventListenerWrap from '../vc-util/Dom/addEventListener';
import { getOffset } from '../vc-util/Dom/css';
import classNames from '../_util/classNames';
import type { MouseEventHandler } from '../_util/EventInterface';
import getScrollBarSize from '../_util/getScrollBarSize';
import { useInjectTable } from './context/TableContext';
import { useLayoutState } from './hooks/useFrame';
interface StickyScrollBarProps {
scrollBodyRef: Ref<HTMLElement>;
onScroll: (params: { scrollLeft?: number }) => void;
offsetScroll: number;
container: HTMLElement | Window;
scrollBodySizeInfo: {
scrollWidth: number;
clientWidth: number;
};
}
export default defineComponent<StickyScrollBarProps>({
name: 'StickyScrollBar',
inheritAttrs: false,
props: ['offsetScroll', 'container', 'scrollBodyRef', 'scrollBodySizeInfo'] as any,
emits: ['scroll'],
setup(props, { emit, expose }) {
const tableContext = useInjectTable();
const bodyScrollWidth = shallowRef(0);
const bodyWidth = shallowRef(0);
const scrollBarWidth = shallowRef(0);
watchEffect(
() => {
bodyScrollWidth.value = props.scrollBodySizeInfo.scrollWidth || 0;
bodyWidth.value = props.scrollBodySizeInfo.clientWidth || 0;
scrollBarWidth.value =
bodyScrollWidth.value && bodyWidth.value * (bodyWidth.value / bodyScrollWidth.value);
},
{ flush: 'post' },
);
const scrollBarRef = shallowRef();
const [scrollState, setScrollState] = useLayoutState({
scrollLeft: 0,
isHiddenScrollBar: true,
});
const refState = ref({
delta: 0,
x: 0,
});
const isActive = shallowRef(false);
const onMouseUp: MouseEventHandler = () => {
isActive.value = false;
};
const onMouseDown: MouseEventHandler = event => {
refState.value = { delta: event.pageX - scrollState.value.scrollLeft, x: 0 };
isActive.value = true;
event.preventDefault();
};
const onMouseMove: MouseEventHandler = event => {
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
const { buttons } = event || (window?.event as any);
if (!isActive.value || buttons === 0) {
// If out body mouse up, we can set isActive false when mouse move
if (isActive.value) {
isActive.value = false;
}
return;
}
let left: number = refState.value.x + event.pageX - refState.value.x - refState.value.delta;
if (left <= 0) {
left = 0;
}
if (left + scrollBarWidth.value >= bodyWidth.value) {
left = bodyWidth.value - scrollBarWidth.value;
}
emit('scroll', {
scrollLeft: (left / bodyWidth.value) * (bodyScrollWidth.value + 2),
});
refState.value.x = event.pageX;
};
const onContainerScroll = () => {
if (!props.scrollBodyRef.value) {
return;
}
const tableOffsetTop = getOffset(props.scrollBodyRef.value).top;
const tableBottomOffset = tableOffsetTop + props.scrollBodyRef.value.offsetHeight;
const currentClientOffset =
props.container === window
? document.documentElement.scrollTop + window.innerHeight
: getOffset(props.container).top + (props.container as HTMLElement).clientHeight;
if (
tableBottomOffset - getScrollBarSize() <= currentClientOffset ||
tableOffsetTop >= currentClientOffset - props.offsetScroll
) {
setScrollState(state => ({
...state,
isHiddenScrollBar: true,
}));
} else {
setScrollState(state => ({
...state,
isHiddenScrollBar: false,
}));
}
};
const setScrollLeft = (left: number) => {
setScrollState(state => {
return {
...state,
scrollLeft: (left / bodyScrollWidth.value) * bodyWidth.value || 0,
};
});
};
expose({
setScrollLeft,
});
let onMouseUpListener = null;
let onMouseMoveListener = null;
let onResizeListener = null;
let onScrollListener = null;
onMounted(() => {
onMouseUpListener = addEventListenerWrap(document.body, 'mouseup', onMouseUp, false);
onMouseMoveListener = addEventListenerWrap(document.body, 'mousemove', onMouseMove, false);
onResizeListener = addEventListenerWrap(window, 'resize', onContainerScroll, false);
});
onActivated(() => {
nextTick(() => {
onContainerScroll();
});
});
onMounted(() => {
setTimeout(() => {
watch(
[scrollBarWidth, isActive],
() => {
onContainerScroll();
},
{ immediate: true, flush: 'post' },
);
});
});
watch(
() => props.container,
() => {
onScrollListener?.remove();
onScrollListener = addEventListenerWrap(
props.container,
'scroll',
onContainerScroll,
false,
);
},
{ immediate: true, flush: 'post' },
);
onBeforeUnmount(() => {
onMouseUpListener?.remove();
onMouseMoveListener?.remove();
onScrollListener?.remove();
onResizeListener?.remove();
});
watch(
() => ({ ...scrollState.value }),
(newState, preState) => {
if (
newState.isHiddenScrollBar !== preState?.isHiddenScrollBar &&
!newState.isHiddenScrollBar
) {
setScrollState(state => {
const bodyNode = props.scrollBodyRef.value;
if (!bodyNode) {
return state;
}
return {
...state,
scrollLeft: (bodyNode.scrollLeft / bodyNode.scrollWidth) * bodyNode.clientWidth,
};
});
}
},
{ immediate: true },
);
const scrollbarSize = getScrollBarSize();
return () => {
if (
bodyScrollWidth.value <= bodyWidth.value ||
!scrollBarWidth.value ||
scrollState.value.isHiddenScrollBar
) {
return null;
}
const { prefixCls } = tableContext;
return (
<div
style={{
height: `${scrollbarSize}px`,
width: `${bodyWidth.value}px`,
bottom: `${props.offsetScroll}px`,
}}
class={`${prefixCls}-sticky-scroll`}
>
<div
onMousedown={onMouseDown}
ref={scrollBarRef}
class={classNames(`${prefixCls}-sticky-scroll-bar`, {
[`${prefixCls}-sticky-scroll-bar-active`]: isActive.value,
})}
style={{
width: `${scrollBarWidth.value}px`,
transform: `translate3d(${scrollState.value.scrollLeft}px, 0, 0)`,
}}
/>
</div>
);
};
},
});