Source: br-presenter/src/br/presenter/component/PresenterComponent.js

"use strict";

/**
 * @module br/presenter/component/PresenterComponent
 */

br.Core.thirdparty("presenter-knockout");

var Utility = require('br/core/Utility');
var Errors = require('br/Errors');
var ServiceRegistry = require('br/ServiceRegistry');

/**
 * Constructs a new instance of <code>PresenterComponent</code>. Instances of <code>PresenterComponent</code> can also
 * be created from an XML snippet using the {@link module:br/presenter/component/PresenterComponent#deserialize} method.
 *
 * @class
 * @alias module:br/presenter/component/PresenterComponent
 * @implements module:br/component/Component
 *
 * @classdesc
 * <p>When component life cycle events are triggered on the <code>PresenterComponent</code> these
 * are proxied through to the <code>PresentationModel</code> if they are defined in the <code>PresentationModel</code>.</p>
 *
 * @param {String} sTemplateId The id of a template to render the presentation model with.
 * @param {Object} vPresentationModel A presentation model instance, or the name of a presentation model class that can be constructed.
 */
function PresenterComponent(sTemplateId, vPresentationModel) {
	this.m_sTemplateId = sTemplateId;
	this.m_eTemplate = this._getTemplate(sTemplateId);
	this.m_sPresentationModel = null;
	this.m_oPresentationModel = null;
	this.m_bViewBound = false;
	this.m_bViewAttached = false;
	this.m_oFrame = null;
	this.m_pLifecycleListeners = [];

	var oPresentationModel;
	if (typeof(vPresentationModel) == "string") {
		this.m_sPresentationModel = vPresentationModel;
		var fPresentationModel = Utility.locate(this.m_sPresentationModel);
		oPresentationModel = new fPresentationModel();
	} else {
		oPresentationModel = vPresentationModel;
	}

	if (!(oPresentationModel instanceof br.presenter.PresentationModel)) {
		throw new Errors.InvalidParametersError("Presentation Model passed to PresenterComponent is not a br.presenter.PresentationModel");
	}
	this.m_oPresentationModel = oPresentationModel;

	PresenterComponent._initializePlugins();

	this.m_oPresentationModel._$setPath(this);
};

br.Core.implement(PresenterComponent, br.component.Component);
br.Core.implement(PresenterComponent, br.component.Serializable);

/**
 * @private
 */
PresenterComponent.TEMPLATE_NOT_FOUND = "TemplateNotFound";
PresenterComponent.TemplateNotFoundError = function(message, filename, lineNumber) {
	Errors.CustomError.call(this, PresenterComponent.TEMPLATE_NOT_FOUND, message, filename, lineNumber);
};
br.Core.extend(PresenterComponent.TemplateNotFoundError, Errors.CustomError);

/**
 * @private
 */
PresenterComponent._initializePlugins = function() {
	if (!presenter_knockout.bindingHandlers.control) {
		presenter_knockout.bindingHandlers.control = new br.presenter.view.knockout.ControlPlugin();
		presenter_knockout.bindingHandlers.tooltip = new br.presenter.view.knockout.TooltipPlugin();
	}
};

/**
 * Retrieve the presentation model being displayed by this component.
 *
 * @type br.presenter.PresentationModel
 */
PresenterComponent.prototype.getPresentationModel = function() {
	return this.m_oPresentationModel;
};

/**
 * Returns <code>true</code> once {@link br/component/Frame#onOpen} has
 * fired, and the display element has been attached to the page.
 *
 * @type boolean
 */
PresenterComponent.prototype.isViewAttached = function() {
	return this.m_bViewAttached;
};

// *********************** Component Interface ***********************

PresenterComponent.prototype.setDisplayFrame = function(frame) {
	this.m_oFrame = frame;

	this.m_oPresentationModel.setComponentFrame(frame);

	function getEventHandler(event) {
		var handlerName = "on"+event.charAt(0).toUpperCase()+event.substring(1);
		if (this[handlerName]) {
			return function() {
				this[handlerName].apply(this, arguments);
			}
		}
		return function() {
			this._propagateComponentEvent(handlerName, arguments);
		};
	}

	br.component.Frame.EVENTS.forEach(function(event) {
		frame.on(event, getEventHandler.call(this, event), this);
	}, this);

	frame.setContent(this.getElement());

};

PresenterComponent.prototype.getElement = function() {
	if (!this.m_bViewBound) {
		this.m_bViewBound = true;
		presenter_knockout.applyBindings(this.m_oPresentationModel, this.m_eTemplate);
	}

	return this.m_eTemplate;
};

// It is the responsibility of the containing system to call serialize and then persist the resultant string.
// It can then call deserialize with that string to (re)create an instance of the component.
// We write the component class into the serialized form so this component can throw an exception
// if an attempt is made to deserialize it with a string that was not created by this class.
// This class identifier should NOT be used by the containing system to map its serialized blobs to component type as
// it is a private concern of this class and liable to change.
PresenterComponent.prototype.serialize = function() {
	var sSerializedState = "";

	if (!this.m_sPresentationModel) {
		this.m_sPresentationModel = this.m_oPresentationModel.getClassName();
	}

	if (br.Core.fulfills(this.m_oPresentationModel, br.presenter.SerializablePresentationModel)) {
		sSerializedState = this.m_oPresentationModel.serialize();
	}

	var sSerializedString = '<br.presenter.component.PresenterComponent templateId="' + this.m_sTemplateId + '" presentationModel="' + this.m_sPresentationModel + '">'
							+ sSerializedState +
							'</br.presenter.component.PresenterComponent>';

	return sSerializedString;
};

/**
 * Extracts the data inside the presenter tag and gives it to the PresentationModel for deserialization.
 * Only has affect if the Presentation Model implements {@link module:br/presenter/SerializablePresentationModel}.
 *
 * @param {String} sPresenterData The presenter xml node in string format
 */
PresenterComponent.prototype.deserialize = function(sPresenterData) {
	if (br.Core.fulfills(this.m_oPresentationModel, br.presenter.SerializablePresentationModel)) {
		var vOffsetPresenterOpeningTag = sPresenterData.indexOf('>');
		var vOffsetPresenterClosingTag = sPresenterData.indexOf('</br.presenter.component.PresenterComponent>');

		//If it doesn't have a closing presenter tag means it has no serialized data
		var sPresenterTagData = (vOffsetPresenterClosingTag !== -1) ? sPresenterData.substring(vOffsetPresenterOpeningTag+1, vOffsetPresenterClosingTag) : "";
		this.m_oPresentationModel.deserialize(sPresenterTagData);
	}
};

PresenterComponent.deserialize = function(sXml)
{
	var oPresenterNode = br.util.XmlParser.parse( sXml );
	var sPresenterNodeName = oPresenterNode.nodeName;

	if(sPresenterNodeName !== "br.presenter.component.PresenterComponent" ) {
		var sErrorMsg = "Nodename for Presenter Configuration XML must be 'br.presenter.component.PresenterComponent', but was:" + sPresenterNodeName;

		throw new Errors.InvalidParametersError(sErrorMsg);
	}

	var sTemplateId = oPresenterNode.getAttribute("templateId");
	var sPresentationModel = oPresenterNode.getAttribute("presentationModel");

	var oPresenterComponent = new br.presenter.component.PresenterComponent(sTemplateId, sPresentationModel);
	oPresenterComponent.deserialize(sXml);

	return oPresenterComponent;
};

/**
 * When invoked, the onOpen component life cycle event is propagated to the
 * <code>PresentationModel</code>.
 *
 * @private
 */
PresenterComponent.prototype.onAttach = function() {
	if (this.m_bViewAttached === true) {
		return;
	}
	this.m_bViewAttached = true;
	this._propagateComponentEvent("onOpen", [this.m_oFrame.width, this.m_oFrame.height]);
};

/**
 * When invoked, the onClose component life cycle event is propagated to the
 * <code>PresentationModel</code>.
 *
 * @private
 * @see br/component/Component#onClose
 */
PresenterComponent.prototype.onClose = function() {
	this._propagateComponentEvent("onClose", arguments);
	presenter_knockout.cleanNode(this.m_eTemplate);
	this.m_oPresentationModel.removeChildListeners();
	this._nullObject(this.m_oPresentationModel);
	this.m_oPresentationModel = null;
	this.m_oComponentFrame = null;
	this.m_eTemplate = null;
	if (this.m_oFrame) {
		this.m_oFrame.clearListeners(this);
	}
};

/**
 * When invoked, the onResize component life cycle event is propagated to the
 * <code>PresentationModel</code>.
 *
 * @private
 * @param {int} nWidth
 * @param {int} nHeight
 * @see br/component/Component#onResize
 */
PresenterComponent.prototype.onResize = function() {
	this._propagateComponentEvent("onResize", [this.m_oFrame.width, this.m_oFrame.height]);
};

/**
 * When invoked, the onActivate component life cycle event is propagated to the
 * <code>PresentationModel</code>.
 *
 * @private
 * @see br/component/Component#onActivate
 */
PresenterComponent.prototype.onFocus = function() {
	this._propagateComponentEvent("onActivate", arguments);
};

/**
 * When invoked, the onDeactivate component life cycle event is propagated to the
 * <code>PresentationModel</code>.
 *
 * @private
 * @param {int} nWidth
 * @param {int} nHeight
 * @see br/component/Component#onDeactivate
 */
PresenterComponent.prototype.onBlur = function(nWidth, nHeight) {
	this._propagateComponentEvent("onDeactivate", arguments);
};

// *********************** Private Methods ***********************

/**
 * @private
 * @param {Object} oObjectToBeCleaned
 */
PresenterComponent.prototype._nullObject = function(oObjectToBeCleaned) {
	for(var sChildToBeCleaned in oObjectToBeCleaned) {
		var oChildToBeCleaned = oObjectToBeCleaned[sChildToBeCleaned];

		if (typeof oChildToBeCleaned === "object" && oChildToBeCleaned !== null) {
			oObjectToBeCleaned[sChildToBeCleaned] = null;

			if (oChildToBeCleaned instanceof br.presenter.node.PresentationNode) {
				this._nullObject(oChildToBeCleaned);
			}
		}
	}
};

/**
 * @private
 * @param {String} sEvent
 * @param {Array} pArguments
 */
PresenterComponent.prototype._propagateComponentEvent = function(sEvent, pArguments) {
	if (this.m_oPresentationModel[sEvent]) {
		this.m_oPresentationModel[sEvent].apply(this.m_oPresentationModel, pArguments);
	}
	this.m_pLifecycleListeners.forEach(function(listener){
		if (listener[sEvent]) {
			listener[sEvent].apply(listener, pArguments);
		}
	});
};

PresenterComponent.prototype.addLifeCycleListener = function(listener) {
	this.m_pLifecycleListeners.push(listener);
};

PresenterComponent.prototype.removeLifeCycleListener = function(listener) {
	var index = this.m_pLifecycleListeners.indexOf(listener);
	if (index >= 0) {
		this.m_pLifecycleListeners.splice(index, 1);
	}
}

/**
 * @private
 * @param {String} sTemplateId
 * @type Element
 */
PresenterComponent.prototype._getTemplate = function(sTemplateId) {
	var eTemplateNode = ServiceRegistry.getService("br.html-service").getTemplateElement(sTemplateId);

	if(!eTemplateNode) {
		throw new PresenterComponent.TemplateNotFoundError("Template with ID '" + sTemplateId + "' couldn't be found");
	}

	return eTemplateNode;
};

module.exports = PresenterComponent;

// TODO: delete this line once the package is CommonJs
br.presenter.component.PresenterComponent = module.exports;