123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- /*!
- * x-scrollbar 自定义滚动条插件
- * 版本: v3.1.0
- * 作者: 清晨的阳光(QQ:765550360)
- * 许可: MIT
- * https://gitee.com/xujz520/x-scrollbar
- */
- import './index.css'
- class XScrollbar {
- constructor(dom, options) {
- this.$dom = dom
- if (this.$dom.classList.contains('x-scrollbar')) return
- this.$dom.classList.add('x-scrollbar')
- // 移动端检测
- this.isMobile = window.navigator.userAgent.toLowerCase().includes('mobile')
- // 合并配置
- const defaultOptions = {
- // 响应容器和内容大小改变(自动更新滚动条)
- autoUpdate: true,
- // 阻止向上传递滚动事件
- preventDefault: true,
- // 仅水平滚动(拨动鼠标滚轮时将作用于X轴)
- onlyHorizontal: false,
- // 自动隐藏
- autoHide: true,
- }
- const defaultStyle = {
- // 滑块大小
- thumbSize: '5px',
- // 轨道颜色
- trackBackground: '#ddd',
- // 滑块颜色
- thumbBackground: '#5f5f5f',
- // 滑块圆角大小
- thumbRadius: '5px',
- }
- Object.assign(this, defaultOptions, defaultStyle, options)
- // 构造dom
- const scrollLeft = this.$dom.scrollLeft
- const scrollTop = this.$dom.scrollTop
- this.$container = this.html2dom('<div class="x-scrollbar__container"></div>')
- this.$content = this.html2dom('<div class="x-scrollbar__content"></div>')
- this.$trackX = this.html2dom('<div class="x-scrollbar__track-x"></div>')
- this.$trackY = this.html2dom('<div class="x-scrollbar__track-y"></div>')
- this.$thumbX = this.html2dom('<div class="x-scrollbar__thumb-x"></div>')
- this.$thumbY = this.html2dom('<div class="x-scrollbar__thumb-y"></div>')
- this.$trackX.appendChild(this.$thumbX)
- this.$trackY.appendChild(this.$thumbY)
- const childNodes = []
- Array.prototype.forEach.call(this.$dom.childNodes, node => {
- childNodes.push(node)
- })
- childNodes.forEach(node => {
- this.$content.appendChild(node)
- })
- this.$container.appendChild(this.$content)
- this.$dom.appendChild(this.$container)
- // 处理内边距
- const styleObj = getComputedStyle(this.$dom)
- const padding = `${styleObj.paddingTop} ${styleObj.paddingRight} ${styleObj.paddingBottom} ${styleObj.paddingLeft}`
- if (padding != '0px 0px 0px 0px') {
- this.$dom.style.padding = '0px 0px 0px 0px'
- this.$container.style.padding = padding
- }
- // 设置初始值
- this.$container.scrollLeft = scrollLeft
- this.$container.scrollTop = scrollTop
- if (this.preventDefault) {
- this.$container.classList.add('x-scrollbar__container--preventDefault')
- }
- if (this.isMobile) return
- this.$dom.appendChild(this.$trackX)
- this.$dom.appendChild(this.$trackY)
- this.$container.classList.add('x-scrollbar__container--hideScrollbar')
- if (JSON.stringify(defaultStyle) != JSON.stringify(Object.keys(defaultStyle).reduce((obj, k) => ({ ...obj, [k]: this[k] }), {}))) {
- this.style()
- }
- // 自动隐藏
- if (!this.autoHide) this.$dom.classList.add('x-scrollbar-keep')
- // 绑定事件
- this.bindScroll()
- this.bindDrag()
- if (this.onlyHorizontal) {
- this.bindWheel()
- }
- // 响应容器和内容大小改变
- if (this.autoUpdate) {
- // 首次自动触发
- this.resizeObserver()
- } else {
- this.update()
- }
- }
- /**
- * 设置滑块大小
- */
- setThumbSize() {
- // (clientWidth / scrollWidth) = (滑块大小 / clientWidth)
- // 最大滑动距离 = clientWidth - 滑块大小
- // 最大滚动距离 = scrollWidth - clientWidth
- // (滑动距离 / 最大滑动距离) = (滚动距离 / 最大滚动距离)
- // 容器大小
- this.clientWidth = this.$container.clientWidth
- this.clientHeight = this.$container.clientHeight
- // 内容大小
- this.scrollWidth = this.$container.scrollWidth
- this.scrollHeight = this.$container.scrollHeight
- //是否存在滚动条
- this.hasXScrollbar = this.scrollWidth > this.clientWidth
- this.hasYScrollbar = this.scrollHeight > this.clientHeight
- //滑块大小
- this.thumbXWidth = Math.max((this.clientWidth / this.scrollWidth) * this.clientWidth, 30)
- this.thumbYHeight = Math.max((this.clientHeight / this.scrollHeight) * this.clientHeight, 30)
- //最大滑动距离
- this.thumbXMaxLeft = this.clientWidth - this.thumbXWidth
- this.thumbYMaxTop = this.clientHeight - this.thumbYHeight
- //最大滚动距离
- this.maxScrollLeft = this.scrollWidth - this.clientWidth
- this.maxScrollTop = this.scrollHeight - this.clientHeight
- this.$trackX.style.display = this.hasXScrollbar ? 'block' : 'none'
- this.$trackY.style.display = this.hasYScrollbar ? 'block' : 'none'
- this.$thumbX.style.width = `${this.thumbXWidth}px`
- this.$thumbY.style.height = `${this.thumbYHeight}px`
- }
- /**
- * 拖动事件
- */
- bindDrag() {
- // 上一次的拖动位置
- let screenX = null
- let screenY = null
- this.$thumbX.addEventListener('mousedown', e => {
- this.$trackX.classList.add('x-scrollbar__track--draging')
- this.thumbXActive = true
- screenX = e.screenX
- })
- this.$thumbY.addEventListener('mousedown', e => {
- this.$trackY.classList.add('x-scrollbar__track--draging')
- this.thumbYActive = true
- screenY = e.screenY
- })
- document.addEventListener('mouseup', _ => {
- this.$trackX.classList.remove('x-scrollbar__track--draging')
- this.$trackY.classList.remove('x-scrollbar__track--draging')
- this.thumbXActive = false
- this.thumbYActive = false
- })
- document.addEventListener('mousemove', e => {
- if (!(this.thumbXActive || this.thumbYActive)) return
- e.preventDefault()
- requestAnimationFrame(() => {
- if (this.thumbXActive) {
- const offset = e.screenX - screenX
- screenX = e.screenX
- const left = Math.max(Math.min(Number.parseFloat(this.$thumbX.style.left || 0) + offset, this.thumbXMaxLeft), 0)
- this.$thumbX.style.left = `${left}px`
- this.$container.scrollLeft = (left / this.thumbXMaxLeft) * this.maxScrollLeft
- } else {
- const offset = e.screenY - screenY
- screenY = e.screenY
- const top = Math.max(Math.min(Number.parseFloat(this.$thumbY.style.top || 0) + offset, this.thumbYMaxTop), 0)
- this.$thumbY.style.top = `${top}px`
- this.$container.scrollTop = (top / this.thumbYMaxTop) * this.maxScrollTop
- }
- })
- })
- }
- /**
- * 仅水平滚动(拨动鼠标滚轮时将作用于X轴)
- */
- bindWheel() {
- const easeout = (start, end) => {
- if (Math.abs(end - start) <= 1) return end
- return start + (end - start) / 4
- }
- this.$container.addEventListener('wheel', e => {
- // 仅响应 y 滚动 => 作用于 x
- if (!this.hasXScrollbar) return
- if (e.deltaY && !e.shiftKey) {
- // 结束值
- this.scrollLeft = Math.max(Math.min((this.scrollLeft || this.$container.scrollLeft) + (e.deltaY > 0 ? 100 : -100), this.maxScrollLeft), 0)
- this.left = (this.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft
- // 阻止向上传递 || !(终点)
- if (this.preventDefault || !(this.scrollLeft == 0 || this.scrollLeft == this.maxScrollLeft)) {
- e.preventDefault()
- e.stopPropagation()
- }
- if (this.reqId) return
- // 起始值
- let scrollLeft = this.$container.scrollLeft
- let left = Number.parseFloat(this.$thumbX.style.left || 0)
- const animate = () => {
- scrollLeft = easeout(scrollLeft, this.scrollLeft)
- left = easeout(left, this.left)
- this.$container.scrollLeft = scrollLeft
- this.$thumbX.style.left = `${left}px`
- this.innerScroll = true
- if (scrollLeft != this.scrollLeft) {
- this.reqId = requestAnimationFrame(animate)
- } else {
- this.reqId = null
- this.scrollLeft = null
- requestAnimationFrame(() => (this.innerScroll = false))
- }
- }
- animate()
- }
- })
- }
- /**
- * 滚动事件 => 修正滑块位置
- */
- bindScroll() {
- this.$container.addEventListener('scroll', () => {
- if (this.thumbXActive || this.thumbYActive || this.innerScroll) return
- if (this.hasXScrollbar) {
- this.$thumbX.style.left = `${(this.$container.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft}px`
- }
- if (this.hasYScrollbar) {
- this.$thumbY.style.top = `${(this.$container.scrollTop / this.maxScrollTop) * this.thumbYMaxTop}px`
- }
- })
- }
- /**
- * 观察容器大小
- */
- resizeObserver() {
- this.$resizeObserver = new ResizeObserver(entries => {
- const contentRect = entries[0].contentRect
- if (!(contentRect.width || contentRect.height)) return
- this.update()
- })
- this.$resizeObserver.observe(this.$container)
- this.$resizeObserver.observe(this.$content)
- }
- /**
- * 使用滚动值修正滑块
- * 在 容器大小 或 内容大小 发生改变时调用
- */
- update() {
- this.setThumbSize()
- if (this.hasXScrollbar) {
- this.$thumbX.style.left = `${(this.$container.scrollLeft / this.maxScrollLeft) * this.thumbXMaxLeft}px`
- }
- if (this.hasYScrollbar) {
- this.$thumbY.style.top = `${(this.$container.scrollTop / this.maxScrollTop) * this.thumbYMaxTop}px`
- }
- }
- /**
- * html字符串 转 dom对象
- * @param {*} html
- * @returns
- */
- html2dom(html) {
- const element = document.createElement('div')
- element.innerHTML = html
- const children = element.children
- if (children.length <= 1) {
- return children[0]
- } else {
- return children
- }
- }
- /**
- * 生成自定义样式
- */
- style() {
- let content = `
- /* 轨道 */
- .x-scrollbar__track-x {
- height: ${Number.parseInt(this.thumbSize) * 2 + 4}px;
- }
-
- .x-scrollbar__track-y {
- width: ${Number.parseInt(this.thumbSize) * 2 + 4}px;
- }
-
- /* 滑块 */
- .x-scrollbar__track-x > .x-scrollbar__thumb-x,
- .x-scrollbar__track-y > .x-scrollbar__thumb-y {
- background: ${this.thumbBackground};
- border-radius: ${Number.parseInt(this.thumbRadius || 0) != 5 ? Number.parseInt(this.thumbRadius || 0) : Number.parseInt(this.thumbSize)}px;
- }
-
- .x-scrollbar__track-x > .x-scrollbar__thumb-x {
- height: ${Number.parseInt(this.thumbSize)}px;
- }
-
- .x-scrollbar__track-y > .x-scrollbar__thumb-y {
- width: ${Number.parseInt(this.thumbSize)}px;
- }
-
- /* 激活后大小 */
- .x-scrollbar__track-x:hover > .x-scrollbar__thumb-x,
- .x-scrollbar__track--draging > .x-scrollbar__thumb-x {
- height: ${Number.parseInt(this.thumbSize) * 2}px;
- }
-
- .x-scrollbar__track-y:hover > .x-scrollbar__thumb-y,
- .x-scrollbar__track--draging > .x-scrollbar__thumb-y {
- width: ${Number.parseInt(this.thumbSize) * 2}px;
- }
-
- /* 鼠标移入轨道 || 拖动过程中 => 显示轨道 & 高亮滑块 */
- .x-scrollbar__track-x:hover,
- .x-scrollbar__track-y:hover,
- .x-scrollbar__track-x.x-scrollbar__track--draging,
- .x-scrollbar__track-y.x-scrollbar__track--draging {
- background: ${this.trackBackground || 'transparent'};
- }`
- this.key = `x-scrollbar-${Math.abs(Math.trunc((1 + Math.random()) * Date.now())).toString(16)}`
- this.$dom.setAttribute(this.key, '')
- const style = this.html2dom(`<style ${this.key}></style>`)
- content = content.replaceAll('\n.x-scrollbar', `\n[${this.key}] > .x-scrollbar`)
- content = content.replaceAll(';', ' !important;')
- style.innerHTML = content
- document.querySelector('head').appendChild(style)
- }
- }
- export default XScrollbar
|