Skip to content

Instantly share code, notes, and snippets.

@claus
Last active September 2, 2016 02:14
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 claus/adc078c850d1245b5535c74880367c23 to your computer and use it in GitHub Desktop.
Save claus/adc078c850d1245b5535c74880367c23 to your computer and use it in GitHub Desktop.
"use strict";
let listeners = [];
let scrollTop = 0;
let innerWidth = 0;
let innerHeight = 0;
let waitForRAF = false;
const supportsPassive = (function () {
let supportsPassiveOption = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => {
supportsPassiveOption = true;
}
});
window.addEventListener('test', null, opts);
}
catch (error) {}
return supportsPassiveOption;
})();
export function addListener(func) {
if (listeners.length === 0) {
const opts = supportsPassive ? { passive: true } : false;
window.addEventListener('scroll', handleUpdate, opts);
window.addEventListener('resize', handleUpdate, false);
listeners.push(func);
} else if (!listeners.find(listener => listener === func)) {
listeners.push(func);
}
}
export function removeListener(func) {
listeners = listeners.filter(listener => listener !== func);
if (listeners.length === 0) {
const opts = supportsPassive ? { passive: true } : false;
window.removeEventListener('scroll', handleUpdate, opts);
window.removeEventListener('resize', handleUpdate, false);
}
}
export function triggerListener(func) {
update();
func(scrollTop, innerWidth, innerHeight);
}
export function triggerAll() {
update();
dispatch();
}
function handleUpdate(event) {
update();
if (!waitForRAF) {
window.requestAnimationFrame(() => {
dispatch();
waitForRAF = false;
});
waitForRAF = true;
}
}
function update() {
const doc = document.documentElement;
scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
innerWidth = window.innerWidth;
innerHeight = window.innerHeight;
}
function dispatch() {
listeners.forEach(listener => {
listener(scrollTop, innerWidth, innerHeight);
});
}
import React, { Component, PropTypes } from 'react';
import { addListener, removeListener, triggerListener } from './ScrollEventDispatcher';
export default class ViewportWatcher extends Component {
static propTypes = {
children: PropTypes.oneOfType([ PropTypes.node, PropTypes.func ]),
component: PropTypes.oneOfType([ PropTypes.string, PropTypes.element ]),
onChange: PropTypes.func,
initialDelay: PropTypes.number,
active: PropTypes.bool,
debug: PropTypes.bool,
};
static defaultProps = {
initialDelay: 0,
active: true,
debug: false,
};
static childContextTypes = {
viewportWatcherState: PropTypes.shape({
inViewPixels: PropTypes.number,
inViewPercent: PropTypes.number,
outViewPixels: PropTypes.number,
outViewPercent: PropTypes.number,
visiblePixels: PropTypes.number,
visiblePercent: PropTypes.number,
isVisible: PropTypes.bool,
direction: PropTypes.oneOf([ 'up', 'down' ]),
})
};
state = {
inViewPixels: 0,
inViewPercent: 0,
outViewPixels: 0,
outViewPercent: 0,
visiblePixels: 0,
visiblePercent: 0,
isVisible: false,
direction: 'down',
};
prevScrollTop = 0;
componentDidMount() {
addListener(this.handleUpdate);
if (this.props.initialDelay === 0) {
triggerListener(this.handleUpdate);
} else {
this.timeout = setTimeout(() => {
this.timeout = null;
triggerListener(this.handleUpdate);
}, this.props.initialDelay);
}
}
componentWillUnmount() {
removeListener(this.handleUpdate);
clearTimeout(this.timeout);
}
componentWillReceiveProps(nextProps) {
if (nextProps.active !== this.props.active && this.props.active) {
triggerListener(this.handleUpdate);
}
}
getChildContext() {
return {
viewportWatcherState: { ...this.state }
};
}
handleUpdate = (scrollTop, winWidth, winHeight) => {
if (!this.props.active || this.timeout) {
return;
}
const bounds = this.refs.container.getBoundingClientRect();
const inViewPixels = Math.max(Math.min(-bounds.top + winHeight, bounds.height), 0);
const inViewPercent = 100 * inViewPixels / bounds.height;
const outViewPixels = -Math.max(Math.min(-bounds.top, bounds.height), -0);
const outViewPercent = Math.abs(100 * outViewPixels / bounds.height);
const visiblePixels = inViewPixels + outViewPixels;
const visiblePercent = 100 * visiblePixels / bounds.height;
const direction = (this.prevScrollTop > scrollTop) ? 'up' : 'down';
const isVisible = (visiblePixels !== 0);
if (isVisible || this.state.isVisible) {
const nextState = {
inViewPixels,
inViewPercent,
outViewPixels,
outViewPercent,
visiblePixels,
visiblePercent,
isVisible,
direction,
};
if (this.props.debug) {
console.info(
'visible: %d (%d%%), inView: %d (%d%%), outView: %d (%d%%), dir: %s',
visiblePixels,
visiblePercent,
inViewPixels,
inViewPercent,
outViewPixels,
outViewPercent,
direction
);
}
this.props.onChange && this.props.onChange(nextState, this.state);
this.setState(nextState);
}
this.prevScrollTop = scrollTop;
};
renderChildren() {
const { children, onChange } = this.props;
if (typeof children === 'function') {
return children(this.state);
} else {
if (onChange) {
return children;
} else {
return React.Children.map(children, child => {
if (typeof child !== 'object' || typeof child.type === 'string') {
return child;
} else {
return React.cloneElement(child, { ...this.state });
}
});
}
}
}
render() {
const { children, onChange, initialDelay, active, debug, component, ...props } = this.props;
return React.createElement(component || 'div', { ...props, ref: 'container' }, this.renderChildren());
}
}
@claus
Copy link
Author

claus commented Aug 27, 2016

Usage examples:

render() {
    <ViewportWatcher>
        <h1>You're being watched</h1>
        <p>The government has a secret system</p>
        <SecretSystem /> {/* Receives ViewportWatcher state as props */}
    </ViewportWatcher>
}
render() {
    <ViewportWatcher>
        <h1>You're being watched</h1>
        <p>The government has a secret system</p>
        <div>
            <SecretSystem /> {/* Can access ViewportWatcher state via context.viewportWatcherState */}
        </div>
    </ViewportWatcher>
}
handleChange = (state, prevState) => {
    // Receives ViewportWatcher current and previous state
    // Use for GSAP stuff or whatever
};

render() {
    <ViewportWatcher onChange={this.handleChange}>
        {/* Does not re-render when ViewportWatcher state changes */}
        <h1>You're being watched</h1>
        <p>The government has a secret system</p>
        <SecretSystem /> {/* Does not receive ViewportWatcher state as props */}
    </ViewportWatcher>
}
renderChildren = state => {
    // Do whatever you want with the ViewportWatcher state
    return <SecretSystem isVisible={state.isVisible} />;
};

render() {
    <ViewportWatcher>
        {this.renderChildren} {/* Pass function reference! Function is executed by ViewportWatcher */}
    </ViewportWatcher>
}

ViewportWatcher state:

{
    isVisible,
    visiblePixels,
    visiblePercent,
    inViewPixels,
    inViewPercent,
    outViewPixels,
    outViewPercent,
    direction
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment