vuescroll
Version:
A powerful, customizable, multi-mode scrollbar plugin based on Vue.js
551 lines (472 loc) • 14.8 kB
JavaScript
import scrollMap from 'shared/scroll-map';
import { eventCenter, getRealParent } from 'shared/util';
import { requestAnimationFrame } from 'core/third-party/scroller/requestAnimationFrame';
import { touchManager } from 'src/shared/env';
const colorCache = {};
const rgbReg = /rgb\(/;
const extractRgbColor = /rgb\((.*)\)/;
// Transform a common color int oa `rgbA` color
function getRgbAColor(color, opacity) {
const id = color + '&' + opacity;
if (colorCache[id]) {
return colorCache[id];
}
const div = document.createElement('div');
div.style.background = color;
document.body.appendChild(div);
const computedColor = window.getComputedStyle(div).backgroundColor;
document.body.removeChild(div);
/* istanbul ignore if */
if (!rgbReg.test(computedColor)) {
return color;
}
return (colorCache[id] = `rgba(${
extractRgbColor.exec(computedColor)[1]
}, ${opacity})`);
}
export default {
name: 'bar',
props: {
ops: Object,
state: Object,
hideBar: Boolean,
otherBarHide: Boolean,
type: String
},
computed: {
bar() {
return scrollMap[this.type];
},
barSize() {
return Math.max(this.state.size, this.ops.bar.minSize);
},
barRatio() {
return (1 - this.barSize) / (1 - this.state.size);
}
},
render(h) {
const vm = this;
/** Get rgbA format background color */
const railBackgroundColor = getRgbAColor(
vm.ops.rail.background,
vm.ops.rail.opacity
);
if (!this.touchManager) {
this.touchManager = new touchManager();
}
/** Rail Data */
const railSize = vm.ops.rail.size;
const endPos = vm.otherBarHide ? 0 : railSize;
const touchObj = vm.touchManager.getTouchObject();
const rail = {
class: `__rail-is-${vm.type}`,
style: {
position: 'absolute',
'z-index': '1',
borderRadius: vm.ops.rail.specifyBorderRadius || railSize,
background: railBackgroundColor,
border: vm.ops.rail.border,
[vm.bar.opsSize]: railSize,
[vm.bar.posName]: vm.ops.rail['gutterOfEnds'] || 0,
[vm.bar.opposName]: vm.ops.rail['gutterOfEnds'] || endPos,
[vm.bar.sidePosName]: vm.ops.rail['gutterOfSide']
}
};
if (touchObj) {
rail.on = {
[touchObj.touchenter]() {
vm.setRailHover();
},
[touchObj.touchleave]() {
vm.setRailLeave();
}
};
}
// left space for scroll button
const buttonSize = vm.ops.scrollButton.enable ? railSize : 0;
const barWrapper = {
class: `__bar-wrap-is-${vm.type}`,
style: {
position: 'absolute',
borderRadius: vm.ops.rail.specifyBorderRadius || railSize,
[vm.bar.posName]: buttonSize,
[vm.bar.opposName]: buttonSize
},
on: {}
};
const scrollDistance = vm.state.posValue * vm.state.size;
const pos = (scrollDistance * vm.barRatio) / vm.barSize;
const opacity = vm.state.opacity;
const parent = getRealParent(this);
// set class hook
parent.setClassHook(
this.type == 'vertical' ? 'vBarVisible' : 'hBarVisible',
!!opacity
);
/** Scrollbar style */
let barStyle = {
cursor: 'pointer',
position: 'absolute',
margin: 'auto',
transition: 'opacity 0.5s',
'user-select': 'none',
'border-radius': 'inherit',
[vm.bar.size]: vm.barSize * 100 + '%',
background: vm.ops.bar.background,
[vm.bar.opsSize]: vm.ops.bar.size,
opacity,
transform: `translate${scrollMap[vm.type].axis}(${pos}%)`
};
const bar = {
style: barStyle,
class: `__bar-is-${vm.type}`,
ref: 'thumb',
on: {}
};
if (vm.type == 'vertical') {
barWrapper.style.width = '100%';
// Let bar to be on the center.
bar.style.left = 0;
bar.style.right = 0;
} else {
barWrapper.style.height = '100%';
bar.style.top = 0;
bar.style.bottom = 0;
}
/* istanbul ignore next */
{
const touchObj = this.touchManager.getTouchObject();
bar.on[touchObj.touchstart] = this.createBarEvent();
barWrapper.on[touchObj.touchstart] = this.createTrackEvent();
}
return (
<div {...rail}>
{this.createScrollbarButton(h, 'start')}
{this.hideBar ? null : (
<div {...barWrapper}>
<div {...bar} />
</div>
)}
{this.createScrollbarButton(h, 'end')}
</div>
);
},
data() {
return {
isBarDragging: false
};
},
methods: {
setRailHover() {
const parent = getRealParent(this);
let { state } = parent.vuescroll;
if (!state.isRailHover) {
state.isRailHover = true;
parent.showBar();
}
},
setRailLeave() {
const parent = getRealParent(this);
let { state } = parent.vuescroll;
state.isRailHover = false;
parent.hideBar();
},
setBarDrag(val) /* istanbul ignore next */ {
this.$emit('setBarDrag', (this.isBarDragging = val));
const parent = getRealParent(this);
// set class hook
parent.setClassHook(
this.type == 'vertical' ? 'vBarDragging' : 'hBarDragging',
!!val
);
},
createBarEvent() {
const ctx = this;
const parent = getRealParent(ctx);
const touchObj = ctx.touchManager.getTouchObject();
function mousedown(e) /* istanbul ignore next */ {
let event = ctx.touchManager.getEventObject(e);
if (!event) return;
e.stopImmediatePropagation();
e.preventDefault();
event = event[0];
document.onselectstart = () => false;
ctx.axisStartPos =
event[ctx.bar.client] -
ctx.$refs['thumb'].getBoundingClientRect()[ctx.bar.posName];
// Tell parent that the mouse has been down.
ctx.setBarDrag(true);
eventCenter(document, touchObj.touchmove, mousemove);
eventCenter(document, touchObj.touchend, mouseup);
}
function mousemove(e) /* istanbul ignore next */ {
if (!ctx.axisStartPos) {
return;
}
let event = ctx.touchManager.getEventObject(e);
if (!event) return;
event = event[0];
const thubmParent = ctx.$refs.thumb.parentNode;
let delta =
event[ctx.bar.client] -
thubmParent.getBoundingClientRect()[ctx.bar.posName];
delta = delta / ctx.barRatio;
const percent =
(delta - ctx.axisStartPos) / thubmParent[ctx.bar.offset];
parent.scrollTo(
{
[ctx.bar.axis.toLowerCase()]:
parent.scrollPanelElm[ctx.bar.scrollSize] * percent
},
false
);
}
function mouseup() /* istanbul ignore next */ {
ctx.setBarDrag(false);
parent.hideBar();
document.onselectstart = null;
ctx.axisStartPos = 0;
eventCenter(document, touchObj.touchmove, mousemove, false, 'off');
eventCenter(document, touchObj.touchend, mouseup, false, 'off');
}
return mousedown;
},
createTrackEvent() {
const ctx = this;
return function handleClickTrack(e) {
const parent = getRealParent(ctx);
const { client, offset, posName, axis } = ctx.bar;
const thumb = ctx.$refs['thumb'];
e.preventDefault();
e.stopImmediatePropagation();
/* istanbul ignore if */
if (!thumb) return;
const barOffset = thumb[offset];
const event = ctx.touchManager.getEventObject(e)[0];
const percent =
(event[client] -
e.currentTarget.getBoundingClientRect()[posName] -
barOffset / 2) /
(e.currentTarget[offset] - barOffset);
parent.scrollTo({
[axis.toLowerCase()]: percent * 100 + '%'
});
};
},
// Scrollbuton relative things...
createScrollbarButton(h, type /* start or end */) {
const barContext = this;
if (!barContext.ops.scrollButton.enable) {
return null;
}
const size = barContext.ops.rail.size;
const { opacity, background } = barContext.ops.scrollButton;
const borderColor = getRgbAColor(background, opacity);
const wrapperProps = {
class: [
'__bar-button',
'__bar-button-is-' + barContext.type + '-' + type
],
style: {
[barContext.bar.scrollButton[type]]: 0,
width: size,
height: size,
position: 'absolute',
cursor: 'pointer',
display: 'table'
},
ref: type
};
const innerProps = {
class: '__bar-button-inner',
style: {
border: `calc(${size} / 2.5) solid transparent`,
width: '0',
height: '0',
margin: 'auto',
position: 'absolute',
top: '0',
bottom: '0',
right: '0',
left: '0'
},
on: {}
};
if (barContext.type == 'vertical') {
if (type == 'start') {
innerProps.style['border-bottom-color'] = borderColor;
innerProps.style['transform'] = 'translateY(-25%)';
} else {
innerProps.style['border-top-color'] = borderColor;
innerProps.style['transform'] = 'translateY(25%)';
}
} else {
if (type == 'start') {
innerProps.style['border-right-color'] = borderColor;
innerProps.style['transform'] = 'translateX(-25%)';
} else {
innerProps.style['border-left-color'] = borderColor;
innerProps.style['transform'] = 'translateX(25%)';
}
}
/* istanbul ignore next */
{
const touchObj = this.touchManager.getTouchObject();
innerProps.on[touchObj.touchstart] = this.createScrollButtonEvent(
type,
touchObj
);
}
return (
<div {...wrapperProps}>
<div {...innerProps} />
</div>
);
},
createScrollButtonEvent(type, touchObj) {
const ctx = this;
const parent = getRealParent(ctx);
const { step, mousedownStep } = ctx.ops.scrollButton;
const stepWithDirection = type == 'start' ? -step : step;
const mousedownStepWithDirection =
type == 'start' ? -mousedownStep : mousedownStep;
const ref = requestAnimationFrame(window);
// bar props: type
const barType = ctx.type;
let isMouseDown = false;
let isMouseout = true;
let timeoutId;
function start(e) {
/* istanbul ignore if */
if (3 == e.which) {
return;
}
// set class hook
parent.setClassHook(`cliking${barType}${type}Button`, true);
e.stopImmediatePropagation();
e.preventDefault();
isMouseout = false;
parent.scrollBy({
['d' + ctx.bar.axis.toLowerCase()]: stepWithDirection
});
eventCenter(document, touchObj.touchend, endPress, false);
if (touchObj.touchstart == 'mousedown') {
const elm = ctx.$refs[type];
eventCenter(elm, 'mouseenter', enter, false);
eventCenter(elm, 'mouseleave', leave, false);
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => /* istanbul ignore next */ {
isMouseDown = true;
ref(pressing, window);
}, 500);
}
function pressing() /* istanbul ignore next */ {
if (isMouseDown && !isMouseout) {
parent.scrollBy(
{
['d' + ctx.bar.axis.toLowerCase()]: mousedownStepWithDirection
},
false
);
ref(pressing, window);
}
}
function endPress() {
clearTimeout(timeoutId);
isMouseDown = false;
eventCenter(document, touchObj.touchend, endPress, false, 'off');
if (touchObj.touchstart == 'mousedown') {
const elm = ctx.$refs[type];
eventCenter(elm, 'mouseenter', enter, false, 'off');
eventCenter(elm, 'mouseleave', leave, false, 'off');
}
parent.setClassHook(`cliking${barType}${type}Button`, false);
}
function enter() /* istanbul ignore next */ {
isMouseout = false;
pressing();
}
function leave() /* istanbul ignore next */ {
isMouseout = true;
}
return start;
}
}
};
function getBarData(vm, type) {
const axis = scrollMap[type].axis;
/** type.charAt(0) = vBar/hBar */
const barType = `${type.charAt(0)}Bar`;
const hideBar =
!vm.bar[barType].state.size ||
!vm.mergedOptions.scrollPanel['scrolling' + axis] ||
(vm.refreshLoad && type !== 'vertical') ||
vm.mergedOptions.bar.disable;
const keepShowRail = vm.mergedOptions.rail.keepShow;
if (hideBar && !keepShowRail) {
return null;
}
return {
hideBar,
props: {
type: type,
ops: {
bar: vm.mergedOptions.bar,
rail: vm.mergedOptions.rail,
scrollButton: vm.mergedOptions.scrollButton
},
state: vm.bar[barType].state,
hideBar
},
on: {
setBarDrag: vm.setBarDrag
},
ref: `${type}Bar`,
key: type
};
}
/**
* create bars
*
* @param {any} size
* @param {any} type
*/
export function createBar(h, vm) {
const verticalBarProps = getBarData(vm, 'vertical');
const horizontalBarProps = getBarData(vm, 'horizontal');
// set class hooks
vm.setClassHook('hasVBar', !!(verticalBarProps && !verticalBarProps.hideBar));
vm.setClassHook(
'hasHBar',
!!(horizontalBarProps && !horizontalBarProps.hideBar)
);
return [
verticalBarProps ? (
<bar
{...{
...verticalBarProps,
...{
props: {
...{ otherBarHide: !horizontalBarProps },
...verticalBarProps.props
}
}
}}
/>
) : null,
horizontalBarProps ? (
<bar
{...{
...horizontalBarProps,
...{
props: {
...{ otherBarHide: !verticalBarProps },
...horizontalBarProps.props
}
}
}}
/>
) : null
];
}