@zippytech/react-scroll-container
Version:
Custom scrollbars for your React components
529 lines (428 loc) • 20.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _SIZES, _OTHER_SIZES, _MARGINS, _TRACK_SIDES;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
var _autoBind = require('@zippytech/react-class/autoBind');
var _autoBind2 = _interopRequireDefault(_autoBind);
var _reactCleanProps = require('@zippytech/react-clean-props');
var _reactCleanProps2 = _interopRequireDefault(_reactCleanProps);
var _shouldComponentUpdate2 = require('./shouldComponentUpdate');
var _shouldComponentUpdate3 = _interopRequireDefault(_shouldComponentUpdate2);
var _join = require('./join');
var _join2 = _interopRequireDefault(_join);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /**
* Copyright (c) 2015-present, Zippy Technologies
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var HORIZONTAL = 'horizontal';
var VERTICAL = 'vertical';
var SIZES = (_SIZES = {}, _defineProperty(_SIZES, VERTICAL, 'height'), _defineProperty(_SIZES, HORIZONTAL, 'width'), _SIZES);
var OTHER_SIZES = (_OTHER_SIZES = {}, _defineProperty(_OTHER_SIZES, VERTICAL, 'width'), _defineProperty(_OTHER_SIZES, HORIZONTAL, 'height'), _OTHER_SIZES);
var MARGINS = (_MARGINS = {}, _defineProperty(_MARGINS, VERTICAL, 'right'), _defineProperty(_MARGINS, HORIZONTAL, 'bottom'), _MARGINS);
var TRACK_SIDES = (_TRACK_SIDES = {}, _defineProperty(_TRACK_SIDES, VERTICAL, ['top', 'bottom']), _defineProperty(_TRACK_SIDES, HORIZONTAL, ['left', 'right']), _TRACK_SIDES);
var STYLES = {
vertical: {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
contain: 'layout paint style'
},
horizontal: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
contain: 'layout paint style'
}
};
var ZippyScrollbar = function (_Component) {
_inherits(ZippyScrollbar, _Component);
function ZippyScrollbar(props) {
_classCallCheck(this, ZippyScrollbar);
var _this = _possibleConstructorReturn(this, (ZippyScrollbar.__proto__ || Object.getPrototypeOf(ZippyScrollbar)).call(this, props));
(0, _autoBind2.default)(_this);
_this.scrollPos = 0;
_this.state = { scrollPos: 0 };
_this.refTrack = function (c) {
_this.trackNode = c;
};
_this.refThumb = function (c) {
_this.thumbNode = c;
};
return _this;
}
_createClass(ZippyScrollbar, [{
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
return (0, _shouldComponentUpdate3.default)(this, nextProps, nextState);
}
}, {
key: 'setVisible',
value: function setVisible(visible) {
this.visible = visible;
// when trying to hide the scrollbar, but the mouse is over it
// just don't hide it. Let setOver(false) do the job
if (!visible && this.state.over) {
return;
}
if (this.state.visible === visible) {
return;
}
this.setState({ visible: visible });
}
}, {
key: 'setScrollPos',
value: function setScrollPos(scrollPos) {
this.scrollPos = scrollPos;
var transform = this.getThumbTransform();
if (this.currentTransform == transform) {
return;
}
// in order to squeeze in the most perf,
// we manage this via direct style.transform assignment
// and not via setState to trigger a render
this.thumbNode.style.transform = this.currentTransform = transform;
}
}, {
key: 'getScrollPos',
value: function getScrollPos() {
return this.scrollPos;
}
}, {
key: 'render',
value: function render() {
var _assign;
var props = this.props;
var autoHide = props.autoHide;
var className = props.className,
style = props.style,
orientation = props.orientation,
vertical = props.vertical,
horizontal = props.horizontal,
thumbVisible = props.visible;
if (orientation) {
vertical = orientation == VERTICAL;
horizontal = !vertical;
}
this.orientation = orientation = horizontal ? HORIZONTAL : VERTICAL;
var sizeName = SIZES[orientation];
var otherSizeName = OTHER_SIZES[orientation];
if (autoHide === false) {
thumbVisible = true;
} else {
if (this.state.visible !== undefined) {
thumbVisible = this.state.visible;
}
if (this.state.active) {
thumbVisible = true;
}
}
var hiding = !thumbVisible && this.prevVisible;
var showing = thumbVisible && !this.prevVisible;
className = (0, _join2.default)(className, 'zippy-react-scroll-container__scrollbar', 'zippy-react-scroll-container__scrollbar--orientation-' + orientation, !thumbVisible && 'zippy-react-scroll-container__scrollbar--hidden', hiding && 'zippy-react-scroll-container__scrollbar--hiding', this.state.active && 'zippy-react-scroll-container__scrollbar--active', showing && 'zippy-react-scroll-container__scrollbar--showing');
style = (0, _objectAssign2.default)({}, style, STYLES[orientation]);
if (showing && this.props.showTransitionDuration) {
style.transitionDuration = this.props.showTransitionDuration;
}
if (hiding && this.props.hideTransitionDuration) {
style.transitionDuration = this.props.hideTransitionDuration;
}
this.prevVisible = thumbVisible;
var divProps = (0, _reactCleanProps2.default)(props, ZippyScrollbar.propTypes);
var thumbSize = this.getThumbSize();
var transform = this.getThumbTransform(orientation, thumbSize);
var scrollThumbOverWidth = Math.max(this.props.scrollThumbOverWidth, this.props.scrollThumbWidth);
var thumbRadius = this.props.scrollThumbRadius !== undefined ? this.props.scrollThumbRadius : this.props.scrollThumbWidth;
var thumbStyle = (0, _objectAssign2.default)({}, this.props.scrollThumbStyle, (_assign = {}, _defineProperty(_assign, sizeName, thumbSize), _defineProperty(_assign, otherSizeName, this.state.over || this.state.active || this.props.alwaysShowTrack ? scrollThumbOverWidth : this.props.scrollThumbWidth), _defineProperty(_assign, 'transform', transform), _defineProperty(_assign, 'borderRadius', thumbRadius), _defineProperty(_assign, 'transitionDuration', this.props.scrollTrackOverTransitionDuration), _assign));
var trackSides = TRACK_SIDES[orientation];
var scrollThumbMargin = this.props.scrollThumbMargin || 0;
if (this.props.scrollThumbMargin) {
style[MARGINS[orientation]] = scrollThumbMargin;
if (this.props.scrollThumbStartEndRespectMargin) {
style[trackSides[0]] = this.props.scrollThumbMargin;
}
}
style[trackSides[1]] = scrollThumbMargin + (this.props.oppositeVisible ? this.props.scrollThumbWidth : 0);
var thumbClassName = 'zippy-react-scroll-container__thumb zippy-react-scroll-container__thumb--orientation-' + orientation;
var trackClassName = 'zippy-react-scroll-container__track zippy-react-scroll-container__track--orientation-' + orientation;
var trackVisible = this.props.alwaysShowTrack || thumbVisible && this.state.over || this.props.showTrackOnDrag && this.state.active;
if (trackVisible) {
trackClassName += ' zippy-react-scroll-container__track--visible';
}
var trackStyle = _defineProperty({
pointerEvents: 'none'
}, otherSizeName, scrollThumbOverWidth);
if (this.props.dragToScroll) {
if (thumbVisible) {
// when the thumb is visible, we should start
// accepting mouseenter/mouseleave events, so activate pointer events
trackStyle.pointerEvents = 'all';
}
trackStyle.borderRadius = thumbRadius;
trackClassName += ' zippy-react-scroll-container__track--drag-to-scroll';
if (this.state.active) {
// set it to be same as thumb, so as not to flicker cursor when
// dragging & moving outside the thumb region
trackStyle.cursor = 'auto';
}
}
var onThumbMouseDown = thumbVisible && this.props.dragToScroll ? this.onThumbMouseDown : null;
var onTrackClick = thumbVisible && this.props.dragToScroll ? this.onTrackClick : null;
var onTrackWheel = this.props.dragToScroll && this.state.over ? this.onTrackWheel : null;
return _react2.default.createElement(
'div',
_extends({}, divProps, {
style: style,
className: className,
'data-orientation': orientation
}),
_react2.default.createElement(
'div',
{
ref: this.refTrack,
style: trackStyle,
className: trackClassName,
onClick: onTrackClick,
onWheel: onTrackWheel,
onMouseEnter: this.props.dragToScroll ? this.onMouseEnter : null,
onMouseLeave: this.props.dragToScroll ? this.onMouseLeave : null
},
_react2.default.createElement('div', {
ref: this.refThumb,
style: thumbStyle,
className: thumbClassName,
onMouseDown: onThumbMouseDown
})
)
);
}
}, {
key: 'setOver',
value: function setOver(over) {
var _this2 = this;
var doSetOver = function doSetOver(overValue) {
_this2.setState({ over: overValue }, function () {
if (!overValue) {
if (_this2.visible !== _this2.state.visible) {
_this2.setVisible(_this2.visible);
}
}
});
};
if (this.setOverFalseTimeoutId) {
clearTimeout(this.setOverFalseTimeoutId);
}
if (!over) {
this.setOverFalseTimeoutId = setTimeout(function () {
doSetOver(false);
}, 500);
} else {
this.setOverFalseTimeoutId = setTimeout(function () {
doSetOver(true);
}, 35);
}
}
}, {
key: 'onMouseEnter',
value: function onMouseEnter() {
this.setOver(true);
}
}, {
key: 'onMouseLeave',
value: function onMouseLeave() {
this.setOver(false);
}
}, {
key: 'onTrackWheel',
value: function onTrackWheel(event) {
// ideally the wheel event should be forwarded to the scroll node so scrolling happens naturally
var delta = this.orientation == VERTICAL ? event.deltaY : event.deltaX;
event.preventDefault();
// in order to prevent browser BACK navigation on mac
this.props.onWheelScroll(this.orientation, delta, event);
}
}, {
key: 'onTrackClick',
value: function onTrackClick(event) {
var target = event.target;
if (target != this.trackNode) {
return;
}
var rect = target.getBoundingClientRect();
var offsetX = event.clientX - rect.left;
var offsetY = event.clientY - rect.top;
var pos = this.orientation == VERTICAL ? offsetY : offsetX;
var thumbPos = this.getThumbPosition();
var direction = pos > thumbPos ? 1 : -1;
if (this.props.onPageScroll) {
this.props.onPageScroll(this.orientation, direction);
}
}
}, {
key: 'getCoord',
value: function getCoord(event) {
return this.orientation == VERTICAL ? event.pageY : event.pageX;
}
}, {
key: 'getThumbPosition',
value: function getThumbPosition() {
return this.scrollPosToThumbPos(this.getScrollPos(), this.orientation, this.getThumbSize());
}
}, {
key: 'onThumbMouseDown',
value: function onThumbMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.initialPos = this.getCoord(event);
this.thumbSize = this.getThumbSize();
this.initialThumbPos = this.getThumbPosition();
this.setState({ active: true });
global.addEventListener('mousemove', this.onMouseMove);
global.addEventListener('mouseup', this.onMouseUp);
this.props.onStartDrag(this.orientation);
}
}, {
key: 'onMouseMove',
value: function onMouseMove(event) {
var pos = this.getCoord(event);
var diff = pos - this.initialPos;
var scrollPos = this.thumbPosToScrollPos(this.initialThumbPos + diff, this.orientation, this.thumbSize);
if (this.orientation == VERTICAL) {
this.onScrollThumbScrollTop(scrollPos);
} else {
this.onScrollThumbScrollLeft(scrollPos);
}
}
}, {
key: 'onScrollThumbScrollTop',
value: function onScrollThumbScrollTop(scrollPos) {
this.props.onScrollThumbScrollTop(scrollPos);
}
}, {
key: 'onScrollThumbScrollLeft',
value: function onScrollThumbScrollLeft(scrollPos) {
this.props.onScrollThumbScrollLeft(scrollPos);
}
}, {
key: 'onMouseUp',
value: function onMouseUp() {
global.removeEventListener('mousemove', this.onMouseMove);
global.removeEventListener('mouseup', this.onMouseUp);
this.setState({ active: false });
this.props.onStopDrag(this.orientation);
}
}, {
key: 'getThumbTransform',
value: function getThumbTransform() {
var orientation = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.orientation;
var thumbSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.getThumbSize();
var scrollPos = this.scrollPosToThumbPos(this.getScrollPos(), orientation, thumbSize) || 0;
if (orientation == VERTICAL) {
return 'translate3d(0px, ' + scrollPos + 'px, 0px)';
}
return 'translate3d(' + scrollPos + 'px, 0px, 0px)';
}
}, {
key: 'thumbPosToScrollPos',
value: function thumbPosToScrollPos(thumbPos, orientation, thumbSize) {
var _props = this.props,
clientSize = _props.clientSize,
scrollSize = _props.scrollSize,
trackSize = _props.trackSize;
var scrollPos = thumbPos * (scrollSize - clientSize) / (trackSize - thumbSize);
return scrollPos;
}
}, {
key: 'scrollPosToThumbPos',
value: function scrollPosToThumbPos(scrollPos, orientation, thumbSize) {
var _props2 = this.props,
clientSize = _props2.clientSize,
scrollSize = _props2.scrollSize,
trackSize = _props2.trackSize;
var thumbPos = scrollPos / (scrollSize - clientSize) * (trackSize - thumbSize);
return Math.floor(thumbPos);
}
}, {
key: 'getRatio',
value: function getRatio() {
return this.props.trackSize / this.props.clientSize;
}
}, {
key: 'getThumbSize',
value: function getThumbSize() {
var _props3 = this.props,
scrollSize = _props3.scrollSize,
clientSize = _props3.clientSize;
/*
* thumbSize clientSize
* --------- = --------
* trackSize scrollSize
*/
var thumbSize = scrollSize ? clientSize * clientSize / scrollSize : 0;
thumbSize *= this.getRatio();
thumbSize = Math.max(thumbSize, this.props.scrollThumbMinSize);
return thumbSize || 0;
}
}]);
return ZippyScrollbar;
}(_react.Component);
exports.default = ZippyScrollbar;
ZippyScrollbar.defaultProps = {
onScrollThumbScrollTop: function onScrollThumbScrollTop() {},
onScrollThumbScrollLeft: function onScrollThumbScrollLeft() {},
onStartDrag: function onStartDrag() {},
onStopDrag: function onStopDrag() {},
scrollThumbMargin: 2,
scrollThumbMinSize: 10,
showTrackOnDrag: false
};
ZippyScrollbar.propTypes = {
alwaysShowTrack: _propTypes2.default.bool,
autoHide: _propTypes2.default.bool,
clientSize: _propTypes2.default.number,
dragToScroll: _propTypes2.default.bool,
emptyScrollOffset: _propTypes2.default.number,
hideTransitionDuration: _propTypes2.default.string,
horizontal: _propTypes2.default.bool,
showTrackOnDrag: _propTypes2.default.bool,
onStartDrag: _propTypes2.default.func,
onStopDrag: _propTypes2.default.func,
onPageScroll: _propTypes2.default.func,
onScrollThumbScrollLeft: _propTypes2.default.func,
onScrollThumbScrollTop: _propTypes2.default.func,
onWheelScroll: _propTypes2.default.func,
oppositeVisible: _propTypes2.default.bool,
// true when the other scrollbar is also visible
orientation: _propTypes2.default.oneOf([VERTICAL, HORIZONTAL]),
nativeScrollbarWidth: _propTypes2.default.number.isRequired,
scrollSize: _propTypes2.default.number,
scrollThumbMargin: _propTypes2.default.number.isRequired,
scrollThumbMinSize: _propTypes2.default.number,
scrollThumbOverWidth: _propTypes2.default.number,
scrollThumbRadius: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]),
scrollThumbStartEndRespectMargin: _propTypes2.default.bool,
scrollThumbStyle: _propTypes2.default.shape(),
scrollThumbWidth: _propTypes2.default.number,
scrollTrackOverTransitionDuration: _propTypes2.default.string,
showTransitionDuration: _propTypes2.default.string,
size: _propTypes2.default.number,
trackSize: _propTypes2.default.number,
vertical: _propTypes2.default.bool,
visible: _propTypes2.default.bool
};