API Docs for: 0.0.1
Show:

File: lib/component.jsx

"use strict";

/**
 * React-Component: refined Button.
 *
 *
 *
 * <i>Copyright (c) 2016 ItsAsbreuk - http://itsasbreuk.nl</i><br>
 * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
 *
 *
 * @module itsa-react-button
 * @class Button
 * @since 0.0.1
*/

require("itsa-dom");

const React = require("react"),
    PropTypes = require("prop-types"),
    utils = require("itsa-utils"),
    later = utils.later,
    async = utils.async,
    MAIN_CLASS = "itsa-button",
    FORM_ELEMENT_CLASS_SPACES = " itsa-formelement",
    MAIN_CLASS_PREFIX = MAIN_CLASS+"-",
    WHITE_SPACE = "&#160;", // white-space
    BOOLEAN = "boolean",
    DEF_BUTTON_PRESS_TIME = 300;

class Button extends React.Component {

    constructor(props) {
        super(props);
        const instance = this;
        instance.state = {
            active: false,
            mouseDown: false
        };
        instance.blur = instance.blur.bind(instance);
        instance.focus = instance.focus.bind(instance);
        instance.handleBlur = instance.handleBlur.bind(instance);
        instance.handleClick = instance.handleClick.bind(instance);
        instance.handleKeyDown = instance.handleKeyDown.bind(instance);
        instance.handleKeyUp = instance.handleKeyUp.bind(instance);
        instance.handleMouseDown = instance.handleMouseDown.bind(instance);
        instance.handleMouseOut = instance.handleMouseOut.bind(instance);
        instance.handleMouseOver = instance.handleMouseOver.bind(instance);
        instance.handleMouseEnter = instance.handleMouseEnter.bind(instance);
        instance.handleMouseLeave = instance.handleMouseLeave.bind(instance);
        instance.handleMouseUp = instance.handleMouseUp.bind(instance);
        instance.press = instance.press.bind(instance);
        instance._getDataAttrs = instance._getDataAttrs.bind(instance);
        instance._processKeyUp = instance._processKeyUp.bind(instance);
        instance._saveHTML = instance._saveHTML.bind(instance);
    }

    /**
     * Blurs the Component.
     *
     * @method blur
     * @chainable
     * @since 0.0.1
     */
    blur() {
        var instance = this;
        instance._buttonNode.blur();
        return instance;
    }

    /**
     * componentDidMount does some initialization.
     *
     * @method componentDidMount
     * @since 0.0.1
     */
    componentDidMount() {
        const instance = this;
        instance._mounted = true;
        instance._knownMobile = (("ontouchstart" in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0));
        if (instance.props.autoFocus) {
            instance._focusLater = later(() => instance.focus(), 50);
        }
    }

    /**
     * componentWilUnmount does some cleanup.
     *
     * @method componentWillUnmount
     * @since 0.0.1
     */
    componentWillUnmount() {
        this._mounted = false;
        this._focusLater && this._focusLater.cancel();
    }

    componentWillReceiveProps(nextProps) {
        const instance = this;
        if (nextProps.disabled && !this.props.disabled) {
            async(instance.handleMouseUp);
        }
    }

    /**
     * Sets the focus on the Component.
     *
     * @method focus
     * @param [transitionTime] {Number} transition-time to focus the element into the view
     * @chainable
     * @since 0.0.1
     */
    focus(transitionTime) {
        var instance = this;
        instance._buttonNode.itsa_focus && instance._buttonNode.itsa_focus(null, null, transitionTime);
        return instance;
    }

    handleBlur() {
        this._keyDown = false;
    }

    /**
     * Callback-fn for the onClick-event.
     * Will invoke `this.props.onChange`
     *
     * @method handleClick
     * @since 0.0.1
     */
    handleClick(e, byMouseDown) {
        let button, leftClick, middleClick, rightClick;
        const instance = this,
            props = instance.props,
            onMiddleClick = props.onMiddleClick,
            onRightClick = props.onRightClick,
            onClick = props.onClick;
        if (!props.disabled && !props.readOnly && !instance._keyDown && (!this.state.mouseDown || (byMouseDown===true))) { // don't double execute
            if (onClick || onMiddleClick || onRightClick) {
                button = e.nativeEvent.button || 0;
                leftClick = (button===0);
                middleClick = (button===1);
                rightClick = (button===2);
                if ((onClick && leftClick) || (onMiddleClick && middleClick) || (onRightClick && rightClick)) {
                    e.preventDefault();
                    // NOT element.focus or node.itsa_focus ! --> would have side-effects, besides, the node is in the view if it got clicked
                    instance._buttonNode.focus();
                }
                if (onClick && leftClick) {
                    onClick(e);
                }
                if (middleClick && onMiddleClick) {
                    onMiddleClick(e);
                }
                if (rightClick && onRightClick) {
                    onRightClick(e);
                }
            }
        }
    }

    /**
     * Callback-fn for the onKeyDown-event.
     *
     * @method handleKeyDown
     * @since 0.0.1
     */
    handleKeyDown(e, directResponse, force) {
        const instance = this,
              props = instance.props,
              onClick = props.onClick,
              activatedBy = props.activatedBy,
              forced = (force===true), // IMPORTANT --> on a keyEvent this is an object in which we are not interested
              pressTimer = instance.pressTimer,
              keyCode = e.keyCode,
              isDirectResponse = (typeof directResponse===BOOLEAN) ? directResponse : props.directResponse;

        if (!props.disabled && !props.readOnly && instance._mounted) {
            (typeof props.onKeyDown==='function') && props.onKeyDown(e);
            if (keyCode===27) {
                // escape keyDown in case it was set
                instance._keyDown = false;
                pressTimer && pressTimer.cancel();
                if (instance.state.active) {
                    instance.setState({
                        active: false
                    });
                }
            }
            else {
                if (forced || (activatedBy.indexOf(keyCode)!==-1)) {
                    instance._keyDown = true;
                    if (typeof props.toggled===BOOLEAN) {
                        onClick && onClick();
                    }
                    else {
                        if (!instance.state.active) {
                            instance.setState({
                                active: true
                            });
                            pressTimer && pressTimer.cancel();
                            instance.pressTimer = later(instance._processKeyUp.bind(instance, null, isDirectResponse, forced), props.buttonPressTime);
                            if (isDirectResponse && onClick) {
                                later(() => {
                                    onClick();
                                }, 100); // we MUST delay, because an `onClick` that rerenders, would prevent `onKeyUp` from happening!
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Callback-fn for the onKeyUp-event.
     *
     * @method handleKeyUp
     * @since 0.0.1
     */
    handleKeyUp() {
        const instance = this,
            props = instance.props;
        // we must go async --> instance._keyDown cannot be set 'false' right away,
        // because the handleClick method needs to be processed first
        // if we don;t do this, props.onClick() would be executed twice when the spacebutton is pressed
        (typeof props.onKeyUp==='function') && props.onKeyUp(e);
        async(() => {
            instance._keyDown = false;
            if ((typeof props.toggled!==BOOLEAN) && instance.state.active) {
                instance._processKeyUp(true);
            }
        });
    }

    /**
     * Callback-fn for the onMouseDown-event.
     *
     * @method handleMouseDown
     * @since 0.0.1
     */
    handleMouseDown(e) {
        const instance = this,
            props = instance.props;

        if (!props.disabled && !props.readOnly && instance._mounted) {
            instance.handleClick(e, true);
            async(() => {
                // check if still mounted AND if still not disabled or readonly!
                if (!props.disabled && !props.readOnly && instance._mounted) {
                    instance._mouseDown = true;
                    instance.setState({
                        active: true,
                        mouseDown: true
                    });
                    if (instance._knownMobile) {
                        async(() => instance.handleMouseUp());
                    }
                    else {
                        // for those mobile devices that were not feature-detected, we need a fallback to simulate mouseUp,
                        // to prevent the button from being blocked:
                        instance._mouseUpEvt = later(instance.handleMouseUp, 2000);

                    }
                }
            });
        }
    }

    handleMouseEnter(e) {
        const onMouseEnter = this.props.onMouseEnter;
        onMouseEnter && onMouseEnter(e);
    }

    handleMouseLeave(e) {
        const onMouseLeave = this.props.onMouseLeave;
        onMouseLeave && onMouseLeave(e);
    }

    handleMouseOver(e) {
        const onMouseOver = this.props.onMouseOver;
        onMouseOver && onMouseOver(e);
    }

    handleMouseOut(e) {
        const onMouseOut = this.props.onMouseOut;
        this.handleMouseUp();
        onMouseOut && onMouseOut(e);
    }

    /**
     * Callback-fn for the onMouseUp-event.
     *
     * @method handleMouseUp
     * @since 0.0.1
     */
    handleMouseUp() {
        const instance = this;
        // only if `_mouseDown` still is false --> it could be set `true` by `onMouseOut`
        if (instance._mouseDown && instance._mounted) {
            instance._mouseDown = false;
            instance._mouseUpEvt && instance._mouseUpEvt.cancel();
            instance.setState({
                active: false,
                mouseDown: false
            });
        }
    }

    /**
     * Callback-fn for the onClick-event.
     * Will invoke `this.props.onChange`
     *
     * @method press
     * @param boolean [directResponse] whether directly call onClick, or wait until the button raises up.
     * @since 0.0.1
     */
    press(directResponse) {
        this.handleKeyDown({}, directResponse, true);
    }

    /**
     * React render-method --> renderes the Component.
     *
     * @method render
     * @return ReactComponent
     * @since 0.0.1
     */
    render() {
        let classname = MAIN_CLASS+FORM_ELEMENT_CLASS_SPACES,
            buttonHTML = this.props.buttonHTML,
            dangerouslySetInnerHTML, buttonText, handleClick,
            handleKeyDown, handleKeyUp, handleMouseDown, handleMouseUp;

        const instance = this,
              props = instance.props,
              state = instance.state,
              disabled = props.disabled,
              directResponse = props.directResponse,
              saveButtonText = instance._saveHTML(props.buttonText),
              isToggleButton = (typeof props.toggled===BOOLEAN),
              ariaLabel = props["aria-label"] || saveButtonText || instance._saveHTML(buttonHTML);

        if (state.active || props.toggled) {
            if (props.showActivated || state.mouseDown) {
                classname += " "+MAIN_CLASS_PREFIX+"active";
            }
            props.toggled && (classname+=" "+MAIN_CLASS_PREFIX+"toggled");
        }
        isToggleButton && (classname+=" "+MAIN_CLASS_PREFIX+"togglebtn");
        props.className && (classname+=" "+props.className);
        props.readOnly && (classname+=" readonly");

        if (!buttonHTML && !props.buttonText) {
            buttonHTML = WHITE_SPACE;
        }
        if (buttonHTML) {
            dangerouslySetInnerHTML = {__html: buttonHTML};
        }
        else {
            buttonText = saveButtonText;
        }

        if (directResponse || isToggleButton) {
            handleMouseUp = instance.handleMouseUp;
        }
        handleKeyUp = instance.handleKeyUp;
        if (!disabled) {
            if (directResponse || isToggleButton) {
                handleMouseDown = instance.handleMouseDown;
            }
            else {
                handleClick = instance.handleClick;
            }
            handleKeyDown = instance.handleKeyDown;
        }
        handleMouseUp = instance.handleMouseUp;

        return (
            <button {...instance._getDataAttrs()}
                accessKey={props.accessKey}
                aria-label={ariaLabel}
                aria-pressed={props.toggled}
                className={classname}
                dangerouslySetInnerHTML={dangerouslySetInnerHTML}
                disabled={disabled}
                name={props.name}
                onBlur={instance.handleBlur}
                onClick={handleClick}
                onKeyDown={handleKeyDown}
                onKeyUp={handleKeyUp}
                onMouseDown={handleMouseDown}
                onMouseEnter={instance.handleMouseEnter}
                onMouseLeave={instance.handleMouseLeave}
                onMouseOut={instance.handleMouseOut}
                onMouseOver={instance.handleMouseOver}
                onMouseUp={handleMouseUp}
                ref={node => instance._buttonNode = node}
                role="button"
                style={props.style}
                tabIndex={props.tabIndex}
                type={props.type} >
                {buttonText}
            </button>
        );
    }

    /**
     * Extracts the `data-*` attributes from props.
     *
     * @method _getDataAttrs
     * @private
     * @return object all the data-* attributes
     * @since 0.0.3
     */
    _getDataAttrs() {
        let dataAttrs = {};
        const props = this.props,
             keys = Object.keys(props);

        keys.forEach(function(key) {
            (key.substr(0,5).toLowerCase()==="data-") && (dataAttrs[key]=props[key]);
        });
        return dataAttrs;
    }

    /**
     * React render-method --> renderes the Component.
     *
     * @method _processKeyUp
     * @param Boolean manual whether this routine gets called manually (keypress), or from a click-event
     * @param Boolean directResponse Whether to direct response or wait for the button to raise up
     * @param Boolean force whether to force (initiated by the method `press`)
     * @private
     * @since 0.0.1
     */
    _processKeyUp(manual, directResponse, force) {
        const instance = this,
              props = instance.props,
              onClick = props.onClick,
              pressTimer = instance.pressTimer,
              manualDeactivation = manual && !pressTimer,
              timerDeactivation = !manual && pressTimer;
        if (this._mounted) { // we don't want unMounted Buttons to trigger the state and onClick-prop
            if (timerDeactivation) {
                pressTimer.cancel();
                delete instance.pressTimer;
            }
            force && (instance._keyDown=false); // because we didn;t came from `handleKeyUp`
            if ((!instance._keyDown || force) && (manualDeactivation || timerDeactivation)) {
                if (instance.state.active) {
                    instance.setState({
                        active: false
                    });
                    if ((typeof directResponse===BOOLEAN) ? !directResponse : !props.directResponse) {
                        onClick && onClick();
                    }
                }
            }
        }
    }

    /**
     * Returns a save string
     *
     * @method _saveHTML
     * @private
     * @param String html the text that should be removed from any html-entities
     * @return String
     * @since 0.0.1
     */
    _saveHTML(html) {
        return html && html.replace(/<[^>]*>/g, "");
    }
}

Button.propTypes = {
    /**
     * Array with the keys that can press the button when focussed.
     * Default: [13, 32]
     *
     * @property activatedBy
     * @type Array
     * @since 0.0.1
    */
    activatedBy: PropTypes.array,

    /**
     * The aria-label. When not set, it will equal the buttonText
     *
     * @property aria-label
     * @type String
     * @since 0.0.1
    */
    "aria-label": PropTypes.string,

    /**
     * Whether to autofocus the Component.
     *
     * @property autoFocus
     * @type Boolean
     * @since 0.0.1
    */
    autoFocus: PropTypes.bool,

    /**
     * The Button-text. Will be escaped. If you need HTML, then use `buttonHTML` instead.
     *
     * @property buttonText
     * @type String
     * @since 0.0.1
    */
    buttonText: PropTypes.string,

    /**
     * The Button-text, retaining html-code. If you don't need HTML,
     * then `buttonText` is preferred.
     *
     * @property buttonHTML
     * @type String
     * @since 0.0.1
    */
    buttonHTML: PropTypes.string,

    /**
     * The time that the button retains in its `pressed-state` when activated by a key-press.
     *
     * Default: 300ms
     *
     * @property buttonPressTime
     * @type Number
     * @since 0.0.1
    */
    buttonPressTime: PropTypes.number,

    /**
     * The class that should be set on the element
     *
     * @property className
     * @type String
     * @since 0.0.1
    */
    className: PropTypes.string,

    /**
     * Whether the button resonses rapidly (keydown and mousedown).
     * Note: native HTMLButtonElements don't resonse rapidly --> the onClick event happens on mouseUp.
     *
     * Default: true
     *
     * @property directResponse
     * @type Boolean
     * @since 0.0.1
    */
    directResponse: PropTypes.bool,

    /**
     * Whether the button is disabled
     *
     * @property disabled
     * @type Boolean
     * @since 0.0.1
    */
    disabled: PropTypes.bool,

    /**
     * The name-attribute of the button
     *
     * @property name
     * @type String
     * @since 0.0.1
    */
    name: PropTypes.string,

    /**
     * Callback whenever the button gets clicked by the left mousebutton.
     *
     * @property onClick
     * @type Function
     * @since 0.0.1
    */
    onClick: PropTypes.func,

    /**
     * Callback wheneveer the button gets clicked by the middle mousebuttin.
     *
     * @property onMiddleClick
     * @type Function
     * @since 0.0.1
    */
    onMiddleClick: PropTypes.func,

    onMouseEnter: PropTypes.func,

    onMouseLeave: PropTypes.func,

    onMouseOver: PropTypes.func,

    onMouseOut: PropTypes.func,

    /**
     * Callback wheneveer the button gets clicked by the right mouse-button.
     *
     * @property onRightClick
     * @type Function
     * @since 0.0.1
    */
    onRightClick: PropTypes.func,

    /**
     * Whether the checkbox is readonly
     *
     * @property readOnly
     * @type Boolean
     * @default false
     * @since 15.2.0
    */
    readOnly: PropTypes.bool,

    /**
     * Whether keypress should show active status. (should be set `false` for file-uploadbuttons)
     * Default: true
     *
     * @property showActivated
     * @type Boolean
     * @since 0.0.5
    */
    showActivated: PropTypes.bool,

    /**
     * Inline style
     *
     * @property style
     * @type object
     * @since 0.0.1
    */
    style: PropTypes.object,

    /**
     * The tabIndex
     *
     * @property tabIndex
     * @type Number
     * @since 0.0.1
    */
    tabIndex: PropTypes.number,

    /**
     * Whether the button is in a toggle-state.
     * You don;t need to use this directly: use the module ToggleButton instead.
     *
     * @property toggled
     * @type Boolean
     * @since 0.0.1
    */
    toggled: PropTypes.bool,

    /**
     * The type of the button
     * Default: "button"
     *
     * @property children
     * @type String
     * @since 0.0.1
    */
    type: PropTypes.string
};

Button.defaultProps = {
    activatedBy: [13, 32],
    autoFocus: false,
    buttonPressTime: DEF_BUTTON_PRESS_TIME,
    directResponse: true,
    disabled: false,
    readOnly: false,
    showActivated: true,
    type: "button"
};

module.exports = Button;