Source: widgets/grid.js

/**
 * @fileOverview Requirejs module containing the antie.widgets.Grid class.
 * @preserve Copyright (c) 2013-present British Broadcasting Corporation. All rights reserved.
 * @license See https://github.com/fmtvp/tal/blob/master/LICENSE for full licence
 */

define(
    'antie/widgets/grid',
    [
        'antie/widgets/container',
        'antie/events/keyevent',
        'antie/events/selecteditemchangeevent'
    ],
    function (Container, KeyEvent, SelectedItemChangeEvent) {
        'use strict';

        /**
         * The Grid widget class represents a grid of widgets that may be navigated between using up/down/left/right.
         * @name antie.widgets.Grid
         * @class
         * @extends antie.widgets.Container
         * @requires antie.events.KeyEvent
         * @requires antie.events.SelectedItemChangeEvent
         * @param {String} [id] The unique ID of the widget. If excluded, a temporary internal ID will be used (but not included in any output).
         * @param {Integer} cols The number of columns in the grid.
         * @param {Integer} rows The number of rows in the grid.
         * @param {boolean} horizontalWrapping Enable or disable horizontal wrapping.
         * @param {boolean} verticalWrapping Enable or disable vertical wrapping.
         */
        var Grid = Container.extend(/** @lends antie.widgets.Grid.prototype */ {
            /**
             * @constructor
             * @ignore
             */
            init: function init (id, cols, rows, horizontalWrapping, verticalWrapping) {
                init.base.call(this, id);
                this.addClass('grid');

                this._cols = cols;
                this._rows = rows;

                this._horizontalWrapping =  !!horizontalWrapping;
                this._verticalWrapping = !!verticalWrapping;

                this._selectedRow = 0;
                this._selectedCol = 0;

                for (var i = 0; i < cols * rows; i++) {
                    this._childWidgetOrder.push(null);
                }

                var self = this;
                this.addEventListener('keydown', function (e) {
                    self._onKeyDown(e);
                });
            },
            /**
             * Get the widget positioned at the specified column and row.
             * @param {Integer} col The column the widget it in
             * @param {Integer} row The row the widget it in
             * @returns The widget in the specified column and row
             */
            getWidgetAt: function getWidgetAt (col, row) {
                return this._childWidgetOrder[(this._cols * row) + col];
            },
            /**
             * Positions a widget at the specified column and row.
             * @param {Integer} col The column to position the widget in
             * @param {Integer} row The row to position the widget in
             * @param {antie.widgets.Widget} widget The widget to add
             */
            setWidgetAt: function setWidgetAt (col, row, widget) {
                if (!this.hasChildWidget(widget.id)) {
                    this._childWidgets[widget.id] = widget;
                    this._childWidgetOrder[(this._cols * row) + col] = widget;
                    widget.parentWidget = this;

                    // If there's no active child widget set, try and set it to this
                    // (Will only have an affect if it's focusable (i.e. contains a button))
                    if (!this._activeChildWidget) {
                        this.setActiveChildWidget(widget);
                    }

                    if (this.outputElement && this._autoRenderChildren) {
                        var device = this.getCurrentApplication().getDevice();

                        if (!widget.outputElement) {
                            widget.render(device);
                        }

                        device.appendChildElement(this.outputElement, widget.outputElement);
                    }
                }
            },
            /**
             * Renders the widget and any child widgets to device-specific output.
             * @param {antie.devices.Device} device The device to render to.
             * @returns A device-specific object that represents the widget as displayed on the device (in a browser, a DOMElement);
             */
            render: function render (device) {
                if (!this.outputElement) {
                    this.outputElement = device.createContainer(this.id, this.getClasses());
                } else {
                    device.clearElement(this.outputElement);
                }

                var rowElement;
                for (var row = 0; row < this._rows; row++) {
                    rowElement = device.createContainer(this.id + '_row_' + row, ['row']);
                    for (var col = 0; col < this._cols; col++) {
                        var widget = this.getWidgetAt(col, row);
                        if (widget) {
                            if (col === 0) {
                                widget.addClass('firstcol');
                            } else if (col === this._cols - 1) {
                                widget.addClass('lastcol');
                            }
                            device.appendChildElement(rowElement, this.getWidgetAt(col, row).render(device));
                        } else {
                            var classes = ['spacer'];
                            if (col === 0) {
                                classes.push('firstcol');
                            } else if (col === this._cols - 1) {
                                classes.push('lastcol');
                            }
                            device.appendChildElement(rowElement, device.createContainer(this.id + '_' + col + '_' + row, classes));
                        }
                    }
                    device.appendChildElement(this.outputElement, rowElement);
                }

                return this.outputElement;
            },
            /**
             * Appends a child widget to this widget. Not supported for Grids - use setWidgetAt() instead.
             * @param {antie.widgets.Widget} widget The child widget to add.
             */
            appendChildWidget: function appendChildWidget (/*widget*/) {
                throw new Error('Not supported for Grids - use setWidgetAt() instead.');
            },
            /**
             * Inserts a child widget at the specified index. Not supported for Grids.
             * @param {Integer} index The index where to insert the child widget.
             * @param {antie.widgets.Widget} widget The child widget to add.
             */
            insertChildWidget: function insertChildWidget (/*index, widget*/) {
                throw new Error('Not supported');
            },
            /**
             * Removes a specific child widget from this widget. Not supported for Grids.
             * @param {antie.widgets.Widget} widget The child widget to remove.
             * @param {Boolean} [retainElement] Pass <code>true</code> to retain the child output element of the given widget
             */
            removeChildWidget: function removeChildWidget (/*widget, retainElement*/) {
                throw new Error('Not supported');
            },
            /**
             * Attempt to set focus to the given child widget.
             *
             * Note: You can only set focus to a focusable widget. A focusable widget is one that
             * contains an enabled antie.widgets.Button as either a direct or indirect child.
             *
             * Note: Widgets have 2 independant states: active and focussed. A focussed widget is
             * either the Button with focus, or any parent of that Button. An active widget is
             * one which is the active child of its parent Container. When the parent widget
             * receives focus, focus will be placed on the active child.
             *
             * Classes 'active' and 'focus' are appended to widgets with these states.
             *
             * @param {antie.widgets.Widget} widget The child widget to set focus to.
             * @returns Boolean true if the child widget was focusable, otherwise boolean false.
             */
            setActiveChildWidget: function setActiveChildWidget (widget) {
                var changed = this._activeChildWidget !== widget;
                if (setActiveChildWidget.base.call(this, widget)) {
                    var selectedIndex = this.getIndexOfChildWidget(widget);
                    this._selectedRow = Math.floor(selectedIndex / this._cols);
                    this._selectedCol = Math.floor(selectedIndex % this._cols);

                    if (changed) {
                        this.bubbleEvent(new SelectedItemChangeEvent(this, widget, selectedIndex));
                    }
                    return true;
                } else {
                    return false;
                }
            },
            /**
             * Key handler for grids. Processes KeyEvent.VK_UP, VK_DOWN, VK_LEFT and VK_RIGHT keys and stops propagation
             * if the keypress is handled. Otherwise allows the event to be bubbled up to the parent widget to allow
             * spatial navigation out of the list.
             * @param {antie.events.KeyEvent} evt The key event.
             */
            _onKeyDown: function _onKeyDown (evt) {
                if (evt.keyCode !== KeyEvent.VK_UP && evt.keyCode !== KeyEvent.VK_DOWN &&
                    evt.keyCode !== KeyEvent.VK_LEFT && evt.keyCode !== KeyEvent.VK_RIGHT) {
                    return;
                }

                var _newSelectedCol = this._selectedCol;
                var _newSelectedRow = this._selectedRow;
                var _newSelectedWidget = null;
                do {
                    if (evt.keyCode === KeyEvent.VK_UP) {
                        _newSelectedRow--;
                    } else if (evt.keyCode === KeyEvent.VK_DOWN) {
                        _newSelectedRow++;
                    } else if (evt.keyCode === KeyEvent.VK_LEFT) {
                        _newSelectedCol--;
                    } else if (evt.keyCode === KeyEvent.VK_RIGHT) {
                        _newSelectedCol++;
                    }

                    if (_newSelectedCol < 0) {
                        if(this._horizontalWrapping) {
                            _newSelectedCol = this._cols - 1;
                        } else {
                            break;
                        }
                    }

                    if(_newSelectedCol >= this._cols) {
                        if(this._horizontalWrapping) {
                            _newSelectedCol = 0;
                        } else {
                            break;
                        }
                    }

                    if (_newSelectedRow < 0) {
                        if(this._verticalWrapping) {
                            _newSelectedRow = this._rows - 1;
                        } else {
                            break;
                        }
                    }

                    if(_newSelectedRow >= this._rows) {
                        if(this._verticalWrapping) {
                            _newSelectedRow = 0;
                        } else {
                            break;
                        }
                    }

                    var _newSelectedIndex = (_newSelectedRow * this._cols) + _newSelectedCol;
                    var _widget = this._childWidgetOrder[_newSelectedIndex];
                    if (_widget && _widget.isFocusable()) {
                        _newSelectedWidget = _widget;
                        break;
                    }
                } while (true);

                if (_newSelectedWidget) {
                    this.setActiveChildWidget(_newSelectedWidget);
                    evt.stopPropagation();

                }
            },

            /**
             * Broadcasts an event from the application level to every single
             * object it contains.
             */
            broadcastEvent: function broadcastEvent (evt) {
                this.fireEvent(evt);
                if (!evt.isPropagationStopped()) {
                    for (var i = 0; i < this._childWidgetOrder.length; i++) {
                        // Grids are the only type of container that may contain
                        // null entries in the _childWidgetOrder array
                        if (this._childWidgetOrder[i]) {
                            this._childWidgetOrder[i].broadcastEvent(evt);
                        }
                    }
                }
            }
        });

        Grid.WRAP_MODE = {
            HORIZONTAL: {
                ON: 1,
                OFF: 0
            },

            VERTICAL: {
                ON: 1,
                OFF: 0
            }
        };

        return Grid;
    }
);