Vue3 + TS Scrollbar组件 - 戴向天

大家好!我叫戴向天

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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值