Source: ui/base/abstract-select.js

/*global require, exports*/

/**
 * @module montage/ui/base/abstract-select.reel
 */
var AbstractControl = require("./abstract-control").AbstractControl,
    PressComposer = require("../../composer/press-composer").PressComposer,
    RangeController = require("../../core/range-controller").RangeController;

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

    /**
     * Dispatched when the select is changed through a mouse click or finger
     * tap.
     * @event action
     * @memberof AbstractSelect
     * @param {Event} event
     */

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

            this._pressComposer = new PressComposer();
            this.addComposer(this._pressComposer);
            this.contentController = new RangeController();

            this.addPathChangeListener("contentController", this, "handleContentControllerChange");

            this.defineBindings({
                "content": {
                    "<->": "contentController.content"
                },
                // FIXME: due to issues with 2 way bindings with rangeContent()
                // we aren't currently able to have this "value" binding.
                // "value": {
                //     "<->": "values.one()"
                // },
                "contentController.allowsMultipleSelection": {
                    "<-": "multiSelect"
                },
                // classList management
                "classList.has('montage--disabled')": {
                    "<-": "!enabled"
                },
                "classList.has('montage--active')": {
                    "<-": "active"
                }
            });

            // Need to draw when "content" or "values" change
            this.addRangeAtPathChangeListener("content", this, "handleContentRangeChange");

            // TODO: "value" <-> "values.one()"
            this.addRangeAtPathChangeListener("values", this, "handleValuesRangeChange");
            this.classList.add("matte-Select");
        }
    },

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

    acceptsActiveTarget: {
        value: true
    },

    _pressComposer: {
        value: null
    },

    active: {
        value: false
    },

    content: {
        value: null
    },

    contentController: {
        value: null
    },

    _labelPropertyName: {
        value: "label"
    },

    labelPropertyName: {
        set: function (value) {
            if (value) {
                this._labelPropertyName = value;
            } else {
                this._labelPropertyName = "label";
            }
            this._contentIsDirty = true;
            this.needsDraw = true;
        },
        get: function () {
            return this._labelPropertyName;
        }
    },

    _value: {
        value: null
    },

    value: {
        get: function () {
            return this._value;
        },
        set: function (value) {
            if (value !== this._value) {
                this._value = value;
                // TODO: "value" <-> "values.one()"
                if (this.values[0] !== value) {
                    this.values.splice(0, this.values.length, value);
                }
                this.needsDraw = true;
            }
        }
    },

    _values: {
        value: null
    },

    values: {
        get: function () {
            return this._values;
        },
        set: function (value) {
            var args = [0, this._values.length].concat(value);
            this._values.splice.apply(this._values, args);

            this.needsDraw = true;
        }
    },

    multiSelect: {
        value: false
    },

    _contentIsDirty: {
        value: true
    },

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

    // Handlers

    /**
     * Called when the user starts interacting with the component.
     */
    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 select.
     */
    handlePress: {
        value: function (event) {
            this.active = false;

            if (!this.enabled) {
                return;
            }

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

    /**
     * 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();
        }
    },

    handleContentRangeChange: {
        value: function () {
            // When the content changes we need to update the "value" if none is
            // set (new content) or if the previous "value" was removed in this
            // range change.
            // FIXME: we only operate on the selection and not on the "values"
            // to avoid issues with 2-way binding to rangeContent().
            if (this.contentController.selection.length === 0 &&
                this.contentController.organizedContent.length > 0) {
                this.contentController.selection.push(this.contentController.organizedContent[0]);
            }

            this._contentIsDirty = true;
            this.needsDraw = true;
        }
    },

    handleValuesRangeChange: {
        value: function () {
            // TODO: "value" <-> "values.one()"
            if (this.values.length > 0) {
                this.value = this.values.one();
            }
            this.needsDraw = true;
        }
    },

    handleContentControllerChange: {
        value: function (value) {
            if (value) {
                this._values = value.selection;
                this.handleValuesRangeChange();
            }
        }
    },

    enterDocument: {
        value: function (firstDraw) {
            if(firstDraw) {
                this.element.setAttribute("role", "listbox");
            }
        }
    }

});