Source: ui/button.reel/button.js

 /*global require, exports*/

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

// TODO migrate away from using undefinedGet and undefinedSet

/**
    Wraps a native <code>&lt;button></code> or <code>&lt;input[type="button"]></code> HTML element. The element's standard attributes are exposed as bindable properties.
    @class module:"montage/ui/native/button.reel".Button
    @extends module:montage/ui/control.Control
    @fires action
    @fires hold
    @example
<caption>JavaScript example</caption>
var b1 = new Button();
b1.element = document.querySelector("btnElement");
b1.addEventListener("action", function(event) {
    console.log("Got event 'action' event");
});
    @example
<caption>Serialized example</caption>
{
    "aButton": {
        "prototype": "montage/ui/native/button.reel",
        "values": {
            "element": {"#": "btnElement"}
        },
        "listeners": [
            {
                "type": "action",
                "listener": {"@": "appListener"}
            }
        ]
    },
    "listener": {
        "prototype": "appListener"
    }
}
&lt;button data-montage-id="btnElement"></button>
*/

var Button = exports.Button = Control.specialize(/** @lends module:"montage/ui/native/button.reel".Button# */ {

    /**
        Dispatched when the button is activated through a mouse click, finger tap,
        or when focused and the spacebar is pressed.

        @event action
        @memberof module:"montage/ui/native/button.reel".Button
        @param {Event} event
    */

    /**
        Dispatched when the button is pressed for a period of time, set by
        {@link holdThreshold}.

        @event hold
        @memberof module:"montage/ui/native/button.reel".Button
        @param {Event} event
    */

    standardElementTagName: {
        value: "BUTTON"
    },

    /* TODO: remove when adding template capability */
    hasTemplate: {
        value: false
    },

    drawsFocusOnPointerActivation : {
        value: true
    },

    /**
        converter
        A Montage converter object used to convert or format the label displayed by the Button instance. When a new value is assigned to <code>label</code>, the converter object's <code>convert()</code> method is invoked, passing it the newly assigned label value.
        @type {Property}
        @default null
    */

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

    _label: { value: undefined, enumerable: false },
    _emptyLabel: { value: "", enumerable: false },

    /**
        The displayed text on the button. In an &lt;input> element this is taken from the element's <code>value</code> attribute. On any other element (including &lt;button>) this is the first child node which is a text node. If one isn't found then it will be created.

        If the button has a non-null <code>converter</code> property, the converter object's <code>convert()</code> method is called on the value before being assigned to the button instance.

        @type {string}
        @default undefined
    */
    label: {
        get: function () {
            return this._label;
        },
        set: function (value) {
            if (typeof value !== "undefined" && this.converter) {
                try {
                    value = this.converter.convert(value);
                    if (this.error) {
                        this.error = null;
                    }
                } catch(e) {
                    // unable to convert - maybe error
                    this.error = e;
                }
            }

            this._label = String(value);

            if (this.isInputElement) {
                this._value = value;
            }

            this.needsDraw = true;
        }
    },

    // setLabelInitialValue: {
    //     value: function(value) {
    //         if (this._label === undefined) {
    //                 this._label = value;
    //             }
    //     }
    // },

    _promise: {
        value: undefined
    },

    promise: {
        get: function () {
            return this._promise;
        },
        set: function (value) {
            var self = this;
            var test = function promiseResolved(){
                            if (promiseResolved.promise === self._promise){
                                self.classList.remove('montage--pending');
                                self._promise = undefined;
                            }
                        };

            if (this._promise !== value) {
                this._promise = value;

                if (this._promise){
                    this.classList.add('montage--pending');
                    test.promise = value;
                    this._promise.then(test);
                }
            }
        }
    },

    /**
        The amount of time in milliseconds the user must press and hold the button a <code>hold</code> event is dispatched. The default is 1 second.
        @type {number}
        @default 1000
    */
    holdThreshold: {
        get: function () {
            return this._pressComposer.longPressThreshold;
        },
        set: function (value) {
            this._pressComposer.longPressThreshold = value;
        }
    },

    __pressComposer: {
        enumerable: false,
        value: null
    },

    _pressComposer: {
        enumerable: false,
        get: function () {
            if (!this.__pressComposer) {
                this.__pressComposer = new PressComposer();
                this.addComposer(this.__pressComposer);
            }

            return this.__pressComposer;
        }
    },

    __spaceKeyComposer: {
        value: null
    },

    _spaceKeyComposer: {
        get: function () {
            if (!this.__spaceKeyComposer) {
                this.__spaceKeyComposer = KeyComposer.createKey(this, "space", "space");
            }
            return this.__spaceKeyComposer;
        }
    },

    _enterKeyComposer: {
        get: function () {
            if (!this.__enterKeyComposer) {
                this.__enterKeyComposer = KeyComposer.createKey(this, "enter", "enter");
            }
            return this.__enterKeyComposer;
        }
    },

    // HTMLInputElement/HTMLButtonElement methods
    // click() deliberately omitted (it isn't available on <button> anyways)

    prepareForActivationEvents: {
        value: function () {
            this._pressComposer.addEventListener("pressStart", this, false);
            this._spaceKeyComposer.addEventListener("keyPress", this, false);
            this._enterKeyComposer.addEventListener("keyPress", this, false);
        }
    },

    handleKeyPress: {
        value: function (mutableEvent) {
            // when focused action event on spacebar & enter
            // FIXME - property identifier is not set on the mutable event
            if (mutableEvent._event.identifier === "space" ||
                mutableEvent._event.identifier === "enter") {
                this.active = false;
                this._dispatchActionEvent();
            }
        }
    },

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

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

            //fixme: @benoit: we should maybe have a flag for this kind of event.
            // can be tricky with the event delegation for example if we don't add it.
            // same issue for: the pressComposer and the translate composer.
            this._pressComposer.addEventListener("longPress", this, false);
        }
    },

    _removeEventListeners: {
        value: function () {
            this._pressComposer.removeEventListener("press", this, false);
            this._pressComposer.removeEventListener("pressCancel", this, false);
            this._pressComposer.removeEventListener("longPress", this, false);
        }
    },

    // Handlers

    /**
    Called when the user starts interacting with the component.
    */
    handlePressStart: {
        value: function (event) {
            if (!this._promise){
                this.active = true;
                this._addEventListeners();
            }
        }
    },

    /**
    Called when the user has interacted with the button.
    */
    handlePress: {
        value: function (event) {
            if (!this._promise){
                this.active = false;
                this._dispatchActionEvent();
                this._removeEventListeners();
            }
        }
    },

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

                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;
            this._removeEventListeners();
        }
    },

    /**
    If this is an input element then the label is handled differently.
    @private
    */
    _isInputElement: {
        value: undefined,
        enumerable: false
    },
    isInputElement: {
        get: function() {
            return this._isInputElement !== undefined ? this._isInputElement : (this._isInputElement = (this.element ? (this.element.tagName === "INPUT") : false));
        },
        enumerable: false
    },

    enterDocument: {
        value: function (firstDraw) {
            if (Control.prototype.enterDocument) {
                Control.prototype.enterDocument.apply(this, arguments);
            }

            if (firstDraw) {
                // Only take the value from the element if it hasn't been set
                // elsewhere (i.e. in the serialization)
                if (this.isInputElement) {
                    // NOTE: This might not be the best way to do this
                    // With an input element value and label are one and the same
                    Object.defineProperty(this, "value", {
                        get: function() {
                            return this._label;
                        },
                        set: function(value) {
                            this.label = value;
                        }
                    });

                    if (this._label === undefined) {
                        this.label = this.originalElement.value;
                    }
                    //<button> && Custom
                } else {
                    if(this.originalElement === this.element && this._label === undefined) {
                        this._label = this.originalElement.data;
                    }
                    if (!this.element.firstChild) {
                        this.element.appendChild(document.createTextNode(""));
                    }
                    this._labelNode = this.element.firstChild;
                    // this.setLabelInitialValue(this._labelNode.data)
                    // if (this._label === undefined) {
                    //     this._label = this._labelNode.data;
                    // }
                }

                //this.classList.add("montage-Button");
                this.element.setAttribute("role", "button");
                this.element.addEventListener("keyup", this, false);
            }
        }
    },

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


    draw: {
        value: function () {
            this.super();
            this._drawLabel(this._label);
        }
    }

});

Button.addAttributes( /** @lends module:"montage/ui/native/button.reel".Button# */{

/**
    The URL to which the form data will be sumbitted.
    @type {string}
    @default null
*/
    formaction: null,

/**
    The content type used to submit the form to the server.
    @type {string}
    @default null
*/
    formenctype: null,

/**
    The HTTP method used to submit the form.
    @type {string}
    @default null
*/
    formmethod: null,

/**
    Indicates if the form should be validated upon submission.
    @type {boolean}
    @default null
*/
    formnovalidate: {dataType: 'boolean'},

/**
    The target frame or window in which the form output should be rendered.
    @type string}
    @default null
*/
    formtarget: null,

/**
    A string indicating the input type of the component's element.
    @type {string}
    @default "button"
*/
    type: {value: 'button'},

/**
    The name associated with the component's DOM element.
    @type {string}
    @default null
*/
    name: null,

/**
    <strong>Use <code>label</code> to set the displayed text on the button</strong>
    The value associated with the element. This sets the value attribute of
    the button that gets sent when the form is submitted.
    @type {string}
    @default null
    @see label
*/
    value: null

});