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;
} .resize-trigger {
position: absolute;
display: block;
top:;
left:;
height: 100%;
width: 100%;
overflow: hidden;
pointer-events: none;
z-index: -1;
opacity:;
} .scrollbar-container {
position: relative;
overflow-x: hidden!important;
overflow-y: hidden!important;
width: 100%;
height: 100%;
} .scrollbar-container--auto {
overflow-x: visible!important;
overflow-y: visible!important;
} .scrollbar-container .scrollbar-view {
width: 100%;
height: 100%;
-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-vertical,
.scrollbar-container .scrollbar-horizontal {
position: absolute;
opacity:;
cursor: pointer;
transition: opacity 0.25s linear;
background: rgba(0, 0, 0, 0.2);
} .scrollbar-container .scrollbar-vertical {
top:;
left: auto;
right:;
width: 12px;
} .scrollbar-container .scrollbar-horizontal {
top: auto;
left:;
bottom:;
height: 12px;
} .scrollbar-container:hover .scrollbar-vertical,
.scrollbar-container:hover .scrollbar-horizontal,
.scrollbar-container .scrollbar-vertical.scrollbar-show,
.scrollbar-container .scrollbar-horizontal.scrollbar-show {
opacity:;
} .scrollbar-container.cssui-scrollbar--s .scrollbar-vertical {
width: 6px;
} .scrollbar-container.cssui-scrollbar--s .scrollbar-horizontal {
height: 6px;
}

然后,把模板贴出来:

<template>
<div
:style="containerStyle"
:class="containerClass"
@mouseenter="quietUpdate"
@mouseleave="quietOff"
>
<div
ref="scroll"
:style="scrollStyle"
:class="scrollClass"
@scroll.stop.prevent="realUpdate"
>
<div
ref="content"
v-resize="resizeHandle"
>
<slot />
</div>
</div>
<div
v-if="yBarShow"
:style="yBarStyle"
:class="yBarClass"
@mousedown="downVertical"
/>
<div
v-if="xBarShow"
:style="xBarStyle"
:class="xBarClass"
@mousedown="downHorizontal"
/>
</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;
}
},
});

还有用到 tools js中的工具方法:

if (!Date.now) { Date.now = function () { return new Date().getTime(); }; }

const vendors = ['webkit', 'moz'];

if (!window.requestAnimationFrame) {
for (let i = 0; i < vendors.length; ++i) {
const vp = vendors[i];
window.requestAnimationFrame = window[`${vp}RequestAnimationFrame`];
window.cancelAnimationFrame = (window[`${vp}CancelAnimationFrame`] || window[`${vp}CancelRequestAnimationFrame`]);
}
} if (!window.requestAnimationFrame || !window.cancelAnimationFrame) {
let lastTime = 0; window.requestAnimationFrame = callback => {
const now = Date.now();
const nextTime = Math.max(lastTime + 16, now);
return setTimeout(() => { callback(lastTime = nextTime); }, nextTime - now);
}; window.cancelAnimationFrame = clearTimeout;
} let scrollWidth = 0; // requestAnimationFrame 封装
export const ref = (fn) => { window.requestAnimationFrame(fn); }; // 检测 class
export 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
export 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;
}
}; // 获取滚动条宽度
export const getScrollWidth = () => {
if (scrollWidth > 0) { return scrollWidth; } const block = docu.createElement('div');
block.style.cssText = 'position:absolute;top:-1000px;width:100px;height:100px;overflow-y:scroll;';
body.appendChild(block);
const { clientWidth, offsetWidth } = block;
body.removeChild(block);
scrollWidth = offsetWidth - clientWidth; return scrollWidth;
};

下面是 js 功能的部分,代码还是不少,有一些方法做了节流处理,用了一些 lodash 的方法,主要还是上面提到的滚动条计算的原理,大小的计算,具体看  toUpdate  这个方法,位置的计算,主要是 horizontalHandler,verticalHandler,实际滚动距离的计算,看mouseMoveHandler 这个方法:

import { raf, addClass, removeClass, getScrollWidth } from 'src/tools';

const SCROLLBARSIZE = getScrollWidth();

/**
* ----------------------------------------------------------------------------------
* UiScrollBar Component
* ----------------------------------------------------------------------------------
*
* @author zhangmao
* @change 2019/4/15
*/
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 {
enter: false,
yRatio: 0,
xRatio: 0,
lastPageY: 0,
lastPageX: 0,
realWidth: 0,
realHeight: 0,
yBarTop: 0,
yBarHeight: 0,
xBarLeft: 0,
xBarWidth: 0,
scrollWidth: 0,
scrollHeight: 0,
containerWidth: 0,
containerHeight: 0,
cursorDown: false,
};
}, computed: {
xLimit() { return this.width > 0 || this.maxWidth > 0; },
yLimit() { return this.height > 0 || this.maxHeight > 0; },
yBarShow() { return this.getYBarShow(); },
xBarShow() { return this.getXBarShow(); },
yBarStyle() { return { top: `${this.yBarTop}%`, height: `${this.yBarHeight}%` }; },
yBarClass() { return ['scrollbar-vertical', { 'scrollbar-show': this.cursorDown }]; },
xBarStyle() { return { left: `${this.xBarLeft}%`, width: `${this.xBarWidth}%` }; },
xBarClass() { return ['scrollbar-horizontal', { 'scrollbar-show': this.cursorDown }]; },
scrollClass() {
return ['scrollbar-view', {
'scrollbar-view-x': this.xBarShow,
'scrollbar-view-y': this.yBarShow,
}];
},
scrollStyle() {
const hasWidth = this.yBarShow && this.scrollWidth > 0;
const hasHeight = this.xBarShow && this.scrollHeight > 0;
return {
width: hasWidth ? `${this.scrollWidth}px` : '',
height: hasHeight ? `${this.scrollHeight}px` : '',
};
},
containerClass() {
return ['scrollbar-container', {
'cssui-scrollbar--s': this.size === 'small',
'scrollbar-container--auto': !this.xBarShow && !this.yBarShow,
}];
},
containerStyle() {
const showSize = this.xBarShow || this.yBarShow;
const styleObj = {}; if (showSize) {
if (this.containerWidth > 0) { styleObj.width = `${this.containerWidth}px`; }
if (this.containerHeight > 0) { styleObj.height = `${this.containerHeight}px`; }
} return styleObj;
},
}, watch: {
show: 'showChange',
width: 'initail',
height: 'initail',
maxWidth: 'initail',
maxHeight: 'initail',
}, created() {
this.dftData();
this.initEmiter();
}, mounted() { this.$nextTick(this.initail); }, methods: { // ------------------------------------------------------------------------------ // 外部调用方法
refresh() { this.initail(); }, // 手动更新滚动条
scrollX(x) { this.$refs.scroll.scrollLeft = x; },
scrollY(y) { this.$refs.scroll.scrollTop = y; },
scrollTop() { this.$refs.scroll.scrollTop = 0; },
getScrollEl() { return this.$refs.scroll; },
scrollBottom() { this.$refs.scroll.scrollTop = this.$refs.content.offsetHeight; }, // -------------------------------------------------------------------------- quietOff() { this.enter = false; }, // ------------------------------------------------------------------------------ quietUpdate() {
this.enter = true;
this.scrollUpdate();
}, // ------------------------------------------------------------------------------ realUpdate() {
this.quietOff();
this.scrollUpdate();
}, // ------------------------------------------------------------------------------ resizeHandle() { this.initail(); }, // ------------------------------------------------------------------------------ // 默认隐藏 异步展示的情况
showChange(val) { if (val) { this.initail(); } }, // ------------------------------------------------------------------------------ // 组件渲染成功后的入口
initail() {
this.setContainerSize();
this.setScrollSize();
this.setContentSize();
this.realUpdate();
}, // ------------------------------------------------------------------------------ // 设置整个容器的大小
setContainerSize() {
this.setContainerXSize();
this.setContainerYSize();
}, // ------------------------------------------------------------------------------ // 设置滚动容器的大小
setScrollSize() {
this.scrollWidth = this.containerWidth + SCROLLBARSIZE;
this.scrollHeight = this.containerHeight + SCROLLBARSIZE;
}, // ------------------------------------------------------------------------------ // 设置内容区域的大小
setContentSize() {
const realElement = this.$refs.content.firstChild; if (realElement) {
const { offsetWidth = 0, offsetHeight = 0 } = realElement; this.realWidth = this.lodash.round(offsetWidth);
this.realHeight = this.lodash.round(offsetHeight);
}
}, // ------------------------------------------------------------------------------ setContainerXSize() {
if (this.xLimit) {
this.containerWidth = this.width || this.maxWidth;
return;
}
if (this.yLimit) { this.containerWidth = this.lodash.round(this.$el.offsetWidth); }
}, // ------------------------------------------------------------------------------ setContainerYSize() {
if (this.yLimit) {
this.containerHeight = this.height || this.maxHeight;
return;
}
if (this.xLimit) { this.containerHeight = this.lodash.round(this.$el.offsetHeight); }
}, // ------------------------------------------------------------------------------ downVertical(e) {
this.lastPageY = e.pageY;
this.cursorDown = true;
addClass(document.body, 'disable-selection');
document.addEventListener('mousemove', this.moveVertical, false);
document.addEventListener('mouseup', this.upVertical, false);
document.onselectstart = () => false;
return false;
}, // ------------------------------------------------------------------------------ downHorizontal(e) {
this.lastPageX = e.pageX;
this.cursorDown = true;
addClass(document.body, 'disable-selection');
document.addEventListener('mousemove', this.moveHorizontal, false);
document.addEventListener('mouseup', this.upHorizontal, false);
document.onselectstart = () => false;
return false;
}, // ------------------------------------------------------------------------------ moveVertical(e) {
const delta = e.pageY - this.lastPageY;
this.lastPageY = e.pageY; raf(() => { this.$refs.scroll.scrollTop += delta / this.yRatio; });
}, // ------------------------------------------------------------------------------ moveHorizontal(e) {
const delta = e.pageX - this.lastPageX;
this.lastPageX = e.pageX; raf(() => { this.$refs.scroll.scrollLeft += delta / this.xRatio; });
}, // ------------------------------------------------------------------------------ upVertical() {
this.cursorDown = false;
removeClass(document.body, 'disable-selection');
document.removeEventListener('mousemove', this.moveVertical);
document.removeEventListener('mouseup', this.upVertical);
document.onselectstart = null;
}, // ------------------------------------------------------------------------------ upHorizontal() {
this.cursorDown = false;
removeClass(document.body, 'disable-selection');
document.removeEventListener('mousemove', this.moveHorizontal);
document.removeEventListener('mouseup', this.upHorizontal);
document.onselectstart = null;
}, // ------------------------------------------------------------------------------ scrollUpdate() {
const {
clientWidth = 0,
scrollWidth = 0,
clientHeight = 0,
scrollHeight = 0,
} = this.$refs.scroll; this.yRatio = clientHeight / scrollHeight;
this.xRatio = clientWidth / scrollWidth; raf(() => {
if (this.yBarShow) {
this.yBarHeight = Math.max(this.yRatio * 100, 1);
this.yBarTop = this.lodash.round((this.$refs.scroll.scrollTop / scrollHeight) * 100, 2); // 只更新不触发事件
if (this.enter) { return; } const top = this.$refs.scroll.scrollTop;
const left = this.$refs.scroll.scrollLeft;
const cHeight = this.$refs.scroll.clientHeight;
const sHeight = this.$refs.scroll.scrollHeight; // trigger event
this.debounceScroll({ top, left });
if (top === 0) {
this.debounceTop();
} else if (top + cHeight === sHeight) {
this.debounceBottom();
}
} if (this.xBarShow) {
this.xBarWidth = Math.max(this.xRatio * 100, 1);
this.xBarLeft = this.lodash.round((this.$refs.scroll.scrollLeft / scrollWidth) * 100, 2); // 只更新不触发事件
if (this.enter) { return; } const top = this.$refs.scroll.scrollTop;
const left = this.$refs.scroll.scrollLeft;
const cWidth = this.$refs.scroll.clientWidth;
const sWidth = this.$refs.scroll.scrollWidth; // trigger event
this.debounceScroll({ top, left });
if (left === 0) {
this.debounceLeft();
} else if (left + cWidth === sWidth) {
this.debounceRight();
}
}
});
}, // ------------------------------------------------------------------------------ dftData() {
this.debounceLeft = null;
this.debounceRight = null;
this.debounceTop = null;
this.debounceBottom = null;
this.debounceScroll = null;
}, // ------------------------------------------------------------------------------ // 初始化触发事件
initEmiter() {
this.turnOn('winResize', this.initail);
this.debounceTop = this.lodash.debounce(() => this.$emit('top'), 500);
this.debounceLeft = this.lodash.debounce(() => this.$emit('left'), 500);
this.debounceRight = this.lodash.debounce(() => this.$emit('right'), 500);
this.debounceBottom = this.lodash.debounce(() => this.$emit('bottom'), 500);
this.debounceScroll = this.lodash.debounce(obj => this.$emit('scroll', obj), 250);
}, // ------------------------------------------------------------------------------ // 是否展示垂直的滚动条
getYBarShow() {
if (this.yLimit) {
if (this.height > 0) { return this.realHeight > this.height; }
if (this.maxHeight > 0) { return this.realHeight > this.maxHeight; }
return this.realHeight > this.containerHeight;
}
return false;
}, // ------------------------------------------------------------------------------ // 是否展示横向的滚动条
getXBarShow() {
if (this.xLimit) {
if (this.width > 0) { return this.realWidth > this.width; }
if (this.maxWidth > 0) { return this.realWidth > this.maxWidth; }
return this.realWidth > this.containerWidth;
}
return false;
}, // ------------------------------------------------------------------------------ },
};

使用 Vue 开发 scrollbar 滚动条组件的更多相关文章

  1. 使用vue开发输入型组件更好的一种解决方式(子组件向父组件传值,基于2.2.0)

    (本人想封装一个带有input输入框的组件) 之前使用vue开发组件的时候,在遇到子组件向父组件传递值时我采用的方法是这样的: 比如子组件是一个输入框,父组件调用时需要获取到子组件输入的值,子组件通过 ...

  2. vue开发可复用组件

    组件,是一个具有一定功能,且不同组件间功能相对独立的模块.高内聚.低耦合.   开发可复用性的组件应遵循以下原则:   1.规范化命名:组件的命名应该跟业务无关,而是依据组件的功能命名. 2.数据扁平 ...

  3. 【vue开发】 父组件传值给子组件时 ,watch props 监听不到解决方案

    解决方案: watch:{ data:{ immediate:true, handler:function(){ } }} 示例:  

  4. Vue.js 桌面端自定义滚动条组件|vue美化滚动条VScroll

    基于vue.js开发的小巧PC端自定义滚动条组件VScroll. 前段时间有给大家分享一个vue桌面端弹框组件,今天再分享最近开发的一个vue pc端自定义滚动条组件. vscroll 一款基于vue ...

  5. vue(9)—— 组件化开发 - webpack(3)

    前面两个终于把webpack相关配置解析完了.现在终于进入vue的开发了 vue组件化开发预热 前期准备 创建如下项目: app.js: footer.js: main.js: webpack.con ...

  6. vue(8)—— 组件化开发 - webpack(2)

    webpack的常用loder和插件 loder和插件是什么,现在暂且不表,看到后面你就懂了 引入css问题 直接用link标签导入css 在前面的 vue(7)—— 组件化开发 — webpack( ...

  7. vue(7)—— 组件化开发 — webpack(1)

    引子 在研究完前面的vue开发后,其实已经可以自己开发点东西了,靠前面的指令集,组件,还有vue-router,还有异步请求这些知识点,是完全可以开发出来的,完全可以达到时下前后端分离的效果. 但是, ...

  8. 基于Vue的数字输入框组件开发

    1.概述 Vue组件开发的API:props.events和slots 2.组件代码 github地址:https://github.com/MengFangui/VueInputNumber 效果: ...

  9. 基于Vue开发的tab切换组件

    github地址:https://github.com/MengFangui/VueTabSwitch 1.index.html <!DOCTYPE html> <html lang ...

随机推荐

  1. WinForm时间选择控件(DateTimePicker)如何选择(显示)时分秒

    C# Windows窗体应用中,用到时间选择控件DateTimePicker,发现不能选择时分秒,难道要自己写一个控件?! 答案是否定的,通过属性修改是可以选择时间的,DateTimePicker完全 ...

  2. 巩固java(七)-----java反射机制

    一般而言,开发者社群说到动态语言,大致认同的一个定义是:"程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言".从这个观点看,Perl,Python,Ruby是动态语言, ...

  3. apigateway-kong(一)简介及部署

    时隔三年,本人重出江湖,哈哈哈 浏览之前写的博客,有些深度还不是太够.篇幅太短,并且很多专题没有坚持写下去,部分技(dai)术(ma)没有从业务中抽离出来,本人感觉好遗憾--为此,痛下决心,重拾博客, ...

  4. util.go 源码阅读

        }     h := md5.New()     baseString, _ := json.Marshal(obj)     h.Write([]byte(baseString))      ...

  5. bzoj 2002 弹飞绵羊 分块

    正解lct,然而本蒟蒻并不会.... 分块思路很清晰,处理出每个点弹出所在块所需要的步数及出去后的第一个位置 #include<cstdio> #include<cstring> ...

  6. 阿里巴巴Java开发程序猿年薪40W是什么水平?

    对于年薪40万的程序员,不只是技术过硬,还有一个原因是他们所在的公司福利高,或者会直接持股.在BAT中就是一个很好的案例,例如阿里巴巴P7,P8级别的员工不仅是年薪30到80万不等,还有更多股票持有. ...

  7. 如何查找元素对应事件的js代码,检测定位js事件

    比如一张图片当鼠标放到上面时,图片改变.想找到这个事件对应的js代码,假设另存为html之后,文件夹中有.js文件. 如果你会调试,可以用打开浏览器的调试功能,以chrome为例,按F12打开调试窗口 ...

  8. 分析Class类和ClassLoader类下的同名方法getResourceAsStream

    在读取本地资源的时候我们经常需要用到输入流,典型的场景就是使用Druid连接池时读取连接池的配置文件.Java为我们提供了读取资源的方法getResourceAsStream(),该方法有三种: Cl ...

  9. 5.1基于JWT的认证和授权「深入浅出ASP.NET Core系列」

    希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注. Cookie-Based认证 认证流程 我们先看下传统Web端的认 ...

  10. eclipse中运行出错:无法初始化主类的解决办法

    问题描述:eclipse中运行程序时,出现如下错误 解决办法: 出现此类:无法初始化主类有可能是因为eclipse中Java的版本与JDK的版本不匹配,我开始用的时候eclipse中用的是Java s ...