UNPKG

fixed-data-table-one.com

Version:

A React table component designed to allow presenting thousands of rows of data.

465 lines (389 loc) 14.7 kB
'use strict'; var _DOMMouseMoveTracker = require('./DOMMouseMoveTracker'); var _DOMMouseMoveTracker2 = _interopRequireDefault(_DOMMouseMoveTracker); var _Keys = require('./Keys'); var _Keys2 = _interopRequireDefault(_Keys); var _React = require('./React'); var _React2 = _interopRequireDefault(_React); var _createReactClass = require('create-react-class'); var _createReactClass2 = _interopRequireDefault(_createReactClass); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _ReactDOM = require('./ReactDOM'); var _ReactDOM2 = _interopRequireDefault(_ReactDOM); var _ReactComponentWithPureRenderMixin = require('./ReactComponentWithPureRenderMixin'); var _ReactComponentWithPureRenderMixin2 = _interopRequireDefault(_ReactComponentWithPureRenderMixin); var _ReactWheelHandler = require('./ReactWheelHandler'); var _ReactWheelHandler2 = _interopRequireDefault(_ReactWheelHandler); var _cssVar = require('./cssVar'); var _cssVar2 = _interopRequireDefault(_cssVar); var _cx = require('./cx'); var _cx2 = _interopRequireDefault(_cx); var _emptyFunction = require('./emptyFunction'); var _emptyFunction2 = _interopRequireDefault(_emptyFunction); var _FixedDataTableTranslateDOMPosition = require('./FixedDataTableTranslateDOMPosition'); var _FixedDataTableTranslateDOMPosition2 = _interopRequireDefault(_FixedDataTableTranslateDOMPosition); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Copyright Schrodinger, LLC * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule Scrollbar * @typechecks */ var UNSCROLLABLE_STATE = { position: 0, scrollable: false }; var FACE_MARGIN = parseInt((0, _cssVar2.default)('scrollbar-face-margin'), 10); var FACE_MARGIN_2 = FACE_MARGIN * 2; var FACE_SIZE_MIN = 30; var KEYBOARD_SCROLL_AMOUNT = 40; var _lastScrolledScrollbar = null; var Scrollbar = (0, _createReactClass2.default)({ displayName: 'Scrollbar', mixins: [_ReactComponentWithPureRenderMixin2.default], propTypes: { contentSize: _propTypes2.default.number.isRequired, defaultPosition: _propTypes2.default.number, isOpaque: _propTypes2.default.bool, orientation: _propTypes2.default.oneOf(['vertical', 'horizontal']), onScroll: _propTypes2.default.func, position: _propTypes2.default.number, size: _propTypes2.default.number.isRequired, trackColor: _propTypes2.default.oneOf(['gray']), zIndex: _propTypes2.default.number, verticalTop: _propTypes2.default.number }, getInitialState: function getInitialState() /*object*/{ var props = this.props; return this._calculateState(props.position || props.defaultPosition || 0, props.size, props.contentSize, props.orientation); }, componentWillReceiveProps: function componentWillReceiveProps( /*object*/nextProps) { var controlledPosition = nextProps.position; if (controlledPosition === undefined) { this._setNextState(this._calculateState(this.state.position, nextProps.size, nextProps.contentSize, nextProps.orientation)); } else { this._setNextState(this._calculateState(controlledPosition, nextProps.size, nextProps.contentSize, nextProps.orientation), nextProps); } }, getDefaultProps: function getDefaultProps() /*object*/{ return { defaultPosition: 0, isOpaque: false, onScroll: _emptyFunction2.default, orientation: 'vertical', zIndex: 99 }; }, render: function render() /*?object*/{ if (!this.state.scrollable) { return null; } var size = this.props.size; var mainStyle; var faceStyle; var isHorizontal = this.state.isHorizontal; var isVertical = !isHorizontal; var isActive = this.state.focused || this.state.isDragging; var faceSize = this.state.faceSize; var isOpaque = this.props.isOpaque; var verticalTop = this.props.verticalTop || 0; var mainClassName = (0, _cx2.default)({ 'ScrollbarLayout/main': true, 'ScrollbarLayout/mainVertical': isVertical, 'ScrollbarLayout/mainHorizontal': isHorizontal, 'public/Scrollbar/main': true, 'public/Scrollbar/mainOpaque': isOpaque, 'public/Scrollbar/mainActive': isActive }); var faceClassName = (0, _cx2.default)({ 'ScrollbarLayout/face': true, 'ScrollbarLayout/faceHorizontal': isHorizontal, 'ScrollbarLayout/faceVertical': isVertical, 'public/Scrollbar/faceActive': isActive, 'public/Scrollbar/face': true }); var position = this.state.position * this.state.scale + FACE_MARGIN; if (isHorizontal) { mainStyle = { width: size }; faceStyle = { width: faceSize - FACE_MARGIN_2 }; (0, _FixedDataTableTranslateDOMPosition2.default)(faceStyle, position, 0, this._initialRender); } else { mainStyle = { top: verticalTop, height: size }; faceStyle = { height: faceSize - FACE_MARGIN_2 }; (0, _FixedDataTableTranslateDOMPosition2.default)(faceStyle, 0, position, this._initialRender); } mainStyle.zIndex = this.props.zIndex; if (this.props.trackColor === 'gray') { mainStyle.backgroundColor = (0, _cssVar2.default)('fbui-desktop-background-light'); } return _React2.default.createElement( 'div', { onFocus: this._onFocus, onBlur: this._onBlur, onKeyDown: this._onKeyDown, onMouseDown: this._onMouseDown, onWheel: this._wheelHandler.onWheel, className: mainClassName, style: mainStyle, tabIndex: 0 }, _React2.default.createElement('div', { ref: 'face', className: faceClassName, style: faceStyle }) ); }, componentWillMount: function componentWillMount() { var isHorizontal = this.props.orientation === 'horizontal'; var onWheel = isHorizontal ? this._onWheelX : this._onWheelY; this._wheelHandler = new _ReactWheelHandler2.default(onWheel, this._shouldHandleX, // Should hanlde horizontal scroll this._shouldHandleY // Should handle vertical scroll ); this._initialRender = true; }, componentDidMount: function componentDidMount() { this._mouseMoveTracker = new _DOMMouseMoveTracker2.default(this._onMouseMove, this._onMouseMoveEnd, document.documentElement); if (this.props.position !== undefined && this.state.position !== this.props.position) { this._didScroll(); } this._initialRender = false; }, componentWillUnmount: function componentWillUnmount() { this._nextState = null; this._mouseMoveTracker.releaseMouseMoves(); if (_lastScrolledScrollbar === this) { _lastScrolledScrollbar = null; } delete this._mouseMoveTracker; }, scrollBy: function scrollBy( /*number*/delta) { this._onWheel(delta); }, _shouldHandleX: function _shouldHandleX( /*number*/delta) /*boolean*/{ return this.props.orientation === 'horizontal' ? this._shouldHandleChange(delta) : false; }, _shouldHandleY: function _shouldHandleY( /*number*/delta) /*boolean*/{ return this.props.orientation !== 'horizontal' ? this._shouldHandleChange(delta) : false; }, _shouldHandleChange: function _shouldHandleChange( /*number*/delta) /*boolean*/{ var nextState = this._calculateState(this.state.position + delta, this.props.size, this.props.contentSize, this.props.orientation); return nextState.position !== this.state.position; }, _calculateState: function _calculateState( /*number*/position, /*number*/size, /*number*/contentSize, /*string*/orientation) /*object*/{ if (size < 1 || contentSize <= size) { return UNSCROLLABLE_STATE; } var stateKey = position + '_' + size + '_' + contentSize + '_' + orientation; if (this._stateKey === stateKey) { return this._stateForKey; } // There are two types of positions here. // 1) Phisical position: changed by mouse / 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 isHorizontal = orientation === 'horizontal'; 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 scrollable = true; var maxPosition = contentSize - size; if (position < 0) { position = 0; } else if (position > maxPosition) { position = maxPosition; } var isDragging = this._mouseMoveTracker ? this._mouseMoveTracker.isDragging() : false; // This function should only return flat values that can be compared quiclky // by `ReactComponentWithPureRenderMixin`. var state = { faceSize: faceSize, isDragging: isDragging, isHorizontal: isHorizontal, position: position, scale: scale, scrollable: scrollable }; // cache the state for later use. this._stateKey = stateKey; this._stateForKey = state; return state; }, _onWheelY: function _onWheelY( /*number*/deltaX, /*number*/deltaY) { this._onWheel(deltaY); }, _onWheelX: function _onWheelX( /*number*/deltaX, /*number*/deltaY) { this._onWheel(deltaX); }, _onWheel: function _onWheel( /*number*/delta) { var props = this.props; // The mouse may move faster then the animation frame does. // Use `requestAnimationFrame` to avoid over-updating. this._setNextState(this._calculateState(this.state.position + delta, props.size, props.contentSize, props.orientation)); }, _onMouseDown: function _onMouseDown( /*object*/event) { var nextState; if (event.target !== _ReactDOM2.default.findDOMNode(this.refs.face)) { // Both `offsetX` and `layerX` are non-standard DOM property but they are // magically available for browsers somehow. var nativeEvent = event.nativeEvent; var position = this.state.isHorizontal ? 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. var props = this.props; position /= this.state.scale; nextState = this._calculateState(position - this.state.faceSize * 0.5 / this.state.scale, props.size, props.contentSize, props.orientation); } else { nextState = {}; } nextState.focused = true; this._setNextState(nextState); this._mouseMoveTracker.captureMouseMoves(event); // Focus the node so it may receive keyboard event. _ReactDOM2.default.findDOMNode(this).focus(); }, _onMouseMove: function _onMouseMove( /*number*/deltaX, /*number*/deltaY) { var props = this.props; var delta = this.state.isHorizontal ? deltaX : deltaY; delta /= this.state.scale; this._setNextState(this._calculateState(this.state.position + delta, props.size, props.contentSize, props.orientation)); }, _onMouseMoveEnd: function _onMouseMoveEnd() { this._nextState = null; this._mouseMoveTracker.releaseMouseMoves(); this.setState({ isDragging: false }); }, _onKeyDown: function _onKeyDown( /*object*/event) { var keyCode = event.keyCode; if (keyCode === _Keys2.default.TAB) { // Let focus move off the scrollbar. return; } var distance = KEYBOARD_SCROLL_AMOUNT; var direction = 0; if (this.state.isHorizontal) { switch (keyCode) { case _Keys2.default.HOME: direction = -1; distance = this.props.contentSize; break; case _Keys2.default.LEFT: direction = -1; break; case _Keys2.default.RIGHT: direction = 1; break; default: return; } } if (!this.state.isHorizontal) { switch (keyCode) { case _Keys2.default.SPACE: if (event.shiftKey) { direction = -1; } else { direction = 1; } break; case _Keys2.default.HOME: direction = -1; distance = this.props.contentSize; break; case _Keys2.default.UP: direction = -1; break; case _Keys2.default.DOWN: direction = 1; break; case _Keys2.default.PAGE_UP: direction = -1; distance = this.props.size; break; case _Keys2.default.PAGE_DOWN: direction = 1; distance = this.props.size; break; default: return; } } event.preventDefault(); var props = this.props; this._setNextState(this._calculateState(this.state.position + distance * direction, props.size, props.contentSize, props.orientation)); }, _onFocus: function _onFocus() { this.setState({ focused: true }); }, _onBlur: function _onBlur() { this.setState({ focused: false }); }, _blur: function _blur() { var el = _ReactDOM2.default.findDOMNode(this); if (!el) { return; } try { this._onBlur(); el.blur(); } catch (oops) { // pass } }, _setNextState: function _setNextState( /*object*/nextState, /*?object*/props) { props = props || this.props; var controlledPosition = props.position; var willScroll = this.state.position !== nextState.position; if (controlledPosition === undefined) { var callback = willScroll ? this._didScroll : 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) { this.props.onScroll(nextState.position); } return; } if (willScroll && _lastScrolledScrollbar !== this) { _lastScrolledScrollbar && _lastScrolledScrollbar._blur(); _lastScrolledScrollbar = this; } }, _didScroll: function _didScroll() { this.props.onScroll(this.state.position); } }); Scrollbar.KEYBOARD_SCROLL_AMOUNT = KEYBOARD_SCROLL_AMOUNT; Scrollbar.SIZE = parseInt((0, _cssVar2.default)('scrollbar-size'), 10); Scrollbar.OFFSET = FACE_MARGIN / 2 + 1; module.exports = Scrollbar;