/**
* @fileOverview Requirejs module containing the antie.widgets.Container abstract 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/container',
[
'antie/widgets/widget',
'antie/events/focusevent',
'antie/events/blurevent'
],
function(Widget, FocusEvent, BlurEvent) {
'use strict';
/**
* The abstract Container widget class represents any Widget that may contain children widgets.
* @name antie.widgets.Container
* @class
* @abstract
* @extends antie.widgets.Widget
* @requires antie.events.FocusEvent
* @requires antie.events.BlurEvent
* @param {String} [id] The unique ID of the widget. If excluded, a temporary internal ID will be used (but not included in any output).
*/
var Container;
Container = Widget.extend(/** @lends antie.widgets.Container.prototype */ {
/**
* @constructor
* @ignore
*/
init: function init (id) {
/*
* Performance consideration - do we need to store 2 references to each child widget?
* One keyed by ID, one in an array to maintain order?
*/
this._childWidgets = {};
this._childWidgetOrder = [];
this._activeChildWidget = null;
this._autoRenderChildren = true;
init.base.call(this, id);
this.addClass('container');
},
/**
* 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) {
var i;
if(!this.outputElement) {
this.outputElement = device.createContainer(this.id, this.getClasses());
} else {
device.clearElement(this.outputElement);
}
for(i=0; i<this._childWidgetOrder.length; i++) {
device.appendChildElement(this.outputElement, this._childWidgetOrder[i].render(device));
}
return this.outputElement;
},
/**
* Appends a child widget to this widget.
* @param {antie.widgets.Widget} widget The child widget to add.
*/
appendChildWidget: function appendChildWidget (widget) {
if(!this.hasChildWidget(widget.id)) {
this._childWidgets[widget.id] = widget;
this._childWidgetOrder.push(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();
device.appendChildElement(this.outputElement, widget.render(device));
}
return widget;
}
},
/**
* Inserts a widget before the current one.
* @param {antie.widgets.Widget} widget The child widget to
* add.
*/
prependWidget: function prependWidget (widget) {
// Find the current widget's order.
if(this.parentWidget instanceof Container) {
var widgetOrder, insertionIndex;
widgetOrder = this.parentWidget._childWidgetOrder;
for (insertionIndex = 0; insertionIndex !== widgetOrder.length; insertionIndex +=1) {
if (widgetOrder[insertionIndex] === this) {
break;
}
}
// Insert the new widget in the current one's position.
this.parentWidget.insertChildWidget(insertionIndex, widget);
}
return widget;
},
/**
* Inserts a child widget at the specified index.
* @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) {
if(!this.hasChildWidget(widget.id)) {
if(index >= this._childWidgetOrder.length) {
return this.appendChildWidget(widget);
}
this._childWidgets[widget.id] = widget;
this._childWidgetOrder.splice(index, 0, 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.insertChildElementBefore(this.outputElement, widget.outputElement, this._childWidgetOrder[index+1].outputElement);
}
return widget;
}
},
/**
* Remove all child widgets from this widget.
*/
removeChildWidgets: function removeChildWidgets () {
if(this._isFocussed && this._activeChildWidget) {
var logger = this.getCurrentApplication().getDevice().getLogger();
logger.warn('Removing widget that currently has focus: ' + this._activeChildWidget.id);
}
if(this.outputElement) {
var device = this.getCurrentApplication().getDevice();
device.clearElement(this.outputElement);
}
for(var i=0; i<this._childWidgetOrder.length; i++) {
this._childWidgetOrder[i].parentWidget = null;
}
this._childWidgets = {};
this._childWidgetOrder = [];
this._activeChildWidget = null;
},
/**
* Removes a specific child widget from this widget.
* @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) {
if(!widget) {
return;
}
var widget_index = this.getIndexOfChildWidget(widget);
if (widget_index < 0) {
return;
}
if(widget._isFocussed) {
var logger = this.getCurrentApplication().getDevice().getLogger();
logger.warn('Removing widget that currently has focus: ' + widget.id);
}
if(!retainElement && widget.outputElement) {
var device = this.getCurrentApplication().getDevice();
device.removeElement(widget.outputElement);
}
this._childWidgetOrder.splice(widget_index, 1);
delete(this._childWidgets[widget.id]);
widget.parentWidget = null;
},
/**
* Checks to see if a specific widget is a direct child of this widget.
* @param {String} id The widget id of the widget to check to see if it is a direct child of this widget.
*/
hasChildWidget: function hasChildWidget (id) {
return !!this._childWidgets[id];
},
/**
* Get a child widget from its unique ID.
* @param {String} id The id of the child widget to return.
* @returns antie.widgets.Widget of the widget with the given ID, otherwise undefined if the child does not exist.
*/
getChildWidget: function getChildWidget (id) {
return this._childWidgets[id];
},
/**
* Get an array of all this widget's children.
* @returns An array of all this widget's children.
*/
getChildWidgets: function getChildWidgets () {
return this._childWidgetOrder;
},
getIndexOfChildWidget: function getIndexOfChildWidget (widget) {
return this._childWidgetOrder.indexOf(widget);
},
/**
* 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 independent 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) {
if (!widget) {
return false;
}
if(this.hasChildWidget(widget.id) && widget.isFocusable()) {
if(this._activeChildWidget && this._activeChildWidget !== widget) {
this._activeChildWidget.removeClass('active');
this._setActiveChildFocussed(false);
}
widget.addClass('active');
this._activeChildWidget = widget;
if(!this.getCurrentApplication().getFocussedWidget()) {
var widgetIterator = this;
while(widgetIterator.parentWidget) {
widgetIterator.parentWidget._activeChildWidget = widgetIterator;
widgetIterator._isFocussed = true;
widgetIterator = widgetIterator.parentWidget;
}
}
if(this._isFocussed) {
this._setActiveChildFocussed(true);
}
return true;
}
return false;
},
/**
* Attempts to set focus to the child widget at the given index.
* @see #setActiveChildWidget
* @param {Integer} index Index of the child widget to set focus to.
* @returns Boolean true if the child widget was focusable, otherwise boolean false.
*/
setActiveChildIndex: function setActiveChildIndex (index) {
if(index < 0 || index >= this._childWidgetOrder.length) {
throw new Error('Widget::setActiveChildIndex Index out of bounds. ' + this.id + ' contains ' + this._childWidgetOrder.length + ' children, but an index of ' + index + ' was specified.');
}
return this.setActiveChildWidget(this._childWidgetOrder[index]);
},
/**
* Get the current active widget.
* @returns The current active widget
*/
getActiveChildWidget: function getActiveChildWidget () {
return this._activeChildWidget;
},
/**
* Flags the active child as focussed or blurred.
* @param {Boolean} focus True if the active child is to be focussed, False if the active child is to be blurred.
* @private
*/
_setActiveChildFocussed: function _setActiveChildFocussed (focus) {
if(this._activeChildWidget && (this._activeChildWidget._isFocussed !== focus)) {
this._activeChildWidget._isFocussed = focus;
if(focus) {
this._activeChildWidget.addClass('focus');
this._activeChildWidget.bubbleEvent(new FocusEvent(this._activeChildWidget));
// TODO: force focus to change in the application (rather than relying on the above
// TODO: even to propagate to the application level
} else {
this._activeChildWidget.removeClass('focus');
this._activeChildWidget.bubbleEvent(new BlurEvent(this._activeChildWidget));
}
this._activeChildWidget._setActiveChildFocussed(focus);
}
},
/**
* Gets the number of direct child widgets.
* @returns The number of direct child widgets.
*/
getChildWidgetCount: function getChildWidgetCount () {
return this._childWidgetOrder.length;
},
/**
* Checks to see if a widget is focussable, i.e. contains an enabled button.
* @see antie.widgets.Button
*/
isFocusable: function isFocusable () {
for(var i=0; i<this._childWidgetOrder.length; i++) {
if(this._childWidgetOrder[i].isFocusable()) {
if(!this._activeChildWidget) {
//this._activeChildWidget = this._childWidgetOrder[i];
this.setActiveChildWidget(this._childWidgetOrder[i]);
}
return true;
}
}
return false;
},
setAutoRenderChildren: function setAutoRenderChildren (autoRenderChildren) {
this._autoRenderChildren = autoRenderChildren;
},
/**
* 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++) {
this._childWidgetOrder[i].broadcastEvent(evt);
}
}
},
/**
* Moves focus to a button within this container. Focused button will be that which follows
* the current 'active' path.
* @returns Boolean true if focus has been moved to a button. Otherwise returns false.
*/
focus: function focus () {
if(this._activeChildWidget) {
return this._activeChildWidget.focus();
}
return false;
}
});
return Container;
}
);