Home Manual Reference Source

src/ReactNipple.js

import nipplejs from 'nipplejs';
import autobind from 'autobind-decorator';
import isEqual from 'lodash.isequal';
import PropTypes from 'prop-types';
import React, { Component } from 'react';

import cx from 'classnames';

/**
 * A react wrapper component for `nipplejs`.
 * @see https://www.npmjs.com/package/nipplejs
 * @author Jovica Aleksic <jovica.aleksic@loopmode.de>
 */
export default class ReactNipple extends Component {
    /* eslint-disable no-trailing-spaces */
    /**
     * Component propTypes
     *
     * Any additional (unknown) props will be passed along as attributes of the created DOM element.
     *
     * @property {string} className - A css classname for the DOM element
     * @property {object} options - An object with nipplejs options, see https://github.com/yoannmoinet/nipplejs#options
     * @property {boolean} static - A shortcut for setting the options `{mode: 'static', position: {top: '50%', left: '50%'}}`. Will override values in the `options` object.
     * @property {function} onCreated - Callback that is invoked with the created instance
     * @property {function} onDestroy - Callback that is invoked with the instance that is going to be destroyed
     * @property {function} onStart - Callback for the 'start' event handler, see https://github.com/yoannmoinet/nipplejs#start
     * @property {function} onEnd - Callback for the 'end' event handler, see https://github.com/yoannmoinet/nipplejs#end
     * @property {function} onMove - Callback for the 'move' event handler, see https://github.com/yoannmoinet/nipplejs#move
     * @property {function} onDir - Callback for the 'dir' event handler, see https://github.com/yoannmoinet/nipplejs#dir
     * @property {function} onPlain - Callback for the 'plain' event handler, see https://github.com/yoannmoinet/nipplejs#plain
     * @property {function} onShown - Callback for the 'shown' event handler, see https://github.com/yoannmoinet/nipplejs#shown
     * @property {function} onHidden - Callback for the 'hidden' event handler, see https://github.com/yoannmoinet/nipplejs#hidden
     * @property {function} onPressure - Callback for the 'pressure' event handler, see https://github.com/yoannmoinet/nipplejs#pressure
     */
    /* eslint-enable no-trailing-spaces */
    static get propTypes() {
        return {
            className: PropTypes.string,
            options: PropTypes.shape({
                color: PropTypes.string,
                size: PropTypes.integer,
                threshold: PropTypes.float, // before triggering a directional event
                fadeTime: PropTypes.integer, // transition time
                multitouch: PropTypes.bool,
                maxNumberOfNipples: PropTypes.number, // when multitouch, what is too many?
                dataOnly: PropTypes.bool, // no dom element whatsoever
                position: PropTypes.object, // preset position for 'static' mode
                mode: PropTypes.string, // 'dynamic', 'static' or 'semi'
                restJoystick: PropTypes.bool,
                restOpacity: PropTypes.number, // opacity when not 'dynamic' and rested
                catchDistance: PropTypes.number
            }),
            static: PropTypes.bool,
            onStart: PropTypes.func,
            onEnd: PropTypes.func,
            onMove: PropTypes.func,
            onDir: PropTypes.func,
            onPlain: PropTypes.func,
            onShown: PropTypes.func,
            onHidden: PropTypes.func,
            onPressure: PropTypes.func,
            onCreated: PropTypes.func,
            onDestroy: PropTypes.func
        };
    }

    get ownProps() {
        return [
            'options',
            'static',
            'onStart',
            'onEnd',
            'onMove',
            'onDir',
            'onPlain',
            'onShown',
            'onHidden',
            'onPressure',
            'onCreated'
        ];
    }
    get elementProps() {
        return Object.entries(this.props).reduce((result, [key, value]) => {
            if (this.ownProps.includes(key)) {
                return result;
            }
            result[key] = value;
            return result;
        }, {});
    }

    componentDidUpdate(prevProps) {
        if (!isEqual(prevProps.options, this.props.options)) {
            this.destroyJoystick();
            this.createJoystick();
        }
    }

    render() {
        return (
            <div {...this.elementProps} ref={this.handleElement} className={cx('ReactNipple', this.props.className)} />
        );
    }

    //-----------------------------------
    //
    // impl
    //
    //-----------------------------------

    @autobind
    handleElement(ref) {
        this._element = ref;
        if (ref) {
            this.createJoystick(this.props);
        } else if (this._element) {
            this.destroyJoystick();
        }
    }
    createJoystick(props) {
        const options = {
            zone: this._element,
            ...props.options
        };

        if (this.props.static) {
            options.mode = 'static';
            options.position = {
                top: '50%',
                left: '50%'
            };
        }

        const joystick = nipplejs.create(options);
        joystick.on('start', this.handleJoystickStart);
        joystick.on('end', this.handleJoystickEnd);
        joystick.on('move', this.handleJoystickMove);
        joystick.on('dir', this.handleJoystickDir);
        joystick.on('plain', this.handleJoystickPlain);
        joystick.on('shown', this.handleJoystickShown);
        joystick.on('hidden', this.handleJoystickHidden);
        joystick.on('pressure', this.handleJoystickPressure);

        this.joystick = joystick;

        if (props.onCreated) {
            props.onCreated(this.joystick);
        }
    }
    destroyJoystick() {
        if (this.joystick) {
            this.joystick.destroy();
            this.joystick = undefined;
        }
    }
    invokeCallback(type, evt, data) {
        if (this.props[type]) {
            this.props[type](evt, data);
        }
    }
    @autobind
    handleJoystickStart(evt, data) {
        this.invokeCallback('onStart', evt, data);
    }
    @autobind
    handleJoystickEnd(evt, data) {
        this.invokeCallback('onEnd', evt, data);
    }
    @autobind
    handleJoystickMove(evt, data) {
        this.invokeCallback('onMove', evt, data);
    }
    @autobind
    handleJoystickDir(evt, data) {
        this.invokeCallback('onDir', evt, data);
    }
    @autobind
    handleJoystickPlain(evt, data) {
        this.invokeCallback('onPlain', evt, data);
    }
    @autobind
    handleJoystickShown(evt, data) {
        this.invokeCallback('onShown', evt, data);
    }
    @autobind
    handleJoystickHidden(evt, data) {
        this.invokeCallback('onHidden', evt, data);
    }
    @autobind
    handleJoystickPressure(evt, data) {
        this.invokeCallback('onPressure', evt, data);
    }
}