Source: controls/List.js

var Scroller = require('./Scroller');
var ListCollection = require('../data/ListCollection');
var LayoutGroup = require('./LayoutGroup');
var DefaultListItemRenderer = require('./renderers/DefaultListItemRenderer');

/**
 * The basic list
 *
 * @class List
 * @extends GOWN.Scroller
 * @memberof GOWN
 * @constructor
 * @param [theme] theme for the list {GOWN.Theme}
 */
function List(theme) {
    Scroller.call(this, theme);

    /**
     * The skin name
     *
     * @type String
     * @default List.SKIN_NAME
     */
    this.skinName = this.skinName || List.SKIN_NAME;

    /**
     * Determines if items in the list may be selected. (not implemented yet)
     *
     * @private
     * @type bool
     * @default true
     */
    this._selectable = true;

    /**
     * The index of the currently selected item.
     *
     * @private
     * @type Number
     * @default -1
     */
    this._selectedIndex = -1;

    /**
     * If true multiple items may be selected at a time.
     *
     * @private
     * @type bool
     * @default false
     */
    this._allowMultipleSelection = false;

    /**
     * The indices of the currently selected items.
     *
     * @private
     * @type Number[]
     * @default []
     */
    this._selectedIndices = [];

    /**
     * The item renderer
     *
     * @private
     * @type Array
     * @default []
     */
    this._itemRenderer = [];

    /**
     * The item change handler
     *
     * @private
     * @type function
     */
    this._itemChangeHandler = this.itemChangeHandler.bind(this);

    /**
     * The item renderer change handler
     *
     * @private
     * @type function
     */
    this._itemRendererChangeHandler = this.itemRendererChangeHandler.bind(this);

    /**
     * The item renderer factory creates a new instance of the item renderer
     *
     * @private
     * @type function
     * @default this._defaultItemRendererFactory
     */
    this._itemRendererFactory = this._itemRendererFactory || this._defaultItemRendererFactory;

    /**
     * Properties that will be passed down to every item renderer when the list validates.
     *
     * @private
     * @type Object
     * @default {}
     */
    this._itemRendererProperties = {};

    // TODO: itemRendererStyleName (?)

    if (!this.viewPort) {
        /**
         * We do not implement ListDataViewPort from feathers
         * (most of what it does is implemented in List directly to
         * manage the viewport)
         * and instead use the normal LayoutGroup (less abstraction, less code)
         *
         * @private
         * @type GOWN.LayoutGroup
         */
        this.viewPort = new LayoutGroup();
    }
    this.layoutChanged = true;
}

List.prototype = Object.create( Scroller.prototype );
List.prototype.constructor = List;
module.exports = List;

/**
 * Default list skin name
 *
 * @static
 * @final
 * @type String
 */
List.SKIN_NAME = 'list';

/**
 * Dispatched when the selected item changes.
 *
 * @static
 * @final
 * @type String
 */
List.CHANGE = 'change';

/**
 * A function that is expected to return a new GOWN.DefaultListItemRenderer
 *
 * @param theme The item theme {GOWN.Theme}
 * @returns {DefaultListItemRenderer}
 * @private
 */
List.prototype._defaultItemRendererFactory = function(theme) {
    return new DefaultListItemRenderer(theme);
};

/**
 * Gets called when new data is added or removed
 * to the dataProvider
 *
 * @protected
 */
List.prototype.itemChangeHandler = function() {
    // TODO: test code so it will handle if item is removed
    // deselect removed items
    var index = this._dataProvider.data.length;
    if (this._selectedIndex >= index) {
        this._selectedIndex = -1;
    }
    var indexCount = this._selectedIndices.length;
    for (var i = 0; i < indexCount; i++) {
        var currentIndex = this._selectedIndices[i];
        if (currentIndex >= index) {
            this._selectedIndices.splice(i, 1);
        }
    }
    // force redraw
    this.dataInvalid = true;
};

/**
 * Select one of the items
 *
 * @param item The item to select {String}
 */
List.prototype.selectItem = function(item) {
    this.selectedIndex = this._dataProvider.data.indexOf(item);
};


/**
 * @private
 */
// performance increase to avoid using call.. (10x faster)
List.prototype.scrollerRedraw = Scroller.prototype.redraw;

/**
 * Update before draw call
 *
 * @protected
 */
List.prototype.redraw = function() {
    var basicsInvalid = this.dataInvalid;
    if (basicsInvalid) {
        this.refreshRenderers();
    }
    this.scrollerRedraw();

    if (!this.layout) {
        var layout = new GOWN.layout.VerticalLayout();
        layout.padding = 0;
        layout.gap = 0;
        layout.horizontalAlign = GOWN.layout.VerticalLayout.HORIZONTAL_ALIGN_JUSTIFY;
        layout.verticalAlign = GOWN.layout.VerticalLayout.VERTICAL_ALIGN_TOP;
        this.layout = layout;
    }
};

/**
 * Refresh the renderers
 */
List.prototype.refreshRenderers = function () {
    //TODO: update only new renderer
    //      see ListDataViewPort --> refreshInactieRenderers
    this._itemRenderer.length = 0;
    if (this._dataProvider && this.viewPort) {
        this.viewPort.removeChildren();
        for (var i = 0; i < this._dataProvider.length; i++) {
            var item = this._dataProvider.getItemAt(i);
            var itemRenderer = this._itemRendererFactory(this.theme);

            if (this._itemRendererProperties) {
                itemRenderer.labelField = this._itemRendererProperties.labelField;
            }

            itemRenderer.on('change', this._itemRendererChangeHandler);
            itemRenderer.data = item;
            this._itemRenderer.push(itemRenderer);
            this.viewPort.addChild(itemRenderer);
        }
    }

    this.dataInvalid = false;
};

/**
 * Item catch/forward renderer change event.
 * This is thrown when the state of the itemRenderer changes
 * (e.g. from unselected to selected), not when the data changes
 *
 * @protected
 */
List.prototype.itemRendererChangeHandler = function(itemRenderer, value) {
    // TODO: update selected item
    var i;
    this._selectedIndices.length = 0;

    if (!this.allowMultipleSelection) {
        for (i = 0; i < this._itemRenderer.length; i++) {
            if (this._itemRenderer[i] !== itemRenderer && value === true) {
                this._itemRenderer[i].selected = false;
            }
        }
        if (value === true) {
            this._selectedIndices = [this._itemRenderer.indexOf(itemRenderer)];
        }
    } else {
        for (i = 0; i < this._itemRenderer.length; i++) {
            if (this._itemRenderer[i].selected === true) {
                this._selectedIndices.push(i);
            }
        }
    }

    this.emit(List.CHANGE, itemRenderer, value);
};

/**
 * Set layout and pass event listener to it
 *
 * @name GOWN.List#layout
 * @type LayoutAlignment
 */
Object.defineProperty(List.prototype, 'layout', {
    set: function(layout) {
        if (this._layout === layout) {
            return;
        }
        if (this.viewPort) {
            // this is different from feathers - there the viewport does not
            // know the layout (feathers uses ListDataViewPort, not LayoutGroup
            // as viewPort for List)
            this.viewPort.layout = layout;
        }
        // TODO: this.invalidate(INVALIDATION_FLAG_LAYOUT);
    },
    get: function() {
        return this._layout;
    }
});

/**
 * Set item renderer properties (e.g. labelField) and update all itemRenderer
 *
 * @name GOWN.List#itemRendererProperties
 * @type Object
 */
Object.defineProperty(List.prototype, 'itemRendererProperties', {
    set: function(itemRendererProperties) {
        this._itemRendererProperties = itemRendererProperties;
        this.dataInvalid = true;
    },
    get: function() {
        return this._itemRendererProperties;
    }
});


/**
 * Set item renderer factory (for custom item Renderer)
 *
 * @name GOWN.List#itemRendererFactory
 * @type function
 */
Object.defineProperty(List.prototype, 'itemRendererFactory', {
    set: function(itemRendererFactory) {
        this._itemRendererFactory = itemRendererFactory;
        this.dataInvalid = true;
    },
    get: function() {
        return this._itemRendererFactory;
    }
});

/**
 * Allow/disallow multiple selection.
 * If selection has been disallowed, deselect all but one.
 *
 * @name GOWN.List#allowMultipleSelection
 * @type bool
 */
 Object.defineProperty(List.prototype, 'allowMultipleSelection', {
     set: function(allowMultipleSelection) {
         if (this._allowMultipleSelection === allowMultipleSelection) {
             return;
         }
         this._allowMultipleSelection = allowMultipleSelection;

         if (!this._allowMultipleSelection && this._selectedIndices) {
             // only last index is selected
             this._selectedIndices = [this._selectedIndices.pop()];
         }
         //TODO: this.refreshSelection();
     },
     get: function() {
         return this._allowMultipleSelection;
     }
 });

/**
 * The index of the selected item
 *
 * @name GOWN.List#selectedIndex
 * @type Number
 */
Object.defineProperty(List.prototype, 'selectedIndex', {
    set: function(selectedIndex) {
        this._selectedIndex = selectedIndex;
        // force redraw
        this.dataInvalid = true;
    },
    get: function() {
        return this._selectedIndex;
    }
});

/**
 * dataProvider for list.
 * The dataProvider is a structure that provides the data.
 * In its simplest form it is an array containing the data
 *
 * @name GOWN.List#dataProvider
 * @type Array
 */
Object.defineProperty(List.prototype, 'dataProvider', {
    set: function(dataProvider) {
        if (this._dataProvider === dataProvider) {
            return;
        }
        if (!(dataProvider instanceof ListCollection || dataProvider === null)) {
            throw new Error('the dataProvider has to be a GOWN.ListCollection');
        }

        if (this._dataProvider) {
            this._dataProvider.off(ListCollection.CHANGED, this._itemChangeHandler);
        }
        this._dataProvider = dataProvider;

        //reset the scroll position because this is a drastic change and
        //the data is probably completely different
        this.horizontalScrollPosition = 0;
        this.verticalScrollPosition = 0;

        if (this._dataProvider) {
            this._dataProvider.on(ListCollection.CHANGED, this._itemChangeHandler);
        }

        this.selectedIndex = -1;
        this.dataInvalid = true;
    },
    get: function() {
        return this._dataProvider;
    }
});

// TODO: selectedItem