Source: widgets/grid.js

define('application/widgets/grid', [
    'rofl/widgets/container',
    'rofl/widgets/label',
    'rofl/widgets/button',
    'rofl/widgets/horizontallist',
    'rofl/widgets/verticallist',
    'rofl/lib/l10n',
    'rofl/lib/utils',
    'rofl/widgets/carousel',
    'antie/widgets/carousel/keyhandlers/alignfirsthandler',
    'antie/widgets/carousel/keyhandlers/activatefirsthandler',
    'antie/widgets/carousel/strips/cullingstrip',
    'antie/events/keyevent',
    'antie/runtimecontext'
], function (
    Container,
    Label,
    Button,
    HorizontalList,
    VerticalList,
    L10N,
    Utils,
    Carousel,
    AlignFirstHandler,
    ActivateFirstHandler,
    CullingStrip,
    KeyEvent,
    RuntimeContext
) {
    'use strict';

    var PAGINATION = {
            DISABLED: 'disabled',
            MANUAL: 'manual',
            AUTOMATIC: 'automatic'
        },
        device = RuntimeContext.getDevice(),
        Grid;

    Grid = VerticalList.extend({

        /**
         * Initialises the grid.
         *
         * @param {string} id - The id.
         * @param {Object} config - The config.
         */
        init: function init (id, config) {
            var list,
                classNames;

            init.base.call(this, id, config);

            if (!config) {
                throw 'Grid configuration required.';
            }

            classNames = config.grid.classNames || [];
            this.addClass(['grid', 'partials-grid'].concat(classNames));
            this.setSettings(config);

            this.create();
            list = this._list;

            list.addEventListener('selecteditemchange', Utils.bind(this.onSelectedItemChange, this));
            list.addEventListener('focus', Utils.bind(this.onFocus, this));
            list.addEventListener('keydown', Utils.bind(this.onKeyDown, this));
            list.addEventListener('mousefocus', Utils.bind(this.onMouseFocus, this));

            if (config.grid.continuousListener) {
                list.setContinuousListener(true);
            }

            this.setActiveChildWidget(list);

            switch (this._paginationSettings.type) {
                case PAGINATION.MANUAL:
                    this.showPaginationButton();
                    this._paginationButton.addEventListener('select', Utils.bind(this.onSelectPartialsButton, this));
                    break;
                case PAGINATION.DISABLED:
                case PAGINATION.AUTOMATIC:
                    this.hidePaginationButton();
                    break;
            }
        },

        /**
         * Sets the grid settings.
         *
         * @param {Object} config - Config object.
         */
        setSettings: function (config) {
            var assetConfig = config.asset || {},
                gridConfig = config.grid || {},
                gridSettings = {},
                assetSettings = {};

            /*
             * Determines the grid columns based on defined columns or heigh and width.
             * If columns are supplied, use columns.
             * If asset width and grid width are supplied, calculate columns.
             * If neither is supplied, throw error.
             */
            if (gridConfig.columns) {
                this._columns = gridConfig.columns;
            } else {

                if (!assetConfig.width && !gridConfig.width) {

                    throw 'Columns not defined nor calculable.';
                }

                assetSettings.width = assetConfig.width;
                gridSettings.width = gridConfig.width;

                this._columns = Math.floor(gridConfig.width / assetConfig.width);
            }

            /*
             * Sets the asset formatter used to create the grid items.
             * Throws error if the formatter is not defined.
             */
            if (!assetConfig.formatter) {

                throw 'Grid requires an asset formatter to me supplied.';
            } else {

                assetSettings.formatter = new assetConfig.formatter();
            }

            /*
             * Determines if the grid should support culling.
             * Requires: asset height and grid height.
             * Throws error if culling is enabled but heights are missing.
             */
            if (gridConfig.culling) {

                if (!gridConfig.height || !assetConfig.height) {
                    throw 'Culling requires the grid and asset height to be defined.';
                }

                gridSettings.height = gridConfig.height;
                assetSettings.height = assetConfig.height;
                this._culling = true;
            }

            /*
             * Sets the alignpoint of the grid.
             * Defaults to middle (0.5) if not defined.
             */
            if (!Utils.isUndefined(gridConfig.alignPoint)) {
                gridSettings.alignPoint = gridConfig.alignPoint;
            } else {

                // Default to middle aligning.
                gridSettings.alignPoint = 0.5;
            }

            /*
             * Determines the end and begin of row navigation
             */
            if (gridConfig.navigateNext) {
                gridSettings.navigateNext = true;
            }

            if (gridConfig.navigatePrevious) {
                gridSettings.navigatePrevious = true;
            }

            if (gridConfig.widgetStrip) {
                gridSettings.widgetStrip = gridConfig.widgetStrip;
            }

            gridSettings.animOptions = gridConfig.animOptions;
            gridSettings.continuousListener = gridConfig.continuousListener;

            this._setPaginationSettings(config);
            this._gridSettings = gridSettings;
            this._assetSettings = assetSettings;
        },

        /**
         * Determines pagination settings.
         * Requires: callback if pagination is set to manual or automatic.
         * Throws error if callback is not defined but pagination is manual or automatic.
         *
         * @param {Object} gridConfig - The grid configuration.
         */
        _setPaginationSettings: function (gridConfig) {
            var paginationConfig = gridConfig.pagination || {},
                paginationType = paginationConfig.type || PAGINATION.DISABLED,
                paginationSettings = {},
                paginationCallback;

            if (paginationType !== PAGINATION.DISABLED) {

                paginationCallback = Utils.getNested(gridConfig, 'pagination', 'callback');

                if (!paginationCallback || !Utils.isFunction(paginationCallback)) {

                    throw 'Pagination setting requires a pagination callback function.';
                }

                paginationSettings.callback = paginationCallback;

                /*
                 * When the pagination type is automatic,
                 * we want to start loading new content from a specific row.
                 * Defaults to 3.
                 */
                if (paginationType === PAGINATION.AUTOMATIC) {
                    paginationSettings.loadFrom = !Utils.isUndefined(paginationConfig.loadFrom)
                        ? paginationConfig.loadFrom : 3;
                } else if (paginationType === PAGINATION.MANUAL) {

                    paginationSettings.label = paginationConfig.showMoreText;
                }
            }

            paginationSettings.type = paginationType;

            this._paginationSettings = paginationSettings;
        },

        /**
         * Creates the widget.
         */
        create: function () {
            this.createTitle();
            this.createSkeleton();
            this.createPaginationButton();
        },

        /**
         * Creates the name.
         */
        createTitle: function () {
            var title = this._title = new Label({ text: '', classNames: ['title', 'main-title'], enableHTML: true });

            this.appendChildWidget(title);
        },

        /**
         * Creates the skeleton.
         */
        createSkeleton: function () {
            var list = this._list = new Carousel(null, Carousel.orientations.VERTICAL),
                gridSettings = this._gridSettings,
                assetHeight = this._assetSettings.height,
                handler;

            if (this._culling) {
                list.setWidgetStrip(CullingStrip);
                list.setMaskLength(gridSettings.height);
                list.setWidgetLengths(assetHeight);
            }

            if (gridSettings.widgetStrip) {
                list.setWidgetStrip(gridSettings.widgetStrip);
            }

            if (gridSettings.activateFirstHandler) {
                handler = new ActivateFirstHandler();
            } else {
                handler = new AlignFirstHandler();
            }

            list.setNormalisedAlignPoint(gridSettings.alignPoint);
            handler.setAnimationOptions(gridSettings.animOptions || {
                    easing: 'easeIn',
                    fps: 60,
                    duration: 200,
                    skipAnim: true
                });
            handler.attach(list);

            list.addClass('skeleton');

            this.appendChildWidget(list);
        },

        /**
         * Creates the pagination button.
         */
        createPaginationButton: function () {
            var button = this._paginationButton = new Button(),
                label = new Label({ text: this._paginationSettings.label || '' });

            button.addClass('pagination');
            button.appendChildWidget(label);

            button.render(device);

            this.appendChildWidget(button);
        },

        /**
         * Creates a row.
         *
         * @returns {Object} - A row.
         */
        createRow: function () {
            var row = new HorizontalList(null, this._assetSettings.formatter);

            if (this._gridSettings.continuousListener) {
                row.setContinuousListener(true);
            }

            row.addClass('row');

            return row;
        },

        /**
         * Sets the data item.
         *
         * @param {Object} data - The data.
         * @param {string} data.name - The name.
         * @param {Array} data.items - The items.
         * @param {boolean} [shouldFocus] - True to focus to the grid.
         * @param {boolean} [shouldAlign] - True if the grid should align.
         */
        setDataItem: function setDataItem (data, shouldFocus, shouldAlign) {
            var list = this._list;

            shouldFocus = shouldFocus !== undefined ? shouldFocus : false;
            shouldAlign = shouldAlign !== undefined ? shouldAlign : true;

            setDataItem.base.call(this, data);

            if (list && list.outputElement) {
                list.alignToIndex(0, {
                    skipAnim: true
                });
            }

            list.removeChildWidgets();

            if (data.title) {
                this._title.setText(data.title);
            } else {
                this._title.addClass('display-none');
                this._title.setText('');
            }

            this.appendData(data.items, shouldFocus, shouldAlign);
        },

        /**
         * Prepends more data to the grid.
         *
         * @param {Array} items - The items.
         * @param {boolean} [shouldFocus] - If the new row should be focused.
         * @param {boolean} [shouldAlign] - If aligning should happen.
         */
        prependData: function (items, shouldFocus, shouldAlign) {
            var rowItemCount = this._columns,
                list = this._list,
                lastIndex = list.getActiveChildWidgetIndex(),
                formatter = this._assetSettings.formatter,
                rowIndex = 0,
                index,
                tempRow,
                lastRow,
                length;

            shouldFocus = shouldFocus !== undefined ? shouldFocus : false;
            shouldAlign = shouldAlign !== undefined ? shouldAlign : true;

            // First check if we can append something to the previous row.
            lastRow = list.getChildWidgetByIndex(0);

            if (lastRow && lastRow.getChildWidgetCount() < rowItemCount) {

                tempRow = lastRow;
            }

            for (index = 0, length = items.length; index < length; index++) {

                // Create new row if the max columns for the row has been reached.
                if (!tempRow || tempRow.getChildWidgetCount() >= rowItemCount) {
                    tempRow = this.createRow();
                    list.insert(rowIndex, tempRow, this._assetSettings.height);
                    rowIndex++;
                }
                tempRow.appendChildWidget(formatter.format(items[index]));
            }

            lastIndex++;

            if (list.getMask().getWidgetStrip().getChildWidgetCount() > lastIndex) {

                if (!shouldFocus && shouldAlign) {
                    list.setActiveChildIndex(lastIndex);

                    if (!list.getMask().getWidgetStrip().outputElement) {
                        list.getMask().getWidgetStrip().render(device);
                    }

                    list.alignToIndex(lastIndex);
                }

                if (shouldFocus) {

                    list.setActiveChildIndex(lastIndex);

                    if (!list.getMask().getWidgetStrip().outputElement) {
                        list.getMask().getWidgetStrip().render(device);
                    }

                    list.alignToIndex(lastIndex);

                    list.getChildWidgetByIndex(lastIndex).focus();
                }
            }
        },

        /**
         * Appends more data to the grid.
         *
         * @param {Array} items - The items.
         * @param {boolean} [shouldFocus] - If the new row should be focused.
         * @param {boolean} [shouldAlign] - If aligning should happen.
         */
        appendData: function (items, shouldFocus, shouldAlign) {
            var rowItemCount = this._columns,
                list = this._list,
                lastIndex = list.getActiveChildWidgetIndex(),
                formatter = this._assetSettings.formatter,
                index,
                tempRow,
                lastRow,
                length;

            shouldFocus = shouldFocus !== undefined ? shouldFocus : false;
            shouldAlign = shouldAlign !== undefined ? shouldAlign : true;

            // First check if we can append something to the previous row.
            lastRow = list.getChildWidgetByIndex(list.getChildWidgetCount() - 1);

            if (lastRow && lastRow.getChildWidgetCount() < rowItemCount) {

                tempRow = lastRow;
            }

            for (index = 0, length = items.length; index < length; index++) {

                // Create new row if the max columns for the row has been reached.
                if (!tempRow || tempRow.getChildWidgetCount() >= rowItemCount) {
                    tempRow = this.createRow();
                    list.append(tempRow, this._assetSettings.height);
                }
                tempRow.appendChildWidget(formatter.format(items[index]));
            }

            lastIndex++;

            if (list.getMask().getWidgetStrip().getChildWidgetCount() > lastIndex) {

                if (!shouldFocus && shouldAlign) {
                    list.setActiveChildIndex(lastIndex);

                    if (!list.getMask().getWidgetStrip().outputElement) {
                        list.getMask().getWidgetStrip().render(device);
                    }

                    list.alignToIndex(lastIndex);

                }

                if (shouldFocus) {

                    list.setActiveChildIndex(lastIndex);

                    if (!list.getMask().getWidgetStrip().outputElement) {
                        list.getMask().getWidgetStrip().render(device);
                    }

                    list.alignToIndex(lastIndex);

                    list.getChildWidgetByIndex(lastIndex).focus();
                }
            }
        },

        /**
         * Updated the given asset with the new data.
         *
         * @param {Object} data - New data to be set.
         * @param {Object} currentAsset - Current asset to update.
         */
        updateAsset: function (data, currentAsset) {
            var formatter = this._assetSettings.formatter;

            formatter.format(data, currentAsset);
        },

        /**
         * Displays the pagination button.
         */
        showPaginationButton: function () {
            this._paginationButton.setDisabled(false);
            this._paginationButton.show();
        },

        /**
         * Hides the pagination button.
         */
        hidePaginationButton: function () {
            this._paginationButton.setDisabled(true);
            this._paginationButton.hide();
        },

        /**
         * Gets executed when the selected item changes.
         *
         * @param {Object} e - The event data.
         */
        onSelectedItemChange: function (e) {

            if (e.item.hasClass('listitem') && e.target.isFocussed()) {
                this._lastIndex = e.index;
            }
        },

        /**
         * Gets the Grid carousel.
         *
         * @returns {rofl.widgets.carousel} - The Grid Carousel.
         */
        getList: function () {
            return this._list;
        },

        /**
         * Gets the Grid carousel's items.
         *
         * @returns {Array} - The Grid Carousel's items.
         */
        getCarouselItems: function () {
            var strip = this._list.getMask().getWidgetStrip(),
                items = [],
                carousels = strip.getChildWidgets();

            Utils.each(carousels, function (carousel) {
                items = items.concat(carousel.getChildWidgets());
            });

            return items;
        },

        /**
         * Gets executed when the focus changes.
         *
         * @param {Object} e - The event data.
         */
        onFocus: function (e) {
            var target = e.target,
                lastIndex,
                widgetCount;

            if (target.hasClass('carouselItem')) {

                lastIndex = this._lastIndex || 0;
                widgetCount = target.getChildWidgetCount();

                if (lastIndex >= widgetCount) {
                    lastIndex = widgetCount - 1;
                }

                e.target.setActiveChildIndex(lastIndex);
            }
        },

        /**
         * Gets executed on mouse focus.
         *
         * @param {Object} e - The event data.
         */
        onMouseFocus: function (e) {
            var list = this._list;

            list.alignToIndex(list.getIndexOfChildWidget(e.target.parentWidget));
        },

        /**
         * On select partials button handler.
         */
        onSelectPartialsButton: function () {
            this._loadMore();
        },

        /**
         * Loads more content.
         *
         * @private
         */
        _loadMore: function () {

            if (this._isLoading) {
                return;
            }

            this._isLoading = true;
            this._paginationSettings.callback(this.getTotal())
                .then(Utils.bind(function (response) {

                    if (Utils.getNested(response, 'items', 'length')) {
                        this.appendData(response.items, true);
                    }

                    this._isLoading = false;
                }, this));
        },

        /**
         * Disposes the widget.
         */
        dispose: function () {
            this._paginationSettings = null;
            this._assetSettings = null;
            this._gridSettings = null;
            this._columns = null;
            this._gridSettings = null;
            this._assetSettings = null;
        },

        /**
         * Resets the widget.
         */
        reset: function () {
            if (this._title) {
                this._title.setText('');
            }
            this._list.removeChildWidgets();
            this._gridSettings = {};
            this._assetSettings = {};
        },

        /**
         * Removes all data.
         */
        removeAll: function () {
            if (this._title) {
                this._title.setText('');
            }

            Utils.each(this._list.getChildWidgets(), function (widget) {
                Utils.each(widget.getChildWidgets(), function (innerWidget) {
                    innerWidget.dispose();
                });
            });
            this._list.removeChildWidgets();
        },

        /**
         * Shows the widget.
         *
         * @param {Object} e - The event data.
         */
        show: function show (e) {
            show.base.call(this, e);

            this.removeClass('display-none');
        },

        /**
         * Hides the widget.
         *
         * @param {Object} e - The event data.
         */
        hide: function hide (e) {
            hide.base.call(this, e);

            this.addClass('display-none');
        },

        /**
         * Determines if the widget is focusable.
         *
         * @returns {boolean} - The focusable state.
         */
        isFocusable: function isFocusable () {

            if (!this.isVisible()) {
                return false;
            }

            return isFocusable.base.call(this);
        },

        /**
         * Aligns the grid to the active index.
         */
        alignToActiveIndex: function () {
            var list = this._list,
                index = list.getActiveChildIndex();

            list.alignToIndex(index);
        },

        /**
         * Returns the active child index.
         *
         * @returns {number} - The active child index.
         */
        getActiveChildIndex: function () {
            return this._list.getActiveChildIndex();
        },

        /**
         * Get the total number of items in the grid.
         *
         * @returns {number} - The number of items.
         */
        getTotal: function () {
            var rows = this._list.getChildWidgets(),
                total = 0;

            Utils.each(rows, function (row) {
                total = total + row.getChildWidgetCount();
            });

            return total;
        },

        /**
         * Get the total number of rows in the grid.
         *
         * @returns {number} - The number of rows.
         */
        getRowsTotal: function () {
            return this._list.getChildWidgetCount();
        },

        /**
         * Aligns to proper index.
         *
         * @param {number} index - Index.
         */
        alignToIndexFunc: function (index) {
            var list = this._list;

            list.alignToIndex(index);
        },

        /**
         * Aligns to the first item.
         */
        alignToFirstItem: function () {
            var list = this._list;

            this._lastIndex = 0;

            list.alignToIndex(0);
        },

        /**
         * KeyDown event listener.
         *
         * @param {Object} e - The event params.
         */
        onKeyDown: function (e) {
            var rowIndex;

            switch (e.keyCode) {
                case KeyEvent.VK_LEFT:

                    if (this._gridSettings.navigatePrevious) {
                        rowIndex = this._getPreviousIndex();
                    }
                    break;
                case KeyEvent.VK_RIGHT:

                    if (this._gridSettings.navigateNext) {
                        rowIndex = this._getNextIndex();
                    }
                    break;
            }

            if (!Utils.isUndefined(rowIndex)) {
                this._list.alignToIndex(rowIndex);
            }
        },

        /**
         * Returns the next index.
         *
         * @returns {number|null} - The index or null.
         * @private
         */
        _getNextIndex: function () {
            var list = this._list,
                strip = list.getMask().getWidgetStrip(),
                activeIndex = strip.getActiveChildWidgetIndex(),
                active = strip.getActiveChildWidget(),
                newIndex,
                nextRow;

            // There is nothing to navigate to, already on the last row.
            if (activeIndex + 1 === strip.getChildWidgetCount()) {
                return undefined;
            }

            if (active.getChildWidgetCount() !== active.getActiveChildWidgetIndex() + 1) {
                return undefined;
            }

            nextRow = strip.getChildWidgetByIndex(activeIndex + 1);

            if (nextRow) {
                newIndex = 0;
                this._lastIndex = newIndex;

                return activeIndex + 1;
            }
        },

        /**
         * Returns the previous index.
         *
         * @returns {number|null} - The previous index or null.
         * @private
         */
        _getPreviousIndex: function () {
            var list = this._list,
                strip = list.getMask().getWidgetStrip(),
                activeIndex = strip.getActiveChildWidgetIndex(),
                active = strip.getActiveChildWidget(),
                newIndex,
                previousRow;

            // There is nothing to navigate to, already on the first row.
            if (activeIndex === 0) {
                return undefined;
            }

            if (active.getActiveChildWidgetIndex() !== 0) {
                return undefined;
            }

            previousRow = strip.getChildWidgetByIndex(activeIndex - 1);

            if (previousRow) {

                newIndex = previousRow.getChildWidgetCount() - 1;
                this._lastIndex = newIndex;

                return activeIndex - 1;
            }
        },

        /**
         * Returns the active row.
         *
         * @returns {Object} - The active row.
         */
        getActiveRow: function () {
            return this._list.getActiveChildWidget();
        },

        /**
         * Returns the number of rows.
         *
         * @returns {number} - Gets count of the rows.
         */
        getRowChildWidgetCount: function () {
            return this._list.getChildWidgetCount();
        },

        /**
         * Returns the active item index.
         *
         * @returns {number} - The active item index.
         */
        getActiveItemIndex: function () {
            return this.getActiveRow().getActiveChildWidgetIndex();
        },

        /**
         * Returns the title.
         *
         * @returns {Object} - The title label.
         */
        getTitle: function () {
            return this._title;
        }
    });

    Grid.PAGINATION = PAGINATION;

    return Grid;
});