Commit fe3fa7ee authored by Benjamin Bouvier's avatar Benjamin Bouvier
Browse files

Client: split infinite list component of the list of operations;

parent 62bf70f5
......@@ -12,20 +12,10 @@ import { AmountWell, FilteredAmountWell } from './amount-well';
import SearchComponent from './search';
import OperationItem from './item';
import SyncButton from './sync-button';
import InfiniteList from '../ui/infinite-list';
import throttle from 'lodash.throttle';
// Height of an operation line (px) based on the css settings
let OPERATION_HEIGHT = setOperationHeight();
// Number of operations before / after the ones to render, for flexibility.
const OPERATION_BALLAST = 10;
// Throttling for the scroll event (ms)
const SCROLL_THROTTLING = 150;
// Number of elements
let INITIAL_SHOW_ITEMS = window.innerHeight / OPERATION_HEIGHT | 0;
// Infinite list properties.
const OPERATION_BALLAST = 5;
// Filter functions used in amount wells.
function noFilter() {
......@@ -38,11 +28,6 @@ function isNegative(op) {
return op.amount < 0;
}
function setOperationHeight() {
// Keep in sync with style.css.
return window.innerWidth < 768 ? 41 : 54;
}
function filterOperationsThisMonth(operations) {
let now = new Date();
return operations.filter(op => {
......@@ -55,50 +40,42 @@ class OperationsComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
firstItemShown: 0,
lastItemShown: INITIAL_SHOW_ITEMS
};
this.handleScroll = throttle(this.onScroll.bind(this), SCROLL_THROTTLING);
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
this.renderItems = this.renderItems.bind(this);
this.computeHeightAbove = this.computeHeightAbove.bind(this);
this.getOperationHeight = this.getOperationHeight.bind(this);
this.getNumItems = this.getNumItems.bind(this);
}
handleResize(e) {
e.preventDefault();
OPERATION_HEIGHT = setOperationHeight();
INITIAL_SHOW_ITEMS = window.innerHeight / OPERATION_HEIGHT | 0;
this.handleScroll();
// Implementation of infinite list.
renderItems(low, high) {
let formatCurrency = this.props.account.formatCurrency;
return this.props.filteredOperations
.slice(low, high)
.map(o =>
<OperationItem key={ o.id }
operation={ o }
formatCurrency={ formatCurrency }
/>);
}
onScroll() {
computeHeightAbove() {
let wellH = ReactDOM.findDOMNode(this.refs.wells).scrollHeight;
let searchH = ReactDOM.findDOMNode(this.refs.search).scrollHeight;
let panelH = ReactDOM.findDOMNode(this.refs.panelHeading).scrollHeight;
let theadH = ReactDOM.findDOMNode(this.refs.thead).scrollHeight;
let fixedTopH = wellH + searchH + panelH + theadH;
let topItemH = Math.max(window.scrollY - fixedTopH, 0);
let bottomItemH = topItemH + window.innerHeight;
return wellH + searchH + panelH + theadH;
}
let firstItemShown = Math.max(topItemH / OPERATION_HEIGHT - OPERATION_BALLAST | 0, 0);
let lastItemShown = (bottomItemH / OPERATION_HEIGHT | 0) + OPERATION_BALLAST;
getOperationHeight() {
// Keep in sync with style.css.
return window.innerWidth < 768 ? 41 : 54;
}
this.setState({
firstItemShown,
lastItemShown
});
getNumItems() {
return this.props.filteredOperations.length;
}
// End of implementation of infinite list.
render() {
// Edge case: the component hasn't retrieved the account yet.
......@@ -106,22 +83,6 @@ class OperationsComponent extends React.Component {
return <div/>;
}
let bufferPreH = OPERATION_HEIGHT * this.state.firstItemShown;
let bufferPre = <tr style={ { height: `${bufferPreH}px` } } />;
let formatCurrency = this.props.account.formatCurrency;
let ops = this.props.filteredOperations
.slice(this.state.firstItemShown, this.state.lastItemShown)
.map(o =>
<OperationItem key={ o.id }
operation={ o }
formatCurrency={ formatCurrency }
/>);
let numOps = this.props.filteredOperations.length;
let bufferPostH = OPERATION_HEIGHT * Math.max(numOps - this.state.lastItemShown, 0);
let bufferPost = <tr style={ { height: `${bufferPostH}px` } } />;
let asOf = $t('client.operations.as_of');
let lastCheckedDate = new Date(this.props.account.lastChecked).toLocaleDateString();
let lastCheckDate = `${asOf} ${lastCheckedDate}`;
......@@ -135,6 +96,8 @@ class OperationsComponent extends React.Component {
wellOperations = filterOperationsThisMonth(this.props.operations);
}
let formatCurrency = this.props.account.formatCurrency;
return (
<div>
<div className="row operation-wells" ref="wells">
......@@ -220,11 +183,13 @@ class OperationsComponent extends React.Component {
</th>
</tr>
</thead>
<tbody>
{ bufferPre }
{ ops }
{ bufferPost }
</tbody>
<InfiniteList
ballast={ OPERATION_BALLAST }
getNumItems={ this.getNumItems }
getItemHeight={ this.getOperationHeight }
getHeightAbove={ this.computeHeightAbove }
renderItems={ this.renderItems }
/>
</table>
</div>
......
import React from 'react';
import throttle from 'lodash.throttle';
// Throttling for the scroll event (ms)
const SCROLL_THROTTLING = 150;
export default class InfiniteList extends React.Component {
constructor(props) {
super(props);
let itemHeight = this.props.getItemHeight();
this.state = {
firstItemShown: 0,
lastItemShown: window.innerHeight / itemHeight,
itemHeight
};
this.handleScroll = throttle(this.handleScroll.bind(this), SCROLL_THROTTLING);
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
handleResize(e) {
e.preventDefault();
this.handleScroll();
}
handleScroll(e) {
e.preventDefault();
let heightAbove = this.props.getHeightAbove();
let topItemH = Math.max(window.scrollY - heightAbove, 0);
let bottomItemH = topItemH + window.innerHeight;
let itemHeight = this.props.getItemHeight();
let ballast = this.props.ballast;
let firstItemShown = Math.max(topItemH / itemHeight - ballast | 0, 0);
let lastItemShown = (bottomItemH / itemHeight | 0) + this.props.ballast;
this.setState({
firstItemShown,
lastItemShown,
itemHeight
});
}
render() {
let bufferPreH = this.state.itemHeight * this.state.firstItemShown;
let items = this.props.renderItems(this.state.firstItemShown, this.state.lastItemShown);
let bufferPostH = this.state.itemHeight *
Math.max(this.props.getNumItems() - this.state.lastItemShown, 0);
return (<tbody>
<tr style={ { height: `${bufferPreH}px` } } />
{ items }
<tr style={ { height: `${bufferPostH}px` } } />
</tbody>);
}
}
InfiniteList.propTypes = {
// Number of operations before / after the ones to render, for fast scroll.
ballast: React.PropTypes.number.isRequired,
// Function returning the total number of items in the list.
getNumItems: React.PropTypes.func.isRequired,
// Function returning the space between the component and window's top.
getItemHeight: React.PropTypes.func.isRequired,
// Function returning the height of a single item in the list.
getHeightAbove: React.PropTypes.func.isRequired,
// Function to be called for rendering all the items, with the signature:
// (firstItemShown: Number, lastItemShown: Number) -> [React elements]
renderItems: React.PropTypes.func.isRequired
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment