Last active
July 10, 2017 00:05
-
-
Save myndzi/5b08aae738eff840235e9d0a8979abe4 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import { cloneElement, Children, Component } from 'react'; | |
// Adds sticky scrolling to some react component or element; usage: | |
// <ScrollLocked><whatever></ScrollLocked> | |
class ScrollLocked extends Component { | |
constructor(props) { | |
super(props); | |
this.el = null; | |
this.locked = false; | |
this.immediate = null; | |
this.debounce = null; | |
this.resized = false; | |
this.scrolled = false; | |
this.rebind = this.rebind.bind(this); | |
this.updateLock = this.updateLock.bind(this); | |
this.maybeScroll = this.maybeScroll.bind(this); | |
this.debouncedMaybeScroll = this.debouncedMaybeScroll.bind(this); | |
this.onResize = this.onResize.bind(this); | |
this.onScroll = this.onScroll.bind(this); | |
this.maybeUpdateLock = this.maybeUpdateLock.bind(this); | |
} | |
componentWillMount() { | |
window.addEventListener('resize', this.onResize); | |
} | |
componentDidMount() { | |
this.updateLock(); | |
} | |
componentWillUnmount() { | |
const el = this.el; | |
window.removeEventListener('resize', this.onResize); | |
if (el) { | |
el.removeEventListener('DOMSubtreeModified', this.maybeScroll); | |
el.removeEventListener('scroll', this.onScroll); | |
} | |
} | |
rebind(el) { | |
if (this.el === el) { return; } | |
if (this.el) { | |
this.el.removeEventListener('DOMSubtreeModified', this.maybeScroll); | |
this.el.removeEventListener('scroll', this.onScroll); | |
this.el = null; | |
} | |
if (el) { | |
this.el = el; | |
this.el.addEventListener('DOMSubtreeModified', this.maybeScroll); | |
this.el.addEventListener('scroll', this.onScroll); | |
} | |
} | |
onResize() { | |
this.resized = true; | |
this.maybeUpdateLock(); | |
} | |
onScroll() { | |
this.scrolled = true; | |
this.maybeUpdateLock(); | |
} | |
maybeUpdateLock() { | |
if (this.immediate) { | |
clearImmediate(this.immediate); | |
} | |
// zooming/resizing can trigger the scroll event too, | |
// but in that case we don't want to update the lock | |
// status. this hack ensures we only perform updateLock | |
// if there was not also a resize event in the same tick | |
this.immediate = setImmediate(() => { | |
if (this.scrolled && !this.resized) { | |
this.updateLock(); | |
} else { | |
this.debouncedMaybeScroll(); | |
} | |
this.scrolled = false; | |
this.resized = false; | |
this.immediate = null; | |
}); | |
} | |
updateLock() { | |
const el = this.el; | |
this.locked = ( | |
el !== null && | |
Math.ceil(el.scrollTop) === Math.ceil(el.scrollHeight - el.offsetHeight) | |
); | |
} | |
debouncedMaybeScroll() { | |
if (this.debounce) { | |
clearTimeout(this.debounce); | |
} | |
this.debounce = setTimeout(() => { | |
this.maybeScroll(); | |
this.debounce = null; | |
}, 50); | |
} | |
maybeScroll() { | |
const el = this.el, | |
newTop = Math.ceil(el.scrollHeight - el.offsetHeight); | |
if (this.locked && el.scrollTop !== newTop) { | |
el.scrollTop = newTop; | |
} | |
} | |
render() { | |
return cloneElement( | |
Children.only(this.props.children), | |
{ ref: el => this.rebind(el) } | |
); | |
} | |
}; | |
export default ScrollLocked; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment