标签:通过 poi pre 内容 zha 网上 对象 one 触发器
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);
},
// ------------------------------------------------------------------------------
},
};
标签:通过 poi pre 内容 zha 网上 对象 one 触发器
原文地址:https://www.cnblogs.com/zhangmao/p/10659503.html