大家好!我叫戴向天
QQ群:602504799
如若有不理解的,可加QQ群进行咨询了解
<template>
<div class="y-scrollbar" ref="box">
<div
class="y-scrollbar__wrap"
ref="wrap"
:style="wrapStyle"
@scroll="handlerScroll"
@mouseenter="update"
>
<div class="y-scrollbar__view" ref="resize">
<slot />
</div>
</div>
<div class="y-scrollbar__bar is-horizontal">
<div
class="y-scrollbar__thumb"
:style="{ height: sizeWidth, transform: `translateX(${moveX}%)` }"
ref="horizontal"
tabindex="1"
@mousedown="thumbHandler($event, 'horizontal')"
></div>
</div>
<div class="y-scrollbar__bar is-vertical">
<div
class="y-scrollbar__thumb"
:style="{ height: sizeHeight, transform: `translateY(${moveY}%)` }"
ref="vertical"
tabindex="1"
@mousedown="thumbHandler($event, 'vertical')"
></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, getCurrentScope, nextTick, onBeforeUnmount, onMounted, ref, toRaw } from "vue";
interface IScrollbar {
native?: boolean;
noResize?: boolean;
}
// 获取当前浏览器滚动条的宽度
function getScrollbarWidth() {
const outer = document.createElement('div') as HTMLDivElement;
outer.className = 'el-scrollbar__wrap';
outer.style.visibility = 'hidden';
outer.style.width = '100px';
outer.style.position = 'absolute';
outer.style.top = '-9999px';
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
if (outer.parentNode) {
outer.parentNode.removeChild(outer);
}
return widthNoScroll - widthWithScroll;
}
// dom节点监听封装
function addObserver() {
let observer: MutationObserver | null = null
let timer = 0
return (element: HTMLElement, callBack?: () => void) => {
// 监听的配置选项
const config = { attributes: true, childList: true, subtree: true };
if (observer) {
observer.disconnect();
}
observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (['childList', 'attributes'].includes(mutation.type)) {
if (callBack) {
clearTimeout(timer)
timer = setTimeout(callBack, 50)
}
}
}
});
observer.observe(element, config);
return observer
}
}
export default defineComponent({
name: 'Scrollbar',
props: {
native: {
type: Boolean,
default: false,
},
noResize: {
type: Boolean,
default: false,
}
},
setup(props: IScrollbar, context) {
const $props = toRaw(props)
// 滚动条的宽度
const sizeWidth = ref('0')
// 滚动条的高度
const sizeHeight = ref('0')
// 纵向滚动体的偏移量
const moveY = ref(0)
// 横向滚动条的偏移量
const moveX = ref(0)
// 滚动条容器
const wrap = ref<HTMLElement>()
// 滚动条内容容器
const resize = ref<HTMLElement>()
// 横向滚动条
const horizontal = ref<HTMLElement>()
// 纵向滚动条
const vertical = ref<HTMLElement>()
const isMouseDown = ref(false)
// 获取当前浏览器滚动条的宽度
const scrollbarWidth = computed(getScrollbarWidth)
const gutterWith = computed(() => `-${scrollbarWidth.value}px`)
const wrapStyle = computed(() => `margin-bottom: ${gutterWith.value}; margin-right: ${gutterWith.value};`)
const observe = ref<MutationObserver | null>(null);
// 突变观察者 主要是用来观察内容变化
const observeHandler = addObserver()
// 更新滚动条
const update = () => {
const wrapEl = wrap.value
if (!wrapEl) {
return false
}
const heightPercentage = (wrapEl.clientHeight * 100) / wrapEl.scrollHeight;
const widthPercentage = (wrapEl.clientWidth * 100) / wrapEl.scrollWidth;
sizeHeight.value = heightPercentage < 100 ? `${heightPercentage}%` : '';
sizeWidth.value = widthPercentage < 100 ? `${widthPercentage}%` : '';
}
// 容器滚动的时候触发
const handlerScroll = () => {
const wrapEl = wrap.value
if (!wrapEl) {
return false
}
moveY.value = (wrapEl.scrollTop * 100) / wrapEl.clientHeight;
moveX.value = (wrapEl.scrollLeft * 100) / wrapEl.clientWidth;
}
// 滚动条按住拖拽触发
const thumbHandler = (event: MouseEvent, direction: string) => {
isMouseDown.value = true
const dom = direction === 'horizontal' ? horizontal.value : vertical.value
const wrapEl = wrap.value
if (!dom || !wrapEl) return
// 鼠标移动方向值
const name = direction === 'horizontal' ? 'pageX' : 'pageY'
// 滚动条的方向
const scrollDirection = direction === 'horizontal' ? 'scrollLeft' : 'scrollTop'
// 容器的宽或高
const size = direction === 'horizontal' ? 'clientWidth' : 'clientHeight'
// 滚动条的宽或高
const scrollSize = direction === 'horizontal' ? 'scrollWidth' : 'scrollHeight'
// 滚动条方向的距离
const scrollDirectionSize = wrapEl[scrollDirection]
// 鼠标起始位置
const start = event[name]
// 虚拟滚动动条总路程
const totalTop = wrapEl[size] - dom[size]
// 滚动条实际的最高值
const offsetTotalTop = wrapEl[scrollSize] - wrapEl[size]
// 鼠标每一拖拽1px的实际滚动调的高度
const progress = offsetTotalTop / totalTop
// 鼠标移动事件
const moveHandler = (e: MouseEvent) => {
const end = e[name]
let top = scrollDirectionSize + (end - start) * progress
if (top + wrapEl[size] >= wrapEl[scrollSize]) {
top = wrapEl[scrollSize] - wrapEl[size]
} else if (top <= 0) {
top = 0
}
wrapEl[scrollDirection] = top
}
// 移除监听事件
const upHandler = () => {
isMouseDown.value = false
dom.removeEventListener('blur', upHandler, false)
document.body.removeEventListener('mousemove', moveHandler, false)
document.body.removeEventListener('mouseup', upHandler, false)
}
// 先进行移除所有事件,防止二次绑定
upHandler()
// body绑定鼠标移动事件
document.body.addEventListener('mousemove', moveHandler, false)
// body绑定鼠标抬起事件
document.body.addEventListener('mouseup', upHandler, false)
// 当前dom进行获取焦点 - 主要是防止在拖拽的时候进行了切换窗口啥的,然后鼠标的移动事件还一直在
dom.focus()
// dom绑定失去焦点事件
dom.addEventListener('blur', upHandler, false)
}
onMounted(() => {
if ($props.native) return
if (resize.value && !$props.noResize) {
observe.value = observeHandler(resize.value, () => {
update()
handlerScroll()
})
}
})
onBeforeUnmount(() => {
if ($props.native) return
if (observe.value) {
observe.value.disconnect();
}
})
return {
resize,
wrap,
horizontal,
vertical,
sizeHeight,
sizeWidth,
moveY,
moveX,
wrapStyle,
handlerScroll,
update,
thumbHandler
}
}
})
</script>
<style lang="scss" scoped>
.y-scrollbar {
overflow: hidden;
position: relative;
&:hover,
&:active {
& > .y-scrollbar__bar {
opacity: 1;
transition: opacity 0.34s ease-out;
}
}
&__wrap {
overflow: scroll;
overflow-x: auto;
height: 100%;
}
&__bar {
position: absolute;
right: 2px;
bottom: 2px;
z-index: 1;
border-radius: 4px;
opacity: 0;
transition: opacity 0.12s ease-out;
&.is-horizontal {
height: 6px;
left: 2px;
& > div {
height: 100%;
}
}
&.is-vertical {
width: 6px;
top: 2px;
& > div {
width: 100%;
}
}
}
&__thumb {
position: relative;
display: block;
width: 0;
height: 0;
cursor: pointer;
border-radius: inherit;
background-color: rgba(144, 147, 153, 0.3);
transition: background-color 0.3s;
}
}
</style>
|