Vue 应该说是很火的一款前端库了,和 React 一样的高热度,今天就来用它写一个轻量的滚动条组件;
知识储备:要开发滚动条组件,需要知道知识点是如何计算滚动条的大小和位置,还有一个问题是如何监听容器大小的改变,然后更新滚动条的位置;
先把样式贴出来:
/*禁用选择文本*/ .disable-selection { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } /*object 触发器的样式*/ .resize-trigger { position: absolute; display: block; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1; opacity: 0; } .scrollbar-container { position: relative; overflow: hidden !important; width: 100%; height: 100%; } .scrollbar-box { position: absolute; right: 0; bottom: 0; z-index: 1; } .scrollbar-box.scrollbar-box-vertical { width: 12px; top: 0; } .scrollbar-box.scrollbar-box-horizontal { height: 12px; left: 0; } .cssui-scrollbar--s .scrollbar-box.scrollbar-box-vertical { width: 6px; } .cssui-scrollbar--s .scrollbar-box.scrollbar-box-horizontal { height: 6px; } .scrollbar-box .scrollbar-thumb { position: relative; display: block; cursor: pointer; background-color: rgba(0, 0, 0, 0.2); transform: translate3d(0, 0, 0); } .scrollbar-box .scrollbar-thumb:hover, .scrollbar-box .scrollbar-thumb:active { background-color: rgba(0, 0, 0, 0.3); } .scrollbar-box.scrollbar-box-vertical .scrollbar-thumb { width: 100%; } .scrollbar-box.scrollbar-box-horizontal .scrollbar-thumb { height: 100%; } .scrollbar-container .scrollbar-view { width: 100%; height: 100%; transform: translate3d(0, 0, 0); -webkit-overflow-scrolling: touch; } .scrollbar-container .scrollbar-view-x { overflow-x: scroll!important; } .scrollbar-container .scrollbar-view-y { overflow-y: scroll!important; } .scrollbar-container.scrollbar-autoshow .scrollbar-box { opacity: 0; transition: opacity 120ms ease-out; } .scrollbar-container.scrollbar-autoshow:hover > .scrollbar-box, .scrollbar-container.scrollbar-autoshow:active > .scrollbar-box, .scrollbar-container.scrollbar-autoshow:focus > .scrollbar-box { opacity: 1; transition: opacity 340ms ease-out; }
然后,把模板贴出来:
<template> <div :style="containerStyle" :class="containerClass" > <div ref="scrollEl" :style="scrollStyle" :class="scrollClass" @scroll.stop.prevent="scrollHandler" > <div ref="contentEl" v-resize="resizeHandle" > <slot /> </div> </div> <div v-if="yBarShow" ref="vertical" class="scrollbar-box scrollbar-box-vertical" @mousedown="verticalHandler" > <div ref="verticalBar" :style="yBarStyle" class="scrollbar-thumb" @mousedown="verticalBarHandler" /> </div> <div v-if="xBarShow" ref="horizontal" class="scrollbar-box scrollbar-box-horizontal" @mousedown="horizontalHandler" > <div ref="horizontalBar" :style="xBarStyle" class="scrollbar-thumb" @mousedown="horizontalBarHandler" /> </div> </div> </template>
上面的代码中,我用到了 v-resize 这个指令,这个指令就是封装容器大小改变时,向外触发事件的,看到网上有通过 MutationObserver 来监听的,这个问题是监听所有的属性变化,好像还有兼容问题,还有一种方案是用 GitHub 的这个库:resize-observer-polyfill,上面的这些方法都可以,我也是尝试了一下,但我觉得始终是有点小题大做了,不如下面这个方法好,就是创建一个看不见的 object 对象,然后使它的绝对定位,相对于滚动父容器,和滚动条容器的大小保持一致,监听 object 里面 window 对象的 resize 事件,这样就可以做到实时响应高度变化了,贴上代码:
import Vue from ‘vue‘; import { throttle, isFunction } from ‘lodash‘; Vue.directive(‘resize‘, { inserted(el, { value: handle }) { if (!isFunction(handle)) { return; } const aimEl = el; const resizer = document.createElement(‘object‘); resizer.type = ‘text/html‘; resizer.data = ‘about:blank‘; resizer.setAttribute(‘tabindex‘, ‘-1‘); resizer.setAttribute(‘class‘, ‘resize-trigger‘); resizer.onload = () => { const win = resizer.contentDocument.defaultView; win.addEventListener(‘resize‘, throttle(() => { const rect = el.getBoundingClientRect(); handle(rect); }, 500)); }; aimEl.style.position = ‘relative‘; aimEl.appendChild(resizer); aimEl.resizer = resizer; }, unbind(el) { const aimEl = el; if (aimEl.resizer) { aimEl.style.position = ‘‘; aimEl.removeChild(aimEl.resizer); delete aimEl.resizer; } }, });
下面是 js 功能的部分,代码还是不少,有一些方法做了节流处理,用了一些 lodash 的方法,主要还是上面提到的滚动条计算的原理,大小的计算,具体看 toUpdate 这个方法,位置的计算,主要是 horizontalHandler,verticalHandler,实际滚动距离的计算,看mouseMoveHandler 这个方法:
import {trim, delay, round, throttle } from "lodash"; // ------------------------------------------------------------------------------ // 检测 class const hasClass = (el = null, cls = ‘‘) => { if (!el || !cls) { return false; } if (cls.indexOf(‘ ‘) !== -1) { throw new Error(‘className should not contain space.‘); } if (el.classList) { return el.classList.contains(cls); } return ` ${el.className} `.indexOf(` ${cls} `) > -1; }; // ------------------------------------------------------------------------------ // 添加 class const addClass = (element = null, cls = ‘‘) => { const el = element; if (!el) { return; } let curClass = el.className; const classes = cls.split(‘ ‘); for (let i = 0, j = classes.length; i < j; i += 1) { const clsName = classes[i]; if (!clsName) { continue; } if (el.classList) { el.classList.add(clsName); } else if (!hasClass(el, clsName)) { curClass += ‘ ‘ + clsName; } } if (!el.classList) { el.className = curClass; } }; // ------------------------------------------------------------------------------ // 删除 class const removeClass = (element, cls) => { const el = element; if (!el || !cls) { return; } const classes = cls.split(‘ ‘); let curClass = ` ${el.className} `; for (let i = 0, j = classes.length; i < j; i += 1) { const clsName = classes[i]; if (!clsName) { continue; } if (el.classList) { el.classList.remove(clsName); } else if (hasClass(el, clsName)) { curClass = curClass.replace(` ${clsName} `, ‘ ‘); } } if (!el.classList) { el.className = trim(curClass); } }; // ------------------------------------------------------------------------------ // 获取滚动条宽度 let scrollWidth = 0; const getScrollWidth = () => { if (scrollWidth > 0) { return scrollWidth; } const block = document.createElement(‘div‘); block.style.cssText = ‘position:absolute;top:-1000px;width:100px;height:100px;overflow-y:scroll;‘; document.body.appendChild(block); const { clientWidth, offsetWidth } = block; document.body.removeChild(block); scrollWidth = offsetWidth - clientWidth; return scrollWidth; }; // scrollSize 值 const SCROLLBARSIZE = getScrollWidth(); /** * UiScrollbar Component * @author zhangmao 19/4/3 */ export default { name: ‘UiScrollbar‘, props: { size: { type: String, default: ‘normal‘ }, // small // 主要是为了解决在 dropdown 隐藏的情况下无法获取当前容器的真实 width height 的问题 show: { type: Boolean, default: false }, width: { type: Number, default: 0 }, height: { type: Number, default: 0 }, maxWidth: { type: Number, default: 0 }, maxHeight: { type: Number, default: 0 }, }, data() { return { prevPageX: 0, // 缓存的鼠标横向位置 prevPageY: 0, // 缓存的鼠标垂直位置 cursorDown: false, // 鼠标拖拽标记 minBarSize: 5, // 滚动条的最小快读和高度 xScroll: 0, // 当前滚动条的横向位置 yScroll: 0, // 当前滚动条的垂直位置 realWidth: 0, // 内容的真实宽度 realHeight: 0, // 内容的真实高度 xBarWidth: 0, // 水平滚动条发宽度 yBarHeight: 0, // 垂直滚动条的高度 xBarLastWidth: 0, // 水平滚动条的最终宽度 yBarLastHeight: 0, // 垂直滚动条最终的高度 containerWidth: 0, // 容器的宽度 containerHeight: 0, // 容器的高度 scrollWidth: 0, // 滚动容器的宽度 scrollHeight: 0, // 滚动容器的高度 scrollTopMax: 0, // 垂直最大滚动距离限制 scrollLeftMax: 0, // 水平最大滚动距离限制 trackTopMax: 0, // 垂直步长最大限制 trackLeftMax: 0, // 水平步长最大限制 }; }, computed: { yBarShow() { return this.getYBarShow(); }, xBarShow() { return this.getXBarShow(); }, yBarStyle() { return { height: `${this.yBarLastHeight}px`, msTransform: `translateY(${this.yScroll}px)`, webkitTransform: `translate3d(0, ${this.yScroll}px, 0)`, transform: `translate3d(0, ${this.yScroll}px, 0)`, }; }, xBarStyle() { return { width: `${this.xBarLastWidth}px`, msTransform: `translateX(0, ${this.xScroll}px, 0)`, webkitTransform: `translate3d(${this.xScroll}px, 0, 0)`, transform: `translate3d(${this.xScroll}px, 0, 0)`, }; }, scrollClass() { return [‘scrollbar-view‘, { ‘scrollbar-view-x‘: this.xBarShow, ‘scrollbar-view-y‘: this.yBarShow, }]; }, scrollStyle() { // 注意这里是相反的 const hasWidth = this.yBarShow || this.realWidth > this.containerWidth; const hasHeight = this.xBarShow || this.realHeight > this.containerHeight; return { width: hasWidth && this.scrollWidth > 0 ? `${this.scrollWidth}px` : ‘‘, height: hasHeight && this.scrollHeight > 0 ? `${this.scrollHeight}px` : ‘‘, }; }, containerClass() { return [‘scrollbar-container scrollbar-autoshow‘, { ‘cssui-scrollbar--s‘: this.size === ‘small‘, }]; }, containerStyle() { if (this.xBarShow || this.yBarShow) { return { width: this.containerWidth > 0 ? `${this.containerWidth}px` : ‘‘, height: this.containerHeight > 0 ? `${this.containerHeight}px` : ‘‘, }; } return {}; }, }, watch: { show: ‘showChange‘, width: ‘initail‘, height: ‘initail‘, maxWidth: ‘initail‘, maxHeight: ‘initail‘, }, created() { this.dftData(); this.initEvent(); }, mounted() { this.delayInit(); }, methods: { // ------------------------------------------------------------------------------ // 外部调用方法 scrollX(x) { this.$refs.scrollEl.scrollLeft = x; }, scrollY(y) { this.$refs.scrollEl.scrollTop = y; }, scrollTop() { this.$refs.scrollEl.scrollTop = 0; }, scrollBottom() { this.$refs.scrollEl.scrollTop = this.$refs.contentEl.offsetHeight; }, // ------------------------------------------------------------------------------ // 默认隐藏 异步展示的情况 showChange(val) { if (val) { this.delayInit(); } }, // ------------------------------------------------------------------------------ delayInit() { this.$nextTick(() => { delay(() => { this.initail(); }, 10); }); }, // ------------------------------------------------------------------------------ // 检测是否需要展示垂直的滚动条 getYBarShow() { if (this.height > 0) { return this.realHeight > this.height; } if (this.maxHeight > 0) { return this.realHeight > this.maxHeight; } return this.realHeight > this.containerHeight; }, // ------------------------------------------------------------------------------ // 检测是否需要展示横向的滚动条 getXBarShow() { if (this.width > 0) { return this.realWidth > this.width; } if (this.maxWidth > 0) { return this.realWidth > this.maxWidth; } return this.realWidth > this.containerWidth; }, // ------------------------------------------------------------------------------ // 内容大小改变 resizeHandle({ width, height }) { this.realWidth = width; this.realHeight = height; this.delayInit(); }, // ------------------------------------------------------------------------------ // 设置容器大小 初始化滚动条位置 initail() { this.setContainerSize(); this.setScrollSize(); this.setContentSize(); this.toUpdate(); }, // ------------------------------------------------------------------------------ // 设置整个容器的大小 setContainerSize() { const { offsetWidth = 0, offsetHeight = 0 } = this.$el; this.containerHeight = this.height || this.maxHeight || offsetHeight; this.containerWidth = this.width || this.maxWidth || offsetWidth; }, // ------------------------------------------------------------------------------ // 设置滚动容器的大小 setScrollSize() { this.scrollWidth = this.containerWidth + SCROLLBARSIZE; this.scrollHeight = this.containerHeight + SCROLLBARSIZE; }, // ------------------------------------------------------------------------------ // 设置内容区域的大小 setContentSize() { if (this.$refs.contentEl) { const { offsetWidth = 0, offsetHeight = 0 } = this.$refs.contentEl; this.realWidth = offsetWidth; this.realHeight = offsetHeight; } }, // ------------------------------------------------------------------------------ // 更新滚动条相关的大小位置 toUpdate() { if (this.realWidth > 0) { // 水平滚动条的宽度 this.xBarWidth = round(this.containerWidth / this.realWidth * this.containerWidth); this.scrollLeftMax = this.realWidth - this.containerWidth; } if (this.realHeight > 0) { // 垂直方向滚动条的高度 this.yBarHeight = round(this.containerHeight / this.realHeight * this.containerHeight); this.scrollTopMax = this.realHeight - this.containerHeight; } // 设置滚动条最终的大小 this.xBarLastWidth = Math.max(this.xBarWidth, this.minBarSize); this.yBarLastHeight = Math.max(this.yBarHeight, this.minBarSize); this.trackTopMax = this.containerHeight - this.yBarLastHeight; this.trackLeftMax = this.containerWidth - this.xBarLastWidth; this.scrollHandler(); }, // ------------------------------------------------------------------------------ scrollHandler() { if (this.$refs.scrollEl) { const { scrollLeft = 0, scrollTop = 0, clientHeight = 0, scrollHeight = 0, clientWidth = 0, scrollWidth = 0, } = this.$refs.scrollEl; this.xScroll = round(scrollLeft * this.trackLeftMax / this.scrollLeftMax) || 0; this.yScroll = round(scrollTop * this.trackTopMax / this.scrollTopMax) || 0; this.triggerEvent(scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight); } return false; }, // ------------------------------------------------------------------------------ // 触发事件 triggerEvent(sLeft, sTop, sWidth, sHeight, cWidth, cHeight) { this.throttledScroll(); if (this.xBarShow) { if (sLeft === 0) { this.throttleLeft(); } else if (sLeft + cWidth === sWidth) { this.throttleRight(); } } if (this.yBarShow) { if (sTop === 0) { this.throttleTop(); } else if (sTop + cHeight === sHeight) { this.throttleBottom(); } } }, // ------------------------------------------------------------------------------ verticalHandler({ target, currentTarget, offsetY }) { if (target !== currentTarget) { return; } const offset = offsetY - this.yBarHeight / 2; const barTop = offset / this.containerHeight * 100; this.$refs.scrollEl.scrollTop = round(barTop * this.realHeight / 100); }, // ------------------------------------------------------------------------------ horizontalHandler({ target, currentTarget, offsetX }) { if (target !== currentTarget) { return; } const offset = offsetX - this.xBarWidth / 2; const barLeft = offset / this.containerWidth * 100; this.$refs.scrollEl.scrollLeft = round(barLeft * this.realWidth / 100); }, // ------------------------------------------------------------------------------ verticalBarHandler(e) { this.startDrag(); this.prevPageY = this.yBarLastHeight - e.offsetY; }, // ------------------------------------------------------------------------------ horizontalBarHandler(e) { this.startDrag(); this.prevPageX = this.xBarLastWidth - e.offsetX; }, // ------------------------------------------------------------------------------ startDrag() { this.cursorDown = true; addClass(document.body, ‘disable-selection‘); document.addEventListener(‘mousemove‘, this.throttleMoving, false); document.addEventListener(‘mouseup‘, this.mouseUpHandler, false); document.onselectstart = () => false; }, // ------------------------------------------------------------------------------ mouseUpHandler() { this.cursorDown = false; this.prevPageY = 0; this.prevPageX = 0; removeClass(document.body, ‘disable-selection‘); document.removeEventListener(‘mousemove‘, this.throttleMoving); document.removeEventListener(‘mouseup‘, this.mouseUpHandler); document.onselectstart = null; }, // ------------------------------------------------------------------------------ mouseMoveHandler({ clientY, clientX }) { let offset; let barPosition; if (this.yBarShow && this.prevPageY) { offset = clientY - this.$refs.vertical.getBoundingClientRect().top; barPosition = this.yBarLastHeight - this.prevPageY; const top = this.scrollTopMax * (offset - barPosition) / this.trackTopMax; this.$refs.scrollEl.scrollTop = round(top); } if (this.xBarShow && this.prevPageX) { offset = clientX - this.$refs.horizontal.getBoundingClientRect().left; barPosition = this.xBarLastWidth - this.prevPageX; const left = this.scrollLeftMax * (offset - barPosition) / this.trackLeftMax; this.$refs.scrollEl.scrollLeft = round(left); } }, // ------------------------------------------------------------------------------ dftData() { this.throttledScroll = null; this.throttleLeft = null; this.throttleRight = null; this.throttleTop = null; this.throttleBottom = null; this.throttleMoving = throttle(this.mouseMoveHandler, 10); }, // ------------------------------------------------------------------------------ // 注册事件 initEvent() { const opt = { trailing: false }; this.turnOn(‘winResize‘, this.initail); this.throttleTop = throttle(() => this.$emit(‘top‘), 1000, opt); this.throttleLeft = throttle(() => this.$emit(‘left‘), 1000, opt); this.throttleRight = throttle(() => this.$emit(‘right‘), 1000, opt); this.throttleBottom = throttle(() => this.$emit(‘bottom‘), 1000, opt); this.throttledScroll = throttle(() => this.$emit(‘scroll‘), 1000, opt); }, // ------------------------------------------------------------------------------ }, };
原文地址:https://www.cnblogs.com/zhangmao/p/10659503.html
时间: 2024-10-10 05:43:54