Source: ui/base/abstract-toggle-button.js

/*global require, exports*/

/**
 * @module montage/ui/base/abstract-toggle-button.reel
 */
var AbstractControl = require("./abstract-control").AbstractControl,
    PressComposer = require("../../composer/press-composer").PressComposer,
    KeyComposer = require("../../composer/key-composer").KeyComposer;

/**
 * @class AbstractToggleButton
 * @extends AbstractControl
 * @fires action
 * @fires longAction
 */
var AbstractToggleButton = exports.AbstractToggleButton = AbstractControl.specialize( /** @lends AbstractToggleButton# */ {

    /**
     * Dispatched when the toggle button is activated through a mouse click,
     * finger tap, or when focused and the spacebar is pressed.
     * @event action
     * @memberof AbstractToggleButton
     * @param {Event} event
     */

    /**
     * Dispatched when the toggle button is pressed for a period of time, set
     * by {@link holdThreshold}.
     * @event longAction
     * @memberof AbstractToggleButton
     * @param {Event} event
     */

    /**
     * @private
     */
    constructor: {
        value: function () {
            if(this.constructor === AbstractToggleButton) {
                throw new Error("AbstractToggleButton cannot be instantiated.");
            }

            this._pressComposer = new PressComposer();
            this._pressComposer.defineBinding("longPressThreshold ", {
                "<-": "holdThreshold",
                source: this
            });
            this.addComposer(this._pressComposer);

            this._keyComposer = new KeyComposer();
            this._keyComposer.component = this;
            this._keyComposer.keys = "space";
            this.addComposer(this._keyComposer);

            this.classList.add("montage-ToggleButton");

            this.defineBindings({
                // classList management
                "classList.has('montage--disabled')": {
                    "<-": "!enabled"
                },
                "classList.has('montage--active')": {
                    "<-": "active"
                },
                "classList.has('montage-ToggleButton--pressed')": {
                    "<-": "pressed"
                }
            });
        }
    },

    /**
     * Enables or disables the Button from user input. When this property is
     * set to `false`, the "montage--disabled" CSS style is applied to the
     * button's DOM element during the next draw cycle. When set to `true` the
     * "montage--disabled" CSS class is removed from the element's class list.
     * @type {boolean}
     */
    enabled: {
        value: true
    },

    acceptsActiveTarget: {
        value: function () {
            return true;
        }
    },

    /**
     Stores the node that contains this button's value. Only used for
     non-`<input>` elements.
     @private
     */
    _labelNode: {
        value: null
    },

    _pressedLabel: {
        value: "on"
    },

    /**
     * The displayed text on the button when it is pressed.
     *
     * @type {string}
     * @default undefined
     */
    pressedLabel: {
        get: function () {
            return this._pressedLabel;
        },
        set: function (value) {
            this._pressedLabel = "" + value;
            this.needsDraw = true;
        }
    },

    _unpressedLabel: {
        value: "off"
    },

    /**
     * The displayed text on the button when it is unpressed.
     * @type {string}
     * @default undefined
     */
    unpressedLabel: {
        get: function () {
            return this._unpressedLabel;
        },
        set: function (value) {
            this._unpressedLabel = "" + value;
            this.needsDraw = true;
        }
    },

    /**
     * The amount of time in milliseconds the user must press and hold the
     * button a `longAction` event is dispatched. The default is 1 second.
     * @type {number}
     * @default 1000
     */
    holdThreshold: {
        value: 1000
    },

    _pressComposer: {
        value: null
    },

    _keyComposer: {
        value: null
    },

    _active: {
        value: false
    },

    /**
     * This property is true when the button is being interacted with, either
     * through mouse click or touch event, otherwise false.
     * @type {boolean}
     * @default false
     */
    active: {
        get: function () {
            return this._active;
        },
        set: function (value) {
            this._active = value;
            this.needsDraw = true;
        }
    },

    _pressed: {
        value: false
    },

    pressed: {
        set: function (value) {
            if (value !== this._pressed) {
                this._pressed = value;
                this.needsDraw = true;
            }
        },
        get: function () {
            return this._pressed;
        }
    },

    prepareForActivationEvents: {
        value: function () {
            this._pressComposer.addEventListener("pressStart", this, false);
            this._pressComposer.addEventListener("press", this, false);
            this._pressComposer.addEventListener("pressCancel", this, false);

            this._keyComposer.addEventListener("keyPress", this, false);
        }
    },

    // Optimisation
    addEventListener: {
        value: function (type, listener, useCapture) {
            AbstractControl.prototype.addEventListener.call(this, type, listener, useCapture);
            if (type === "longAction") {
                this._pressComposer.addEventListener("longPress", this, false);
            }
        }
    },

    // Handlers

    /**
     * Called when the user starts interacting with the component.
     * @private
     */
    handlePressStart: {
        value: function (event) {
            this.active = true;

            if (event.touch) {
                // Prevent default on touchmove so that if we are inside a scroller,
                // it scrolls and not the webpage
                document.addEventListener("touchmove", this, false);
            }
        }
    },

    /**
     * Called when the user has interacted with the button.
     * @private
     */
    handlePress: {
        value: function (event) {
            this.active = false;

            if (!this.enabled) {
                return;
            }

            this.pressed = !this.pressed;
            this.dispatchActionEvent();
            document.removeEventListener("touchmove", this, false);
        }
    },

    handleKeyPress: {
        value: function (event) {
            this.active = false;

            if (!this.enabled) {
                return;
            }

            this.pressed = !this.pressed;
            this.dispatchActionEvent();
        }
    },

    handleLongPress: {
        value: function (event) {
            // When we fire the "longAction" event we don't want to fire the
            // "action" event as well.
            this._pressComposer.cancelPress();

            var longActionEvent = document.createEvent("CustomEvent");
            longActionEvent.initCustomEvent("longAction", true, true, null);
            this.dispatchEvent(longActionEvent);
        }
    },

    /**
     * Called when all interaction is over.
     @private
     */
    handlePressCancel: {
        value: function (event) {
            this.active = false;
            document.removeEventListener("touchmove", this, false);
        }
    },

    handleTouchmove: {
        value: function (event) {
            event.preventDefault();
        }
    },

    _toggle: {
        value: function () {
            this.pressed = !this.pressed;
        }
    },

    /**
     * If this is an input element then the label is handled differently.
     * @private
     */
    isInputElement: {
        value: false
    },

    enterDocument: {
        value: function (firstDraw) {
            if(firstDraw) {
                this.isInputElement = (this.originalElement.tagName === "INPUT");

                if (!this.isInputElement) {
                    if (!this.originalElement.firstChild) {
                        this.originalElement.appendChild(document.createTextNode(""));
                    }
                    this._labelNode = this.originalElement.firstChild;
                }

                this.element.setAttribute("role", "button");
            }
        }
    },

    /**
     * Draws the label to the DOM.
     * @function
     * @private
     */
    _drawLabel: {
        enumerable: false,
        value: function (value) {
            if (this.isInputElement) {
                this._element.setAttribute("value", value);
            } else if (this._labelNode) {
                this._labelNode.data = value;
            }
        }
    },

    draw: {
        value: function () {
            if (this.pressed) {
                this._drawLabel(this.pressedLabel);
            } else {
                this._drawLabel(this.unpressedLabel);
            }

            this.element.setAttribute("aria-pressed", this.pressed);
        }
    }

});