/**
* @fileOverview Requirejs module containing the antie.widgets.List 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/list',
[
'antie/widgets/container',
'antie/widgets/listitem',
'antie/iterator',
'antie/events/databoundevent',
'antie/events/selecteditemchangeevent'
],
function (Container, ListItem, Iterator, DataBoundEvent, SelectedItemChangeEvent) {
'use strict';
/**
* The List widget contains an ordered list of items which may be populated either by a static
* array or by binding to an asynchronous data source.
* Note: The List widget has no spatial navigation. See {@link antie.widgets.VerticalList}
* and {@link antie.widgets.HorizontalList} for widgets that support spatial navigation.
* @name antie.widgets.List
* @class
* @extends antie.widgets.Container
* @requires antie.widgets.ListItem
* @requires antie.Iterator
* @requires antie.events.DataBoundEvent
* @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 {antie.Formatter} [itemFormatter] A formatter class used on each data item to generate the list item child widgets.
* @param {antie.DataSource|Array} [dataSource] An array of data to be used to generate the list items, or an asynchronous data source.
*/
var List = Container.extend(/** @lends antie.widgets.List.prototype */ {
/**
* @constructor
* @ignore
*/
init: function init (id, itemFormatter, dataSource) {
this._selectedIndex = 0;
this._dataSource = dataSource;
this._itemFormatter = itemFormatter;
this._dataBound = false;
this._totalDataItems = 0;
this._renderMode = List.RENDER_MODE_CONTAINER;
this._dataBindingOrder = List.DATA_BIND_FORWARD;
init.base.call(this, id);
this.addClass('list');
},
/**
* Appends a child widget to this widget, creating a new list item.
* @param {antie.widgets.Widget} widget The child widget to add.
*/
appendChildWidget: function appendChildWidget (widget) {
if ((this._renderMode === List.RENDER_MODE_LIST) && !(widget instanceof ListItem)) {
var li = new ListItem();
li.appendChildWidget(widget);
li.setDataItem(widget.getDataItem());
appendChildWidget.base.call(this, li);
return li;
} else {
widget.addClass('listitem');
appendChildWidget.base.call(this, 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) {
var w;
if ((this._renderMode === List.RENDER_MODE_LIST) && !(widget instanceof ListItem)) {
w = new ListItem();
w.appendChildWidget(widget);
w.setDataItem(widget.getDataItem());
insertChildWidget.base.call(this, index, w);
} else {
widget.addClass('listitem');
insertChildWidget.base.call(this, index, widget);
w = widget;
}
if (index <= this._selectedIndex &&
(( this._selectedIndex + 1 ) < this.getChildWidgetCount())) {
this._selectedIndex++;
}
return 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.
* @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)) {
this._selectedIndex = this.getIndexOfChildWidget(widget);
if (changed) {
this.bubbleEvent(new SelectedItemChangeEvent(this, widget, this._selectedIndex));
}
return true;
} else {
return false;
}
},
/**
* Renders the widget and any child widgets to device-specific output. If the list is bound
* to an asynchronous data source, get the data.
* @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._dataBound && this._dataSource && this._itemFormatter) {
this._createDataBoundItems();
}
if (!this.outputElement && (this._renderMode === List.RENDER_MODE_LIST)) {
this.outputElement = device.createList(this.id, this.getClasses());
}
return render.base.call(this, device);
},
/**
* Create list items from the bound data.
* @private
*/
_createDataBoundItems: function _createDataBoundItems () {
this._dataBound = true;
var self = this;
function processDataCallback(data) {
self.removeChildWidgets();
var iterator = (data instanceof Iterator) ? data : new Iterator(data);
while (iterator.hasNext()) {
var i = iterator._currentIndex;
var w = self._itemFormatter.format(iterator);
w._listIndex = i;
if (self._dataBindingOrder === List.DATA_BIND_FORWARD) {
self.appendChildWidget(w);
} else if (self._dataBindingOrder === List.DATA_BIND_REVERSE) {
self.insertChildWidget(0, w);
}
}
self._totalDataItems = iterator._currentIndex;
self.bubbleEvent(new DataBoundEvent('databound', self, iterator));
}
function processDataError(response) {
self.removeChildWidgets();
self.bubbleEvent(new DataBoundEvent('databindingerror', self, null, response));
}
self.bubbleEvent(new DataBoundEvent('beforedatabind', self));
if (!this._dataSource || (this._dataSource instanceof Array)) {
processDataCallback(this._dataSource);
} else {
this._dataSource.load({
onSuccess: processDataCallback,
onError: processDataError
});
}
},
/**
* Binds the list to a different data source. If the list is already rendered,
* the output will be updated to reflect the new data.
* @param {antie.DataSource} dataSource The data source to bind to.
*/
setDataSource: function setDataSource (dataSource) {
// abort currently processing data requests
if (this._dataSource && typeof(this._dataSource.abort) === 'function') {
this._dataSource.abort();
}
this._dataSource = dataSource;
if (this.outputElement) {
this._createDataBoundItems();
}
},
/**
* Invalidates the data-related bindings - causing items to be re-created on next render;
*/
resetDataBindings: function resetDataBindings () {
this._dataBound = false;
},
/**
* Re-iterates the data source, recreating list items.
*/
rebindDataSource: function rebindDataSource () {
this._dataBound = false;
this.setDataSource(this._dataSource);
},
/**
* Sets the rendering mode to either <code>List.RENDER_MODE_CONTAINER</code> or <code>List.RENDER_MODE_LIST</code>.
* List.RENDER_MODE_CONTAINER causes the list to be rendered as a generic container (e.g. <div>), with a generic container for each
* list item. List.RENDER_MODE_LIST causes the list to be rendered as a list (e.g. <ul>), with list item elements (e.g. <li>) for each item.
* @param {Integer} mode The rendering mode to use.
*/
setRenderMode: function setRenderMode (mode) {
this._renderMode = mode;
},
/**
* Binds a progress indicator widget to this list.
* @param {antie.Widgets.HorizontalProgress} widget The progress indicator widget.
* @param {Function} [formatterCallback] A function that tkes the current item index and the total number of items and returns
* a string to popular the progress indicator's label.
*/
bindProgressIndicator: function bindProgressIndicator (widget, formatterCallback) {
var self = this;
this._updateProgressHandler = function (evt) {
if (evt.target !== self) {
return;
}
if (evt.type === 'beforedatabind') {
widget.setText('');
return;
}
// TODO: This is a bit of a hack - if more data items were iterated over to populate the list
// TODO: than there are items in the list, we assume some list items contain more than one
// TODO: data item, therefore we have to use their position within the data source, rather than
// TODO: their position within the rendered list widget.
var ignore = self._childWidgetOrder.length - self._totalDataItems;
if (ignore < 0) {
ignore = 0;
}
var activeWidget = self.getActiveChildWidget();
var index = (self._dataBound && activeWidget && (activeWidget._listIndex !== undefined)) ?
activeWidget._listIndex :
self._selectedIndex - ignore;
var total = self._childWidgetOrder.length - ignore;
var p;
if (index < 0) {
p = 0;
} else {
p = index / (total - 1);
if (p < 0) {
p = 0;
}
}
if (formatterCallback) {
var val = formatterCallback(index + 1, total);
if (typeof(val) === 'string') {
widget.setText(val);
} else {
widget.setText(val.text);
p = val.pos;
}
}
//if the formatter function has moved the position indicator, we don't change it
widget.setValue(p);
};
this.addEventListener('selecteditemchange', this._updateProgressHandler);
this.addEventListener('focus', this._updateProgressHandler);
this.addEventListener('blur', this._updateProgressHandler);
this.addEventListener('beforedatabind', this._updateProgressHandler);
this.addEventListener('databound', this._updateProgressHandler);
},
/**
* Unbinds a previously-bound progress indicator widget.
*/
unbindProgressIndicator: function unbindProgressIndicator () {
if (this._updateProgressHandler) {
this.removeEventListener('selecteditemchange', this._updateProgressHandler);
this.removeEventListener('focus', this._updateProgressHandler);
this.removeEventListener('blur', this._updateProgressHandler);
this.removeEventListener('databound', this._updateProgressHandler);
}
},
removeChildWidget: function removeChildWidget (widget) {
// TODO: Make this more generic - it will only work if carousel items contain a
// TODO: single item of data.
if (this._updateProgressHandler && (this._childWidgetOrder.length < this._totalDataItems)) {
this.getCurrentApplication().getDevice().getLogger().warn('antie.widgets.List::removeChildWidget - removing' +
' list items where multiple data items are contained within each list item' +
' can cause unintended behaviour within any position indicator attached' +
' to the list.');
}
var ignore = this._childWidgetOrder.length - this._totalDataItems;
this._totalDataItems--;
var retValue = removeChildWidget.base.call(this, widget);
widget.removeClass('listitem');
for (var i = 0; i < this._childWidgetOrder.length; i++) {
this._childWidgetOrder[i]._listIndex = i - ignore;
}
return retValue;
},
removeChildWidgets: function removeChildWidgets () {
for (var i = 0; i < this._childWidgetOrder.length; i++) {
this._childWidgetOrder[i].removeClass('listitem');
}
this._totalDataItems = 0;
return removeChildWidgets.base.call(this);
},
setDataBindingOrder: function setDataBindingOrder (order) {
this._dataBindingOrder = order;
},
getDataBindingOrder: function getDataBindingOrder () {
return this._dataBindingOrder;
}
});
/**
* Render as a generic container (e.g. <div>), with a generic container for each list item.
* @name RENDER_MODE_CONTAINER
* @memberOf antie.widgets.List
* @constant
* @static
*/
List.RENDER_MODE_CONTAINER = 1;
/**
* Render as a list (e.g. <ul>), with list item elements (e.g. <li>) for each item.
* @name RENDER_MODE_LIST
* @memberOf antie.widgets.List
* @constant
* @static
*/
List.RENDER_MODE_LIST = 2;
List.DATA_BIND_FORWARD = 0;
List.DATA_BIND_REVERSE = 1;
return List;
}
);