Skip to content

Instantly share code, notes, and snippets.

@jordaaash
Created February 24, 2017 02:58
Show Gist options
  • Save jordaaash/10cc469c2dbd872585c45ebaf6053a8d to your computer and use it in GitHub Desktop.
Save jordaaash/10cc469c2dbd872585c45ebaf6053a8d to your computer and use it in GitHub Desktop.
React Immutable virtual list
'use strict';
const call = function (fn) {
return Function.prototype.call.bind(fn);
};
export default call;
'use strict';
import {
fromJS as immutableFromJS,
Iterable
} from 'immutable';
const reviver = function (key, value) {
return Iterable.isIndexed(value) ? value.toList() : value.toOrderedMap();
};
const fromJS = function (value) {
return immutableFromJS(value, reviver);
};
export default fromJS;
'use strict';
import noop from 'lodash/noop';
let getScrollTop;
if (typeof window === 'undefined') {
getScrollTop = noop;
}
else {
getScrollTop = function () {
let scrollTop = window.pageYOffset;
if (scrollTop == null) {
scrollTop = document.documentElement.scrollTop;
}
if (scrollTop == null) {
scrollTop = document.body.scrollTop;
}
if (scrollTop == null) {
scrollTop = 0;
}
return scrollTop;
};
}
export default getScrollTop;
'use strict';
import noop from 'lodash/noop';
let getViewportHeight;
if (typeof window === 'undefined') {
getViewportHeight = noop;
}
else {
getViewportHeight = function () {
let viewportHeight = window.innerHeight;
if (viewportHeight == null) {
viewportHeight = document.documentElement.clientHeight;
}
if (viewportHeight == null) {
viewportHeight = 0;
}
return viewportHeight;
};
}
export default getViewportHeight;
'use strict';
import call from './call';
const hasOwnProperty = call(Object.prototype.hasOwnProperty);
export default hasOwnProperty;
'use strict';
import { is } from 'immutable';
import shallowEqual from 'fbjs/lib/shallowEqual';
import fromJS from './from_js';
const immutableStateMixin = function (getInitialState, isEqual = shallowEqual) {
let initialState;
return {
getInitialState () {
if (typeof initialState === 'undefined') {
const immutable = fromJS(getInitialState());
initialState = { immutable };
}
return initialState;
},
shouldComponentUpdate (props, state) {
const { immutable } = state;
return !is(immutable, this.state.immutable) || !isEqual(props, this.props);
},
getImmutableState (...keys) {
const { immutable } = this.state;
let value;
switch (keys.length) {
case 1:
value = immutable.get(keys[0]);
break;
case 0:
value = immutable;
break;
default:
value = immutable.getIn(keys);
break;
}
return value;
},
setImmutableState (key, value, callback) {
const { immutable } = this.state;
let mutated;
switch (typeof key) {
case 'string':
if (typeof value === 'function') {
mutated = immutable.update(key, value);
}
else {
mutated = immutable.set(key, value);
}
break;
case 'object':
if (typeof value === 'function') {
if (callback == null) {
callback = value;
value = null;
}
else {
throw new TypeError;
}
}
else if (value != null) {
throw new TypeError;
}
mutated = immutable.merge(key);
break;
case 'function':
if (value != null) {
throw new TypeError;
}
mutated = immutable.update(key);
break;
default:
throw new TypeError;
}
if (!is(mutated, immutable)) {
this.setState({ immutable: mutated }, callback);
}
}
};
};
export default immutableStateMixin;
'use strict';
import noop from 'lodash/noop';
let passive;
if (typeof window === 'undefined') {
passive = false;
}
else {
passive = (function () {
let passive = false;
//@formatter:off
try {
document.createElement('div').addEventListener('noop', noop, {
get passive () {
passive = true;
return false;
}
});
}
/*eslint-disable no-empty*/
catch (error) {}
/*eslint-enable no-empty*/
//@formatter:on
return passive ? { capture: false, passive } : false;
})();
}
export default passive;
'use strict';
import raf from 'raf';
import hasOwnProperty from './has_own_property';
import passive from './passive';
const rafMixin = function (events, handler) {
let Mixin;
if (typeof window === 'undefined') {
Mixin = {};
}
else {
const components = {};
const throttle = {};
const key = `_${ handler }Id`;
let count = 0;
const handleEvent = function (event) {
for (let id in components) {
if (hasOwnProperty(components, id)) {
if (!throttle[id]) {
throttle[id] = true;
const { [id]: component } = components;
raf(function () {
throttle[id] = false;
if (component.isMounted()) {
component[handler](event);
}
});
}
}
}
};
if (typeof events === 'string') {
events = [events];
}
events.forEach(function (event) {
window.addEventListener(event, handleEvent, passive);
});
Mixin = {
componentDidMount () {
const _id = this[key] = count++;
components[_id] = this;
throttle[_id] = false;
handleEvent(this);
},
componentWillUnmount () {
const _id = this[key];
delete components[_id];
delete throttle[_id];
}
};
}
return Mixin;
};
export default rafMixin;
'use strict';
import rafMixin from './raf_mixin';
const ResizeMixin = rafMixin(['resize', 'orientationchange'], 'handleResize');
export default ResizeMixin;
'use strict';
import rafMixin from './raf_mixin';
const ScrollMixin = rafMixin('scroll', 'handleScroll');
export default ScrollMixin;
'use strict';
import React, { PropTypes } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { List } from 'immutable';
import { findDOMNode } from 'react-dom';
import debounce from 'lodash/debounce';
import immutableStateMixin from './immutable_state_mixin';
import ResizeMixin from './resize_mixin';
import ScrollMixin from './scroll_mixin';
import getScrollTop from './get_scroll_top';
import getViewportHeight from './get_viewport_height';
const virtualList = function (Component) {
return React.createClass({
displayName: 'VirtualList',
mixins: [
ResizeMixin,
ScrollMixin,
immutableStateMixin(function () {
return {
firstIndex: 0,
lastIndex: -1
};
})
],
propTypes: {
buffer: PropTypes.number,
firstIndex: PropTypes.number.isRequired,
itemHeight: PropTypes.number.isRequired,
items: ImmutablePropTypes.list,
lastIndex: PropTypes.number.isRequired,
style: PropTypes.object,
timeout: PropTypes.number
},
getDefaultProps () {
return {
firstIndex: 0,
buffer: 0,
lastIndex: -1,
timeout: 100
};
},
render () {
/*eslint-disable no-unused-vars*/
let { firstIndex, buffer, itemHeight, items, lastIndex, style, timeout, ...props } = this.props;
/*eslint-enable no-unused-vars*/
firstIndex = this.getImmutableState('firstIndex');
lastIndex = this.getImmutableState('lastIndex');
let listHeight;
if (items == null) {
listHeight = 0;
items = List();
}
else {
listHeight = items.size * itemHeight;
items = (lastIndex > -1) ? items.slice(firstIndex, lastIndex + 1) : List();
}
style = {
...style,
boxSizing: 'border-box',
height: `${ listHeight }px`,
paddingTop: `${ firstIndex * itemHeight }px`
};
return (
<Component
{ ...props }
items={ items }
style={ style }/>
);
},
componentWillMount () {
const { firstIndex, lastIndex } = this.props;
this.setImmutableState({ firstIndex, lastIndex });
},
componentDidMount () {
const { timeout } = this.props;
this.element = findDOMNode(this);
this.willEnablePointerEvents = debounce(this.enablePointerEvents, timeout);
},
componentWillReceiveProps (props) {
const { timeout } = props;
if (timeout !== this.props.timeout) {
this.willEnablePointerEvents = debounce(this.enablePointerEvents, timeout);
}
this.calculateScroll(props);
},
componentWillUnmount () {
this.element = null;
this.willEnablePointerEvents = null;
},
handleResize () {
this.calculateScroll(this.props);
},
handleScroll () {
this.element.style.pointerEvents = 'none';
this.willEnablePointerEvents();
this.calculateScroll(this.props);
},
calculateScroll (props) {
const { itemHeight, items, buffer } = props;
const { element } = this;
let size;
if (items == null) {
size = 0;
}
else {
({ size } = items);
}
const listTop = element.getBoundingClientRect().top;
const offsetTop = getScrollTop() + listTop;
const visibleHeight = getViewportHeight() - offsetTop;
const listHeight = itemHeight * size;
const top = Math.max(0, offsetTop - listTop);
const bottom = Math.max(0, Math.min(listHeight, visibleHeight - listTop));
const firstIndex = Math.max(0, Math.floor(top / itemHeight) - buffer);
const lastIndex = Math.min(size, Math.ceil(bottom / itemHeight) + buffer) - 1;
this.setImmutableState({ firstIndex, lastIndex });
},
enablePointerEvents () {
if (this.isMounted()) {
this.element.style.pointerEvents = null;
}
},
element: null,
willEnablePointerEvents: null
});
};
export default virtualList;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment