/**
* @fileOverview Requirejs module containing the antie.widgets.ComponentContainer 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/componentcontainer',
[
'antie/widgets/container',
'antie/events/componentevent'
],
function (Container, ComponentEvent) {
'use strict';
var _knownComponents = {};
/**
* The ComponentContainer widget class represents a container that Components (sections of UI) may be dynamically loaded into.
* @name antie.widgets.ComponentContainer
* @class
* @extends antie.widgets.Container
* @requires antie.events.ComponentEvent
* @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 ComponentContainer = Container.extend(/** @lends antie.widgets.ComponentContainer.prototype */ {
/**
* @constructor
* @ignore
*/
init: function init (id) {
this._loadingIndex = 0;
this._loadingModule = null;
this._currentModule = null;
this._currentComponent = null;
this._currentArgs = null;
this._historyStack = [];
this._previousFocus = null;
init.base.call(this, id);
this.addClass('componentcontainer');
},
/**
* Callback called when a requirejs containing a component has been loaded.
* @private
* @param {String} module The requirejs module name of the loaded component.
* @param {Class} componentClass The subclass of antie.widgets.Component which has been loaded.
* @param {Object} [args] The arguments passed to populate the component.
*/
_loadComponentCallback: function _loadComponentCallback (module, componentClass, args, keepHistory, state) {
if (!this.getCurrentApplication()) {
// Application has been destroyed, abort
return;
}
var newComponent = new componentClass();
// Add the component to our table of known components.
_knownComponents[module] = newComponent;
// set the parent widget so the next event bubbles correctly through the tree
newComponent.parentWidget = this;
newComponent.bubbleEvent(new ComponentEvent('load', this, _knownComponents[module], args));
// clear the parent widget again
newComponent.parentWidget = null;
// Show the component.
this.show(module, args, keepHistory, state);
},
/**
* Show a component within this container. If the specified component has not been loaded, load it.
* @param {String} module The requirejs module name of the component to show.
* @param {Object} [args] The arguments to populate the component.
* @param {Boolean} [keepHistory] If true, the current component shown in this container is preserved in the history stack.
* @param {Object} [state] Additional component-specific state information
*/
show: function show (module, args, keepHistory, state, fromBack, focus) {
this._loadingModule = module;
this._loadingIndex++;
var loadingIndex = this._loadingIndex;
var self;
if (_knownComponents[module]) {
var device = this.getCurrentApplication().getDevice();
var _focus = this.getCurrentApplication().getFocussedWidget();
if (this._currentComponent) {
this.hide(null, args, keepHistory, state, fromBack);
}
this._currentModule = module;
this._currentComponent = _knownComponents[module];
this._currentArgs = args;
if (!fromBack) {
this._previousFocus = _focus;
}
if (!this._isFocussed) {
// We don't have focus, so any of our children shouldn't
// (_isFocussed state can be set to true if focussed widget is in a unloaded component)
var p = this._currentComponent;
while (p) {
p.removeFocus();
p = p._activeChildWidget;
}
}
// set the parent widget so the next event bubbles correctly through the tree
this._currentComponent.parentWidget = this;
this._currentComponent.bubbleEvent(new ComponentEvent('beforerender', this, this._currentComponent, args, state, fromBack));
this._currentComponent.render(device);
// and clear it again
this._currentComponent.parentWidget = null;
device.hideElement({
el: this._currentComponent.outputElement,
skipAnim: true
});
this.appendChildWidget(this._currentComponent);
var evt = new ComponentEvent('beforeshow', this, this._currentComponent, args, state, fromBack);
this._currentComponent.bubbleEvent(evt);
if (focus) {
focus.focus();
}
self = this;
if (!evt.isDefaultPrevented()) {
var config = device.getConfig();
var animate = !config.widgets || !config.widgets.componentcontainer || (config.widgets.componentcontainer.fade !== false);
device.showElement({
el: this._currentComponent.outputElement,
skipAnim: !animate
});
}
self._currentComponent.bubbleEvent(new ComponentEvent('aftershow', self, self._currentComponent, args, state, fromBack));
var focusRemoved = self.setActiveChildWidget(self._currentComponent);
if (!focusRemoved) {
self._activeChildWidget = self._currentComponent;
self.getCurrentApplication().getDevice().getLogger().warn('active component is not currently focusable', self._activeChildWidget);
}
} else {
// hook into requirejs to load the component from the module and call us again
self = this;
require([module], function (componentClass) {
// Check we've not navigated elsewhere whilst requirejs has been loading the module
if (self._loadingModule === module && self._loadingIndex === loadingIndex) {
self._loadComponentCallback(module, componentClass, args, keepHistory, state, fromBack, focus);
}
});
}
},
/**
* Pushes a component into the history stack of the container (and shows it).
* @param {String} module The requirejs module name of the component to show.
* @param {Object} [args] An optional object to pass arguments to the component.
*/
pushComponent: function pushComponent (module, args) {
this.show(module, args, true);
},
/**
* Returns the widget added to this container.
*/
getContent: function getContent () {
return this._currentComponent;
},
/**
* Return this component container to the previous component in the history.
*/
back: function back () {
var _focus = this._currentComponent.getIsModal() ? this._previousFocus : null;
var _lastComponent = this._historyStack.pop();
if (_lastComponent) {
this._previousFocus = _lastComponent.previousFocus;
this.show(_lastComponent.module, _lastComponent.args, true, _lastComponent.state, true, _focus);
} else {
this.hide(null, null, false, null, false);
}
},
/**
* Hide the component within this container.
*/
hide: function hide (focusToComponent, args, keepHistory, state, fromBack) {
if (this._currentComponent) {
var evt = new ComponentEvent('beforehide', this, this._currentComponent, args, state, fromBack);
this._currentComponent.bubbleEvent(evt);
var _state = keepHistory ? this._currentComponent.getCurrentState() : null;
// remove the child widget, but if default event is prevented, keep the output element in the DOM
this.removeChildWidget(this._currentComponent, evt.isDefaultPrevented());
var _component = this._currentComponent;
this._currentComponent = null;
// set the parent widget so the next event bubbles correctly through the tree
_component.parentWidget = this;
_component.bubbleEvent(new ComponentEvent('afterhide', this, _component, args, state, fromBack));
// and clear it again
_component.parentWidget = null;
if (keepHistory) {
if (!fromBack) {
this._historyStack.push({
module: this._currentModule,
args: this._currentArgs,
state: _state,
previousFocus: this._previousFocus
});
}
} else {
// Reset the history stack when a component is shown in this container without explicitly
// enabling history.
if (_component.getIsModal() && !fromBack) {
if (this._historyStack.length > 0) {
this._historyStack[0].previousFocus.focus();
} else if (this._previousFocus) {
this._previousFocus.focus();
}
}
this._historyStack = [];
}
}
if (this._isFocussed && focusToComponent) {
this.parentWidget.setActiveChildWidget(this.parentWidget._childWidgets[focusToComponent]);
}
},
getCurrentModule: function getCurrentModule () {
return this._currentModule;
},
getCurrentArguments: function getCurrentArguments () {
return this._currentArgs;
}
});
ComponentContainer.destroy = function () {
for (var module in _knownComponents) {
if(_knownComponents.hasOwnProperty(module)) {
delete _knownComponents[module];
}
}
};
return ComponentContainer;
}
);