411 lines
8.6 KiB
Plaintext
411 lines
8.6 KiB
Plaintext
<template>
|
|
<div class="container"><slot></slot></div>
|
|
<div class="is-horizontal"><span class="thumb"></span></div>
|
|
<div class="is-vertical"><span class="thumb"></span></div>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
:host {
|
|
overflow: hidden;
|
|
position: relative;
|
|
display: flex;
|
|
width: 100%;
|
|
|
|
.container {
|
|
overflow: hidden;
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
/* 横向 */
|
|
|
|
.is-horizontal,
|
|
.is-vertical {
|
|
visibility: hidden;
|
|
position: absolute;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
z-index: 10240;
|
|
opacity: 0;
|
|
user-select: none;
|
|
transition: opacity 0.3s linear, visibility 0.3s linear;
|
|
|
|
.thumb {
|
|
display: block;
|
|
border-radius: 5px;
|
|
background: rgba(44, 47, 53, 0.25);
|
|
cursor: default;
|
|
transition: width 0.1s linear, height 0.1s linear;
|
|
|
|
&:hover {
|
|
background: rgba(44, 47, 53, 0.5);
|
|
}
|
|
}
|
|
}
|
|
|
|
.is-horizontal {
|
|
flex-direction: column;
|
|
left: 0;
|
|
bottom: 1px;
|
|
width: 100%;
|
|
height: 10px;
|
|
|
|
.thumb {
|
|
width: 0;
|
|
height: 6px;
|
|
|
|
&:hover {
|
|
height: 10px;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 纵向 */
|
|
.is-vertical {
|
|
top: 0;
|
|
right: 1px;
|
|
width: 10px;
|
|
height: 100%;
|
|
|
|
.thumb {
|
|
width: 6px;
|
|
height: 0;
|
|
|
|
&:hover {
|
|
width: 10px;
|
|
}
|
|
}
|
|
}
|
|
|
|
:host(:hover) {
|
|
.is-horizontal,
|
|
.is-vertical {
|
|
visibility: visible;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
:host([axis='x']) {
|
|
.is-vertical {
|
|
display: none;
|
|
}
|
|
}
|
|
:host([axis='y']) {
|
|
.is-horizontal {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
import $ from '../utils'
|
|
// 是否火狐浏览器
|
|
const IS_FF = !!window.sidebar
|
|
|
|
/* */
|
|
export default class Scroll {
|
|
props = {
|
|
thumbX: 0,
|
|
thumbY: 0,
|
|
disabled: false,
|
|
axis: 'xy' // 滚动方向, 默认x轴和y轴都可以滚动
|
|
}
|
|
__init__() {
|
|
/* render */
|
|
this.__BOX__ = this.root.children[1]
|
|
this.__X__ = this.root.children[2].children[0]
|
|
this.__Y__ = this.root.children[3].children[0]
|
|
}
|
|
|
|
get scrollTop() {
|
|
return this.__BOX__.scrollTop
|
|
}
|
|
|
|
set scrollTop(n) {
|
|
n = +n
|
|
if (n === n) {
|
|
var { sh, oh, yh } = this.props
|
|
|
|
this.__BOX__.scrollTop = n
|
|
var fixedY = (this.__BOX__.scrollTop / (sh - oh)) * (oh - yh)
|
|
this.props.thumbY = fixedY
|
|
|
|
this.__Y__.style.transform = `translateY(${fixedY}px)`
|
|
}
|
|
}
|
|
|
|
get scrollLeft() {
|
|
return this.__BOX__.scrollLeft
|
|
}
|
|
|
|
set scrollLeft(n) {
|
|
n = +n
|
|
if (n === n) {
|
|
var { sw, ow, xw } = this.props
|
|
|
|
this.__BOX__.scrollLeft = n
|
|
var fixedX = (this.__BOX__.scrollLeft / (sw - ow)) * (ow - xw)
|
|
this.props.thumbX = fixedX
|
|
|
|
this.__X__.style.transform = `translateX(${fixedX}px)`
|
|
}
|
|
}
|
|
|
|
get scrollHeight() {
|
|
return this.__BOX__.scrollHeight
|
|
}
|
|
|
|
get disabled() {
|
|
return this.props.disabled
|
|
}
|
|
|
|
set disabled(val) {
|
|
var type = typeof val
|
|
|
|
if (val === this.props.disabled) {
|
|
return
|
|
}
|
|
if ((type === 'boolean' && val) || type !== 'boolean') {
|
|
this.props.disabled = true
|
|
this.setAttribute('disabled', '')
|
|
} else {
|
|
this.props.disabled = false
|
|
this.removeAttribute('disabled')
|
|
}
|
|
}
|
|
|
|
_fetchScrollX(moveX) {
|
|
var { sw, ow, xw } = this.props
|
|
|
|
if (moveX < 0) {
|
|
moveX = 0
|
|
} else if (moveX > ow - xw) {
|
|
moveX = ow - xw
|
|
}
|
|
this.__BOX__.scrollLeft = (sw - ow) * (moveX / (ow - xw))
|
|
this.__X__.style.transform = `translateX(${moveX}px)`
|
|
|
|
return moveX
|
|
}
|
|
|
|
_fetchScrollY(moveY) {
|
|
var { sh, oh, yh } = this.props
|
|
|
|
if (moveY < 0) {
|
|
moveY = 0
|
|
} else if (moveY > oh - yh) {
|
|
moveY = oh - yh
|
|
}
|
|
this.__BOX__.scrollTop = (sh - oh) * (moveY / (oh - yh))
|
|
this.__Y__.style.transform = `translateY(${moveY}px)`
|
|
return moveY
|
|
}
|
|
|
|
mounted() {
|
|
// 初始化滚动条的位置和长度
|
|
this._initFn = $.bind(this.__BOX__, 'mouseenter', ev => {
|
|
// 禁用状态, 不允许滚动
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
var ow = this.__BOX__.offsetWidth
|
|
var sw = this.__BOX__.scrollWidth
|
|
var oh = this.__BOX__.offsetHeight
|
|
var sh = this.__BOX__.scrollHeight
|
|
|
|
var yh = ((oh * oh) / sh) >> 0
|
|
var xw = ((ow * ow) / sw) >> 0
|
|
if (yh < 50) {
|
|
yh = 50
|
|
}
|
|
if (xw < 50) {
|
|
xw = 50
|
|
}
|
|
|
|
// 100%时不显示
|
|
if (xw === ow) {
|
|
xw = 0
|
|
}
|
|
if (yh === oh) {
|
|
yh = 0
|
|
}
|
|
|
|
this.props.oh = oh
|
|
this.props.sh = sh
|
|
this.props.ow = ow
|
|
this.props.sw = sw
|
|
this.props.yh = yh
|
|
this.props.xw = xw
|
|
|
|
this.__X__.style.width = xw + 'px'
|
|
this.__Y__.style.height = yh + 'px'
|
|
})
|
|
|
|
// 鼠标滚动事件
|
|
this._wheelFn = $.bind(this.__BOX__, 'wheel', ev => {
|
|
// 禁用状态, 不允许滚动
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
var { sh, oh, yh, sw, ow, xw } = this.props
|
|
|
|
// x轴 y轴 都为0时, 不作任何处理
|
|
if (!xw && !yh) {
|
|
return
|
|
}
|
|
// ev.preventDefault()
|
|
|
|
//校正兼容苹果鼠标在 chrome和FF下滚动的精度
|
|
var deltaX
|
|
var deltaY
|
|
|
|
if (IS_FF) {
|
|
// 区分是触摸板还是普通鼠标
|
|
deltaX = ev.deltaMode ? 10 * ev.deltaX : ev.deltaX
|
|
deltaY = ev.deltaMode ? 10 * ev.deltaY : ev.deltaY
|
|
} else {
|
|
var delta = Math.abs(ev.wheelDelta)
|
|
// 精度高的(小于120), 一般是触摸板或苹果的鼠标
|
|
if (delta < 120) {
|
|
deltaX = ev.deltaX
|
|
deltaY = ev.deltaY
|
|
} else {
|
|
deltaX = ev.deltaX / (delta / 120)
|
|
deltaY = ev.deltaY / (delta / 120)
|
|
}
|
|
}
|
|
|
|
//
|
|
if (this.props.axis !== 'x') {
|
|
this.__BOX__.scrollTop += deltaY
|
|
|
|
if (yh) {
|
|
// 修正滚动条的位置
|
|
// 滚动比例 y 滚动条的可移动距离
|
|
var fixedY = (this.__BOX__.scrollTop / (sh - oh)) * (oh - yh)
|
|
fixedY = fixedY >> 0
|
|
|
|
if (
|
|
(fixedY === 0 || oh - yh === fixedY) &&
|
|
fixedY === this.props.thumbY
|
|
) {
|
|
return
|
|
}
|
|
|
|
ev.preventDefault()
|
|
ev.stopPropagation()
|
|
|
|
this.props.thumbY = fixedY
|
|
this.__Y__.style.transform = `translateY(${fixedY}px)`
|
|
}
|
|
}
|
|
|
|
if (this.props.axis !== 'y') {
|
|
this.__BOX__.scrollLeft += deltaX
|
|
|
|
if (xw) {
|
|
// 修正滚动条的位置
|
|
// 滚动比例 x 滚动条的可移动距离
|
|
var fixedX = (this.__BOX__.scrollLeft / (sw - ow)) * (ow - xw)
|
|
fixedX = fixedX >> 0
|
|
|
|
if (
|
|
(fixedX === 0 || ow - xw === fixedX) &&
|
|
fixedX === this.props.thumbX
|
|
) {
|
|
return
|
|
}
|
|
|
|
ev.preventDefault()
|
|
ev.stopPropagation()
|
|
|
|
this.props.thumbX = fixedX
|
|
this.__X__.style.transform = `translateX(${fixedX}px)`
|
|
}
|
|
}
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent('scroll', {
|
|
detail: { x: this.props.thumbX, y: this.props.thumbY }
|
|
})
|
|
)
|
|
})
|
|
|
|
var startX,
|
|
startY,
|
|
moveX,
|
|
moveY,
|
|
mousemoveFn = ev => {
|
|
var { thumbY, thumbX } = this.props
|
|
if (startX !== null) {
|
|
moveX = this._fetchScrollX(thumbX + ev.pageX - startX)
|
|
}
|
|
|
|
if (startY !== null) {
|
|
moveY = this._fetchScrollY(thumbY + ev.pageY - startY)
|
|
}
|
|
},
|
|
mouseupFn = ev => {
|
|
startX = null
|
|
startY = null
|
|
this.props.thumbX = moveX
|
|
this.props.thumbY = moveY
|
|
$.unbind(document, 'mousemove', mousemoveFn)
|
|
$.unbind(document, 'mouseup', mouseupFn)
|
|
}
|
|
|
|
$.bind(this.__Y__, 'mousedown', ev => {
|
|
startY = ev.pageY
|
|
if (!this.props.thumbY) {
|
|
this.props.thumbY = 0
|
|
}
|
|
// 禁用状态, 不允许滚动
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
$.bind(document, 'mousemove', mousemoveFn)
|
|
$.bind(document, 'mouseup', mouseupFn)
|
|
})
|
|
|
|
$.bind(this.__X__, 'mousedown', ev => {
|
|
startX = ev.pageX
|
|
if (!this.props.thumbX) {
|
|
this.props.thumbX = 0
|
|
}
|
|
// 禁用状态, 不允许滚动
|
|
if (this.disabled) {
|
|
return
|
|
}
|
|
$.bind(document, 'mousemove', mousemoveFn)
|
|
$.bind(document, 'mouseup', mouseupFn)
|
|
})
|
|
|
|
this.__observer = new MutationObserver(this._initFn)
|
|
this.__observer.observe(this, {
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true
|
|
})
|
|
}
|
|
|
|
unmount() {
|
|
this.__observer.disconnect()
|
|
$.unbind(this.__BOX__, 'mouseenter', this._initFn)
|
|
$.unbind(this.__BOX__, 'wheel', this._wheelFn)
|
|
}
|
|
|
|
watch() {
|
|
switch (name) {
|
|
case 'axis':
|
|
this.props.axis = val
|
|
break
|
|
case 'disabled':
|
|
this[name] = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
</script>
|
JavaScript
95.2%
CSS
4.8%