Skip to content

Instantly share code, notes, and snippets.

@jwaldrip
Created April 26, 2016 04:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jwaldrip/14b412bf8e6a2ed48e5c2f7c6a4ffea6 to your computer and use it in GitHub Desktop.
Save jwaldrip/14b412bf8e6a2ed48e5c2f7c6a4ffea6 to your computer and use it in GitHub Desktop.
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import Relay from 'react-relay';
const nullNode = document.createElement('div');
class Infinity extends React.Component {
static propTypes = {
Component: React.PropTypes.func.isRequired,
totalCount: React.PropTypes.number.isRequired,
list: React.PropTypes.array.isRequired,
fetch: React.PropTypes.func
};
static initialState = {
startRow: 0,
visibleRows: 1,
itemsPerRow: 1,
rowHeight: 0
};
constructor(...args) {
super(...args);
this.state = Infinity.initialState;
}
get calculatedItemMarginLeft() {
const firstNode = this.findFirstNode();
return firstNode ?
parseInt(getComputedStyle(firstNode).marginLeft, 10) : 0;
}
get calculatedItemMarginTop() {
const firstNode = this.findFirstNode();
return firstNode ?
parseInt(getComputedStyle(firstNode).marginTop, 10) : 0;
}
get calculatedItemMarginRight() {
const firstNode = this.findFirstNode();
return firstNode ?
parseInt(getComputedStyle(firstNode).marginRight, 10) : 0;
}
get calculatedItemMarginBottom() {
const firstNode = this.findFirstNode();
return firstNode ?
parseInt(getComputedStyle(firstNode).marginBottom, 10) : 0;
}
get calculatedItemHeight() {
const firstNode = this.findFirstNode();
const { calculatedItemMarginTop, calculatedItemMarginBottom } = this;
const height = firstNode ?
parseInt(getComputedStyle(firstNode).height, 10) : 0;
return calculatedItemMarginTop + height + calculatedItemMarginBottom;
}
get calculatedItemWidth() {
const firstNode = this.findFirstNode();
const { calculatedItemMarginLeft, calculatedItemMarginRight } = this;
const width = firstNode ?
parseInt(getComputedStyle(firstNode).width, 10) : 0;
return calculatedItemMarginLeft + width + calculatedItemMarginRight;
}
get calculatedItemsPerRow() {
const containerWidth = this.findContainerNode().offsetWidth;
const itemWidth = this.calculatedItemWidth;
if (itemWidth === 0 || containerWidth === 0) {
return 1;
}
return Math.floor(containerWidth / itemWidth);
}
get calculatedStartRow() {
const containerTop = this.findContainerNode().getBoundingClientRect().top;
if (this.calculatedItemHeight === 0 || containerTop > 0) {
return 0;
}
const calculatedRow = Math.floor(-containerTop / this.calculatedItemHeight);
const rowAboveZero = Math.max(calculatedRow, 0);
const maxPossibleRow = Math.abs(this.totalRowCount - 1);
return Math.min(maxPossibleRow, rowAboveZero);
}
get calculatedVisibleRows() {
const containerTop = this.findContainerNode().getBoundingClientRect().top;
const containerBottom = this.findContainerNode().getBoundingClientRect().bottom;
if (this.calculatedItemHeight === 0 || containerBottom < 0) {
return 1;
}
const top = Math.max(0, containerTop);
const bottom = Math.min(window.innerHeight, containerBottom);
return Math.max(Math.ceil((bottom - top) / this.calculatedItemHeight), 1);
}
get endIndex() {
const { startRow, visibleRows, itemsPerRow } = this.state;
return (startRow + visibleRows) * itemsPerRow;
}
get startIndex() {
const { startRow, itemsPerRow } = this.state;
return startRow * itemsPerRow;
}
get totalRowCount() {
return Math.ceil(this.props.totalCount / this.state.itemsPerRow);
}
get fullList() {
return _.times(this.props.totalCount, index => this.props.list[index]);
}
findNodes() {
const { startIndex, endIndex } = this;
return this.fullList.slice(startIndex, endIndex).map(
(item, index) => {
index += startIndex;
const ref = `item_${index}`;
return ReactDOM.findDOMNode(this.refs[ref]);
}
).filter(Boolean);
}
findLastNode() {
return this.findNodes().reverse()[0];
}
findFirstNode() {
return this.findNodes()[0];
}
findTopBufferNode() {
return ReactDOM.findDOMNode(this.refs.topBuffer) || nullNode;
}
findBottomBufferNode() {
return ReactDOM.findDOMNode(this.refs.bottomBuffer) || nullNode;
}
findContainerNode() {
return ReactDOM.findDOMNode(this.refs.infiniteContainer) || nullNode;
}
hidePlaceholderAssets() {
const { Component } = this.props;
Object.keys(this.refs).map(
key => this.refs[key]
).forEach(node => {
const isComponent = node instanceof Component;
if (!isComponent) {
return;
}
const { isPlaceholder } = node.props;
const domNode = ReactDOM.findDOMNode(node) || nullNode;
const isHidden = domNode.style.visibility === 'hidden';
if (isPlaceholder || isHidden) {
domNode.style.visibility = isPlaceholder ? 'hidden' : 'visible';
}
});
}
renderTopBuffer() {
const { startRow, rowHeight } = this.state;
const height = startRow * rowHeight || 0;
return <div ref="topBuffer" style={{ height }} />;
}
renderVisibleItems() {
const { startIndex, endIndex } = this;
let { Component, getKey } = this.props;
if (!getKey) {
getKey = item => item.id;
}
return this.fullList.slice(startIndex, endIndex).map((item, index) => {
let isPlaceholder = false;
index += startIndex;
if (item === undefined) {
item = this.fullList[0];
isPlaceholder = true;
}
const key = _.uniqueId('inifiniteItem');
const ref = `item_${index}`;
return <Component key={key} ref={ref} isPlaceholder={isPlaceholder} {...item} />;
});
}
renderBottomBuffer() {
const { startRow, visibleRows, rowHeight } = this.state;
const { totalRowCount } = this;
const endRow = visibleRows + startRow;
const height = endRow > totalRowCount ? 0 : (totalRowCount - endRow) * rowHeight || 0;
return <div ref="bottomBuffer" style={{ height }} />;
}
trackResize = () => {
this.setState(Infinity.initialState);
};
trackScroll = () => {
clearTimeout(this.scroll);
this.scroll = setTimeout(this.calculatePositions.bind(this), 10);
};
updateStateIfChanged(newState) {
if (Object.keys(newState).find(k => this.state[k] !== newState[k])) {
this.setState(newState);
}
}
calculatePositions() {
this.hidePlaceholderAssets();
const {
calculatedStartRow,
calculatedItemHeight,
calculatedVisibleRows,
calculatedItemsPerRow,
endIndex,
} = this;
// Fetch more if needed
const allowFetch = () => { this.fetching = false; };
const { fetch: fetchMore, list, totalCount } = this.props;
if (endIndex >= list.length && list.length <= totalCount && !this.fetching) {
this.fetching = true;
fetchMore().then(allowFetch).catch(allowFetch);
}
this.updateStateIfChanged({
startRow: calculatedStartRow,
visibleRows: calculatedVisibleRows,
rowHeight: calculatedItemHeight,
itemsPerRow: calculatedItemsPerRow
});
}
componentWillMount() {
window.addEventListener('resize', this.trackResize);
window.addEventListener('scroll', this.trackScroll);
}
componentWillUnmount() {
window.removeEventListener('resize', this.trackResize);
window.removeEventListener('scroll', this.trackScroll);
}
componentDidMount() {
this.calculatePositions();
}
componentDidUpdate() {
this.calculatePositions();
}
render() {
return (
<div ref="infiniteContainer">
{this.renderTopBuffer()}
{this.renderVisibleItems()}
{this.renderBottomBuffer()}
</div>
);
}
}
class Asset extends React.Component {
render() {
const style = {
background: `center center no-repeat url(${this.props.asset.thumbnail_url})`,
width: 250,
height: 180,
margin: 10,
textAlign: 'center',
display: 'inline-block',
border: '1px solid black'
};
return (
<div style={style}>
{this.props.asset.apiId}
</div>
);
}
}
class Section extends React.Component {
renderAsset({ node: asset }) {
return (
<Asset asset={asset} />
);
}
loadMoreAssets = () => {
const { relay } = this.props;
const { assetLimit } = relay.variables;
return new Promise((resolve, reject) => {
relay.setVariables(
{ assetLimit: assetLimit + 50 }, ({ done, error, aborted, stale }) => {
if (done || stale) {
return resolve();
}
if (error) {
return reject(error);
}
if (aborted) {
return reject(new Error('the request was aborted'));
}
}
);
});
};
loadMoreAssetsWithTimeout = () => {
this.sectionLoadTimeout = setTimeout(this.loadMoreAssets, 100);
};
componentWillUnmount() {
clearTimeout(this.sectionLoadTimeout);
}
loadAssetsUntilLoaded() {
if (this.props.section.assets.pageInfo.hasNextPage) {
return this.loadMoreAssets().then(
this.loadAssetsUntilLoaded.bind(this)
);
}
return Promise.resolve();
}
componentDidMount() {
this.loadAssetsUntilLoaded();
}
render() {
const { name, assets, number_of_assets: totalCount } = this.props.section;
return (
<div>
<h1>{name}</h1>
<Infinity
Component={Asset}
list={assets.edges.map(({ node }) => ({ asset: node }))}
totalCount={totalCount}
fetch={this.loadMoreAssets}
/>
</div>
);
}
}
class InfinityPage extends React.Component {
renderSection({ node: section }) {
return (
<SectionRelayContainer section={section} />
);
}
loadMoreSections = () => {
if (this.props.brandfolder.sections.pageInfo.hasNextPage) {
const { sectionLimit } = this.props.relay.variables;
this.props.relay.setVariables(
{ sectionLimit: sectionLimit + 5 }, ({ done }) => {
if (done) {
this.loadMoreSectionsWithTimeout();
}
}
);
}
};
loadMoreSectionsWithTimeout = () => {
this.sectionLoadTimeout = setTimeout(this.loadMoreSections, 100);
};
componentWillUnmount() {
clearTimeout(this.sectionLoadTimeout);
}
componentDidMount() {
this.loadMoreSectionsWithTimeout();
}
renderSections() {
return this.props.brandfolder.sections.edges.map(
({ node: section }) => <SectionRelayContainer key={section.id} section={section} />
);
}
render() {
return (
<div>
{this.renderSections()}
</div>
);
}
}
const SectionRelayContainer = Relay.createContainer(Section, {
initialVariables: {
assetLimit: 50,
},
fragments: {
section: () => Relay.QL`
fragment on Section {
id
name
number_of_assets
assets(first: $assetLimit) {
pageInfo {
hasNextPage
}
edges {
node {
id
apiId
thumbnail_url
}
}
}
}
`
}
});
export const InfinityPageRelayContainer = Relay.createContainer(InfinityPage, {
initialVariables: {
sectionLimit: 5
},
fragments: {
brandfolder: () => Relay.QL`
fragment on Brandfolder {
sections(first: $sectionLimit) {
pageInfo {
hasNextPage
}
edges {
node {
id
name
${SectionRelayContainer.getFragment('section')}
}
}
}
}
`
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment