react-flexigrid
Version:
A React table component designed to allow presenting millions of rows of data.
431 lines (350 loc) • 12.8 kB
JavaScript
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
/* eslint-disable react/require-default-props */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import keys from 'rc-util/lib/KeyCode';
import { shallowEqual, translateDOMPosition, clamp } from './utils';
import WheelHandler from './dom/WheelHandler';
import MouseMoveTracker from './dom/MouseMoveTracker';
var FACE_MARGIN = 4;
var FACE_MARGIN_2X = FACE_MARGIN * 2;
var SCROLLBAR_SIZE = 15;
var FACE_SIZE_MIN = 30;
var KEYBOARD_SCROLL_AMOUNT = 40;
var lastScrolledScrollbar = null;
var Scrollbar = function (_React$Component) {
_inherits(Scrollbar, _React$Component);
function Scrollbar(props) {
_classCallCheck(this, Scrollbar);
var _this = _possibleConstructorReturn(this, _React$Component.call(this, props));
_this.onWheel = function (delta) {
_this.setNextState(_this.calculateState(_this.state.position + delta));
};
_this.onWheelX = function (deltaX) {
_this.onWheel(deltaX);
};
_this.onWheelY = function (deltaX, deltaY) {
_this.onWheel(deltaY);
};
_this.onMouseDown = function (e) {
var nextState = void 0;
if (e.target !== _this.faceElem) {
var nativeEvent = e.nativeEvent;
var position = _this.state.horizontal ? nativeEvent.offsetX || nativeEvent.layerX : nativeEvent.offsetY || nativeEvent.layerY;
// mousedown on the scroll-track directly, move the
// center of the scroll-face to the mouse position.
position /= _this.state.scale;
nextState = _this.calculateState(position - _this.state.faceSize * 0.5 / _this.state.scale);
} else {
nextState = {};
}
nextState.focused = true;
_this.setNextState(nextState);
_this.mouseMoveTracker.capture(e);
// focus the container so it may receive keyboard events
_this.containerElem.focus();
};
_this.onMouseMove = function (deltaX, deltaY) {
var delta = _this.state.horizontal ? deltaX : deltaY;
if (delta !== 0) {
delta /= _this.state.scale;
_this.setNextState(_this.calculateState(_this.state.position + delta));
}
};
_this.onMouseMoveEnd = function () {
_this.mouseMoveTracker.release();
_this.setState({ dragging: false, focused: false });
};
_this.onKeyDown = function (e) {
var keyCode = e.keyCode;
// let focus move off the scrollbar
if (keyCode === keys.TAB) {
return;
}
var distance = KEYBOARD_SCROLL_AMOUNT;
var direction = 0;
if (_this.state.horizontal) {
switch (keyCode) {
case keys.HOME:
direction = -1;
distance = _this.props.contentSize;
break;
case keys.LEFT:
direction = -1;
break;
case keys.RIGHT:
direction = 1;
break;
default:
return;
}
} else {
switch (keyCode) {
case keys.SPACE:
if (e.shiftKey) {
direction = -1;
} else {
direction = 1;
}
break;
case keys.HOME:
direction = -1;
distance = _this.props.contentSize;
break;
case keys.UP:
direction = -1;
break;
case keys.DOWN:
direction = 1;
break;
case keys.PAGE_UP:
direction = -1;
distance = _this.props.size;
break;
case keys.PAGE_DOWN:
direction = 1;
distance = _this.props.size;
break;
default:
return;
}
}
e.preventDefault();
_this.setNextState(_this.calculateState(_this.state.position + distance * direction));
};
_this.onFocus = function () {
_this.setState({ focused: true });
};
_this.onBlur = function () {
_this.setState({ focused: false });
};
_this.shouldHandleScrollX = function (delta) {
return _this.props.orientation === 'horizontal' ? _this.shouldHandleScrollChange(delta) : false;
};
_this.shouldHandleScrollY = function (delta) {
return _this.props.orientation !== 'horizontal' ? _this.shouldHandleScrollChange(delta) : false;
};
_this.triggerOnScrollCallback = function () {
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _this.state.position;
if (_this.props.onScroll) {
_this.props.onScroll(position);
}
};
_this.state = _this.calculateState(props.position || props.defaultPosition || 0);
return _this;
}
Scrollbar.prototype.componentWillMount = function componentWillMount() {
var onWheel = this.state.horizontal ? this.onWheelX : this.onWheelY;
this.wheelHandler = new WheelHandler(onWheel, this.shouldHandleScrollX, this.shouldHandleScrollY);
this.initialRender = true;
};
Scrollbar.prototype.componentDidMount = function componentDidMount() {
this.mouseMoveTracker = new MouseMoveTracker(document.documentElement, this.onMouseMove, this.onMouseMoveEnd);
if (this.props.position !== undefined && this.state.position !== this.props.position) {
this.triggerOnScrollCallback();
}
this.initialRender = false;
};
Scrollbar.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
if (!shallowEqual(this.props, nextProps)) {
var controlledPosition = nextProps.position;
if (controlledPosition === undefined) {
this.setNextState(this.calculateState(this.state.position, nextProps));
} else {
this.setNextState(this.calculateState(controlledPosition, nextProps), nextProps);
}
}
};
Scrollbar.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
};
Scrollbar.prototype.componentWillUnmount = function componentWillUnmount() {
this.mouseMoveTracker.release();
delete this.mouseMoveTracker;
if (lastScrolledScrollbar === this) {
lastScrolledScrollbar = null;
}
};
Scrollbar.prototype.setNextState = function setNextState(nextState) {
var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props;
var controlledPosition = props.position;
var willScroll = this.state.position !== nextState.position;
var callback = willScroll ? this.triggerOnScrollCallback : undefined;
if (controlledPosition === undefined) {
this.setState(nextState, callback);
} else if (controlledPosition === nextState.position) {
this.setState(nextState);
} else {
// Scrolling is controlled.
// Don't update the state and let the owner to update the scrollbar instead.
if (nextState.position !== undefined && nextState.position !== this.state.position) {
callback(nextState.position);
}
return;
}
if (willScroll && lastScrolledScrollbar !== this) {
if (lastScrolledScrollbar) {
lastScrolledScrollbar.blur();
}
lastScrolledScrollbar = this;
}
};
Scrollbar.prototype.calculateState = function calculateState(position) {
var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props;
var size = props.size,
contentSize = props.contentSize,
orientation = props.orientation;
// unscrollable
if (size < 1 || contentSize <= size) {
return {
position: 0,
scrollable: false
};
}
var cachedStateKey = position + '_' + size + '_' + contentSize + '_' + orientation;
if (this.cachedStateKey === cachedStateKey) {
return this.cachedState;
}
// There are two types of positions here.
// 1) Phisical position: changed by mouse or keyboard
// 2) Logical position: changed by props.
// The logical position will be kept as as internal state and the `render()`
// function will translate it into physical position to render.
var scale = size / contentSize;
var faceSize = size * scale;
if (faceSize < FACE_SIZE_MIN) {
scale = (size - FACE_SIZE_MIN) / (contentSize - size);
faceSize = FACE_SIZE_MIN;
}
var maxPosition = contentSize - size;
position = clamp(position, 0, maxPosition); // eslint-disable-line
// This function should only return flat values that can be
// compared quiclky by `shallowEqual`.
var state = {
scale: scale,
faceSize: faceSize,
position: position,
dragging: this.mouseMoveTracker ? this.mouseMoveTracker.isDragging() : false,
horizontal: orientation === 'horizontal',
scrollable: true
// cache the state
};this.cachedStateKey = cachedStateKey;
this.cachedState = state;
return state;
};
Scrollbar.prototype.shouldHandleScrollChange = function shouldHandleScrollChange(delta) {
var nextState = this.calculateState(this.state.position + delta);
return nextState.position !== this.state.position;
};
Scrollbar.prototype.blur = function blur() {
if (!this.containerElem) {
return;
}
try {
this.onBlur();
this.containerElem.blur();
} catch (oops) {
// pass
}
};
Scrollbar.prototype.scrollBy = function scrollBy(delta) {
this.onWheel(delta);
};
Scrollbar.prototype.render = function render() {
var _classNames,
_classNames2,
_this2 = this;
if (!this.state.scrollable) {
return null;
}
var _props = this.props,
prefixCls = _props.prefixCls,
size = _props.size,
opaque = _props.opaque;
var faceSize = this.state.faceSize;
var position = this.state.position * this.state.scale + FACE_MARGIN;
var active = this.state.focused || this.state.dragging;
var horizontal = this.state.horizontal;
var mainClassName = classNames((_classNames = {}, _classNames[prefixCls + '-scroll'] = true, _classNames.vertical = !horizontal, _classNames.horizontal = horizontal, _classNames.opaque = opaque, _classNames.active = active, _classNames));
var faceClassName = classNames((_classNames2 = {}, _classNames2[prefixCls + '-scroll-face'] = true, _classNames2));
var mainStyle = void 0;
var faceStyle = void 0;
if (horizontal) {
mainStyle = {
left: this.props.left,
width: size,
height: SCROLLBAR_SIZE
};
faceStyle = {
width: faceSize - FACE_MARGIN_2X
};
translateDOMPosition(faceStyle, position, 0, this.initialRender);
} else {
mainStyle = {
top: this.props.top,
width: SCROLLBAR_SIZE,
height: size
};
faceStyle = {
height: faceSize - FACE_MARGIN_2X
};
translateDOMPosition(faceStyle, 0, position, this.initialRender);
}
if (this.props.zIndex) {
mainStyle.zIndex = this.props.zIndex;
}
if (this.props.trackColor === 'gray') {
mainStyle.backgroundColor = '#f6f7f8';
}
return React.createElement(
'div',
{ // eslint-disable-line
style: mainStyle,
className: mainClassName,
tabIndex: 0 // eslint-disable-line
, onBlur: this.onBlur,
onFocus: this.onFocus,
onKeyDown: this.onKeyDown,
onMouseDown: this.onMouseDown,
onWheel: this.wheelHandler.onWheel,
ref: function ref(containerElem) {
_this2.containerElem = containerElem;
}
},
React.createElement('div', {
style: faceStyle,
className: faceClassName,
ref: function ref(faceElem) {
_this2.faceElem = faceElem;
}
})
);
};
return Scrollbar;
}(React.Component);
Scrollbar.SIZE = SCROLLBAR_SIZE;
Scrollbar.OFFSET = FACE_MARGIN / 2 + 1;
Scrollbar.KEYBOARD_SCROLL_AMOUNT = KEYBOARD_SCROLL_AMOUNT;
Scrollbar.propTypes = {
size: PropTypes.number.isRequired,
contentSize: PropTypes.number.isRequired,
position: PropTypes.number,
defaultPosition: PropTypes.number,
opaque: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']),
trackColor: PropTypes.oneOf(['gray']),
zIndex: PropTypes.number,
top: PropTypes.number, // top position of vertical bar
left: PropTypes.number, // left position of horizontal bar
onScroll: PropTypes.func
};
Scrollbar.defaultProps = {
defaultPosition: 0,
opaque: true,
zIndex: 99,
orientation: 'vertical'
};
export default Scrollbar;