Created
February 24, 2017 02:58
-
-
Save jordaaash/10cc469c2dbd872585c45ebaf6053a8d to your computer and use it in GitHub Desktop.
React Immutable virtual list
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const call = function (fn) { | |
return Function.prototype.call.bind(fn); | |
}; | |
export default call; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import call from './call'; | |
const hasOwnProperty = call(Object.prototype.hasOwnProperty); | |
export default hasOwnProperty; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import rafMixin from './raf_mixin'; | |
const ResizeMixin = rafMixin(['resize', 'orientationchange'], 'handleResize'); | |
export default ResizeMixin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import rafMixin from './raf_mixin'; | |
const ScrollMixin = rafMixin('scroll', 'handleScroll'); | |
export default ScrollMixin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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