Source: manager/presentationManager.js


define([ 'mmirf/controllerManager', 'mmirf/commonUtils', 'mmirf/viewLoader', 'mmirf/logger'
		, 'mmirf/util/deferredWithState', 'mmirf/core', 'module', 'require'
	],

	/**
	 * @class
	 * @name mmir.PresentationManager
	 * @static
	 * @hideconstructor
	 *
	 * @requires dialogManager if reRenderView() or renderPreviousView() are used
	 *
	 */
	function ( controllerManager, commonUtils, viewLoader, Logger
			, deferred, core, module, require
) {

	//the next comment enables JSDoc2 to map all functions etc. to the correct class description
	/** @scope mmir.PresentationManager.prototype */

	/**
	 * Counter that keeps track of the number of times, that a view is rendered
	 *
	 * NOTE: for implementation specific reasons, jQuery Mobile requires that
	 * 		 each page has a different ID. This pageIndex is used to generating
	 * 		 such a unique ID, by increasing the number on each page-change
	 * 		 (i.e. by rendering a view) and appending it to the page's ID/name.
	 *
	 * @type Integer
	 * @public
	 * @memberOf mmir.PresentationManager#
	 */
	var _pageIndex = 0;

	/**
	 * Name for the default layout.
	 *
	 * <p>
	 * There must exist a layout definition by
	 * this name, i.e.
	 * <pre>views/layout/<DEFAULT_LAYOUT_NAME>.ehtml</pre>
	 *
	 * NOTE: while the name begins with an upper case
	 *       letter, the file name for the layout must
	 *       start with a lower case letter, e.g. for
	 *       name <code>Default</code>, the file name
	 *       must be <code>default.ehtml</code>.
	 *
	 * @type String
	 * @private
	 * @constant
	 * @memberOf mmir.PresentationManager#
	 *
	 * @example var defaultLayoutName = mmir.conf.get(mmir.presentation.CONFIG_DEFAULT_LAYOUT);
	 */
	var DEFAULT_LAYOUT_NAME = 'Default';

	/**
	 * Name of the configuration property that specifies a custom name for the default layout.
	 *
	 * <p>
	 * NOTE: if FALSY (other than <code>undefined</code>) no default layout will be loaded.
	 *       Rendering views may fail, if they rely on a {@link mmir.view.Layout}!
	 *
	 * @type String
	 * @private
	 * @constant
	 * @memberOf mmir.PresentationManager#
	 *
	 * @example var defaultLayout = mmir.conf.get(mmir.presentation.CONFIG_DEFAULT_LAYOUT_NAME);
	 *
	 */
	var CONFIG_DEFAULT_LAYOUT_NAME = 'defaultLayoutName';//TODO move this to somewhere else (collected config-vars?)? this should be a public CONSTANT...

	// private members
	/**
	 * The logger for the PresentationManager.
	 *
	 * @private
	 * @type mmir.tools.Logger
	 * @memberOf mmir.PresentationManager#
	 */
	var logger = Logger.create(module);//initialize with requirejs-module information

	/**
	 * Array of layouts of the application
	 *
	 * @type Map
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var _layouts = new Map();

	/**
	 * Array of all the views of the application
	 *
	 * @type Map
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var _views = new Map();

	/**
	 * Array of all the partials of the application
	 *
	 * @type Map
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var _partials = new Map();

	/**
	 * The currently displayed dialog object, if a dialog is displayed. Used
	 * mainly to close the dialog.
	 *
	 * @type Object
	 * @private
	 * @memberOf mmir.PresentationManager#
	 *
	 * @see mmir.PresentationManager#showDialog
	 * @see mmir.PresentationManager#hideCurrentDialog
	 */
	var _currentDialog = null;

	/**
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var viewSeparator 		= '#';

	/**
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var reHandlerName 		= /^on_/;
	/**
	 * @type String
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var partialSeparator 	= commonUtils.getPartialsPrefix();
	/**
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	function createLookupKey(ctrl, viewObj, separator){
		if(typeof ctrl.getName !== 'undefined'){
			ctrl = ctrl.getName();
		}
		if(typeof viewObj.getName !== 'undefined'){
			viewObj = viewObj.getName();
		}
		//TODO remove all >partialSeparator< from partial-string beginning
		return ctrl+separator+viewObj;
	}
	/**
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	function createViewKey(ctrl, view){
		return createLookupKey(ctrl, view, viewSeparator);
	}
	/**
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	function createPartialKey(ctrl, partial){
		return createLookupKey(ctrl, partial, partialSeparator);
	}


	/**
	 * Default implementation for the rendering-engine:
	 *
	 * does nothing but writing an error message to the console,
	 * if any of its functions is invoked.
	 *
	 * The rendering engine can be set via {@link mmir.PresentationManager#setRenderEngine}.
	 *
	 * @type RenderEngine
	 * @private
	 * @memberOf mmir.PresentationManager#
	 */
	var _renderEngine = {
			/**
			 * The function that actually renders the View.<br>
			 *
			 * The function will be invoked in context of the PresentationManager instance
			 * (i.e. the manager will be the <em>this</em> context).
			 *
			 * Implementations of this function should adhere to the following procedure:
			 *
			 * <br>
			 *
			 * First this function fetches the <em>layout</em> for the <em>controller</em>
			 * (or uses the <code>dialogManager.DEFAULT_LAYOUT<code>).
			 *
			 * Then the <code>before_page_prepare</code> of the <em>controller</em> is invoked (if it exists).
			 *
			 * Then renders the <em>view</em> into the
			 * layout-template; <em>partials</em>, <em>helpers</em> etc.
			 * that are referenced in the <em>view</em> will be processed,
			 * executed etc.; during this, localized Strings should be processed and rendered by
			 * {@link mmir.LanguageManager#getText}.
			 *
			 * Then <em>dialogs</em> are created and the <code>dialogManager.pageIndex</code> is updated.
			 *
			 * The new content is inserted into the document/page (invisibly).
			 *
			 * Then the <code>before_page_load</code> of the <em>controller</em> is invoked (if it exists).
			 *
			 * The new content/page is made visible, and the old one invisible and / or is removed.
			 *
			 * At the end the <b>on_page_load</b> action of the <em>controller</em> is performed.
			 *
			 * @function
			 * @memberOf mmir.PresentationManager._renderingEngine
			 *
			 * @param {String}
			 *            ctrlName Name of the controller
			 * @param {String}
			 *            viewName Name of the view to render
			 * @param {mmir.view.View}
			 *            view View object that is to be rendered
			 * @param {mmir.ctrl.Controller}
			 *            ctrl Controller object of the view to render
			 * @param {Object}
			 *            [data] optional data for the view.
			 * @returns {void|Promise}
			 * 			if void/undefined is returned, the view is rendered synchronously, i.e.
			 * 			the view is rendered, when this method returns.
			 * 			If a Promise is returned, the view is rendered asynchronously
			 * 			(rendering is finished, when the promise is resolved)
			 */
			render: function(ctrlName, viewName, view, ctrl, data){
				logger.error('PresentationManager.render: no rendering engine set!');
			},
			showDialog: function(ctrlName, dialogId, data) {
				logger.error('PresentationManager.showDialog: no rendering engine set!');
			},
			hideCurrentDialog: function(){
				logger.error('PresentationManager.hideCurrentDialog: no rendering engine set!');
			},
			showWaitDialog: function(text, data) {
				logger.error('PresentationManager.showWaitDialog: no rendering engine set!');
			},
			hideWaitDialog: function() {
				logger.error('PresentationManager.hideWaitDialog: no rendering engine set!');
			}
	};
	/**
	 * Reference to the rendering-engine implementation / instance.
	 *
	 * This reference should not be accessed directly.
	 * Custom functions of the rendering implementation can be
	 * invoked via {@link mmir.PresentationManager#callRenderEngine}.
	 *
	 * @type Object
	 * @private
	 */
	_renderEngine._engine = _renderEngine;

	var _instance = {
			/** @scope mmir.PresentationManager.prototype  */

			// public members
			/**
			 * @param {mmir.view.Layout} layout
			 * 			the layout to add
			 * @memberOf mmir.PresentationManager.prototype
			 */
			addLayout : function(layout) {
				_layouts.set(layout.getName(), layout);
			},
			/**
			 * This function returns a layout object by name.<br>
			 *
			 * @function
			 * @param {String}
			 *            layoutName Name of the layout which should be returned
			 * @param {Boolean}
			 *            [doUseDefaultIfMissing] if supplied and
			 *            <code>true</code>, the default controller's layout
			 *            will be used as a fallback, in case no corresponding
			 *            layout could be found
			 * @returns {mmir.view.Layout} The requested layout, "false" if not found
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			getLayout : function(layoutName, doUseDefaultIfMissing) {
				var layout = false;
				layout = _layouts.get(layoutName);
				if (!layout) {
					if (doUseDefaultIfMissing) {
						layout = _instance.getLayout(DEFAULT_LAYOUT_NAME, false);
					}
					else {
						logger.error('[PresentationManager.getLayout]: could not find layout "' + layoutName +'"')
						return false;
					}
				}
				return layout;
			},

			/**
			 *
			 * @param {String|Controller} ctrlName
			 * @param {String|mmir.view.View} view
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			addView : function(ctrlName, view) {
				_views.set(createViewKey(ctrlName, view), view);
			},
			/**
			 * This function returns a view object by name.<br>
			 *
			 * @function
			 * @param {String}
			 *            controllerName Name of the controller for the view
			 * @param {String}
			 *            viewName Name of the view which should be returned
			 * @returns {mmir.view.View} The requested view, <tt>false</tt> if not
			 *          found
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			getView : function(controllerName, viewName) {
				viewName = createViewKey(controllerName, viewName);
				var view = false;
				view = _views.get(viewName);

				if (!view) {
					logger.error('[PresentationManager.getView]: could not find view "' + viewName + '"');
					return false;
				}
				return view;
			},
			/**
			 *
			 * @param {String|Controller} ctrlName
			 * @param {String|mmir.view.Partial} partial
			 *
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			addPartial: function(ctrlName, partial){
				_partials.set(createPartialKey(ctrlName, partial), partial);
			},

			/**
			 * This function returns a partial object by name.<br>
			 *
			 * @function
			 * @param {String}
			 *            controllerName Name of the controller for the view
			 * @param {String}
			 *            viewName Name of the partial which should be returned
			 * @returns {mmir.view.Partial} The requested partial, "false" if not found
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			getPartial : function(controllerName, partialName) {
				var partial = false;

				var partialKey = null;
				if (controllerName) {
					partialKey = createPartialKey(controllerName, partialName);
				}
				else {
					logger.error('[PresentationManager.getPartial]: requested partial "' + partialName + '" for unknown controller: "' + (controllerName ? (controllerName.getName? controllerName.getName(): controllerName) : 'undefined')
							+ '"');
					return false;
				}

				partial = _partials.get(partialKey);
				if (!partial) {
					logger.error('[PresentationManager.getPartial]: could not find partial "' + partialName + '" for controller "' + (controllerName ? (controllerName.getName? controllerName.getName(): controllerName) : 'undefined') + '"!');
					return false;
				}
				return partial;
			},

			/**
			 * Closes a modal window / dialog (if one is open).
			 * <br>
			 *
			 * @function
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			hideCurrentDialog : function() {
				_renderEngine.hideCurrentDialog.apply(this,arguments);
			},

			/**
			 * Opens the dialog for ID <code>dialogId</code>.
			 * <br>
			 *
			 * @function
			 * @param {String} ctrlName
			 *            the Name of the controller
			 * @param {String} dialogId
			 *            the ID of the dialog
			 * @param {Object} [data] OPTIONAL
			 *            a data / options object
			 *
			 * @returns {Object} the instance of the opened dialog (void or falsy if dialog was not opened)
			 *
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			showDialog : function(ctrlName, dialogId, data) {
				_currentDialog = _renderEngine.showDialog.apply(this,arguments);
				return _currentDialog;
			},

			/**
			 * Shows a "wait" dialog, i.e. "work in progress" notification.
			 *
			 * @function
			 *
			 * @param {String} [text] OPTIONAL
			 * 				the text that should be displayed.
			 * 				If omitted the language setting for <code>loadingText</code>
			 * 				will be used instead (from dictionary.json)
			 * @param {Object} [data] OPTIONAL
			 * 				a data / options object
			 *
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 *
			 * @see mmir.PresentationManager#hideWaitDialog
			 */
			showWaitDialog : function(text, data) {
				_renderEngine.showWaitDialog.apply(this,arguments);
			},

			/**
			 * Hides / closes the "wait" dialog.
			 *
			 * @function
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 *
			 * @see mmir.PresentationManager#showWaitDialog
			 */
			hideWaitDialog : function() {
				_renderEngine.hideWaitDialog.apply(this,arguments);
			},

			/**
			 * Gets the view for a controller, then executes helper methods on
			 * the view data. The Rendering of the view is done by the
			 * {@link #doRenderView} method. Also
			 * stores the previous and current view with parameters.<br>
			 *
			 * @function
			 * @param {String}
			 *            ctrlName Name of the controller
			 * @param {String}
			 *            viewName Name of the view to render
			 * @param {Object}
			 *            [data] optional data for the view.
			 * @returns {void|Promise}
			 * 			if void/undefined is returned, the view is rendered synchronously, i.e.
			 * 			the view is rendered, when this method returns.
			 * 			If a Promise is returned, the view is rendered asynchronously
			 * 			(rendering is finished, when the promise is resolved)
			 *
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			render : function(ctrlName, viewName, data) {

				var ctrl = controllerManager.get(ctrlName);
				var renderResult;

				if (ctrl != null) {

					var view = this.getView(ctrlName, viewName);

					if(!view){
						logger.error('PresentationManager.renderView: could not find view "'+viewName+'" for controller "'+ctrlName+'"');
						return;
					}

					renderResult = _renderEngine.render.call(this, ctrlName, viewName, view, ctrl, data);

				}
				else {
					logger.error('PresentationManager.renderView: could not retrieve controller "'+ctrlName+'"');
				}

				return renderResult;
			},
			/**
			 * Helper for emitting pre-/post-render events on controller instance and/or
			 * PresentationManager instance.
			 *
			 * @function
			 *
			 * @param {mmir.ctrl.Controller}
			 *            ctrl the controller instance for which the render event should be emitted
			 * @param {String}
			 *            eventName Name of the event: "page_load", ...
			 * @param {Object}
			 *            [eventData] OPTIONAL the rendering data
			 * @param {Object}
			 *            [pageData] OPTIONAL the page data/rendering options (may not be present for all events)
			 * @param {Object}
			 *            [data] optional data for the view.
			 * @returns {any | false}
			 * 	        	if false returned by an event handler, cancalebale rendering actions will stopped
			 */
			_fireRenderEvent: function(ctrl, eventName, eventData, pageData){

				var isContinue = ctrl.performIfPresent(eventName, eventData, pageData);

				//if "global" event handler is set:
				var evtHandlerName = reHandlerName.test(eventName)? eventName : 'on_' + eventName;
				if(typeof this[evtHandlerName] === 'function'){
					//if global event handler cancels rendering (note: may not be possible for all type of events)
					if(this[evtHandlerName](ctrl.getName(), eventName, eventData, pageData) === false){
						isContinue = false;
					}
				}

				return isContinue;
			},

			/**
			 * @function
			 * @async
			 * @memberOf mmir.PresentationManager.prototype
			 * @returns {Promise}
			 * 					a deferred promise that gets fulfilled when initialization is completed.
			 */
			init: function(){

				var defer = deferred();

				var isViewEngineLoaded = false;//MOD modularize view-engine
				var isViewsLoaded = false;//MOD modularize view-loading & -compiling

				var checkResolved = function(){
					if(isViewEngineLoaded && isViewsLoaded){
						defer.resolve(_instance);
					}
				};

				var failPromise = function(msg){
					defer.reject(msg);
				};

				//MOD modularize view-engine: load viewEngine (default uses standard HTML document API)
				require([typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD? 'mmirf/simpleViewEngine' : core.viewEngine], function(viewEngineInit){//FIXME
					viewEngineInit.then(
						function(viewEngine){
							_instance.setRenderEngine(viewEngine);
							isViewEngineLoaded = true;
							checkResolved();
						}, failPromise
					);
				});

				viewLoader(
					_instance, _layouts, _views, _partials, createViewKey, createPartialKey
				).then(function(){
					isViewsLoaded = true;
					checkResolved();
				}, failPromise);

				return defer;
			},// init,

			/**
			 * Sets the <em>rendering engine</em> for the views.
			 *
			 * The render engine <b>must</b> implement a function <em>render</em>
			 * and <i>may</i> implement functions <em>showDialog</em>,
			 * <em>hideCurrentDialog</em>, <em>showWaitDialog</em>, and <em>hideWaitDialog</em>:
			 *
			 * <ul>
			 * 	<li><b>theRenderEngine.<code>render(ctrlName : String, viewName : String, view : View, ctrl : Controller, data : Object) : void|Promise</code></b></li>
			 * 	<li>theRenderEngine.<code>showDialog(ctrlName : String, dialogId : String, data : Object) : Dialog</code></li>
			 * 	<li>theRenderEngine.<code>hideCurrentDialog(): void</code></li>
			 * 	<li>theRenderEngine.<code>showWaitDialog(text : String, data : Object): void</code></li>
			 * 	<li>theRenderEngine.<code>hideWaitDialog(): void</code></li>
			 * </ul>
			 *
			 * The functions of <code>theRenderEngine</code> will be called in
			 * context of the PresentationManager.
			 *
			 * Custom functions of the specific rendering engine implementation
			 * (i.e. non-standard functions) can be call via {@link #callRenderEngine}.
			 *
			 *
			 * <br>
			 * By default, the rendering-engine as defined by the module ID/path in
			 * <code>core.viewEngine</code> will be loaded and set during initialization
			 * of the DialogManager.
			 *
			 * <br>
			 * The implementation of the default view-engine is at
			 * <code>mmirf/env/view/presentation/simpleViewEngine.js</code>.
			 *
			 * @param {Object} theRenderEngine
			 * 			the render-engine for views
			 *
			 * @function
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 *
			 * @see mmir.PresentationManager#renderView
			 * @see mmir.PresentationManager#showDialog
			 * @see mmir.PresentationManager#hideCurrentDialog
			 * @see mmir.PresentationManager#showWaitDialog
			 * @see mmir.PresentationManager#hideWaitDialog
			 *
			 * @see mmir.PresentationManager#callRenderEngine
			 *
			 */
			setRenderEngine: function(theRenderEngine){
				_renderEngine.render 			= theRenderEngine.render;
				_renderEngine.showDialog		= theRenderEngine.showDialog;
				_renderEngine.hideCurrentDialog	= theRenderEngine.hideCurrentDialog;
				_renderEngine.showWaitDialog	= theRenderEngine.showWaitDialog;
				_renderEngine.hideWaitDialog	= theRenderEngine.hideWaitDialog;
				_renderEngine._engine			= theRenderEngine;
			},
			/**
			 * This function allows to call custom functions of the rendering-engine
			 * that was set via {@link #setRenderEngine}.
			 *
			 * IMPORTANT:
			 * note that the function will be invoked in context of rendering-engine
			 * (i.e. <code>this</code> references will refer to rendering-engine
			 * and not to the PresentationManager instance.
			 * For example, when <code>mmir.PresentationManager.callRenderEngine('hideWaitDialog')</code>
			 * is called, any <code>this</code> references within the <code>hideWaitDialog</code>
			 * implementation would refer to object, that was set in <code>setRenderEngine(object)</code>.
			 * In comparison, when called as <code>mmir.PresentationManager.hideWaitDialog()</code> the
			 * <code>this</code> references refer to the mmir.PresentationManager instance.
			 *
			 * <br>
			 * NOTE that calling non-existing functions on the rendering-engine
			 *      will cause an error.
			 *
			 * @param {String} funcName
			 * 			the name of the function, that should be invoked on the rendering
			 * 			engine.
			 * @param {Array<any>} [args] OPTIONAL
			 * 			the arguments for <code>funcName</code> invoked
			 *          via <code>Function.apply(renderingEngine, args)</code>, e.g.
			 * 			for <code>args = [param1, param2, param3]</code> the
			 * 			function will be called with
			 * 			<code>funcName(param1, param2, param3)</code>
			 * 			(note that the function receives 3 arguments, and
			 *           not 1 Array-argument).
			 *
			 * @function
			 * @public
			 * @memberOf mmir.PresentationManager.prototype
			 */
			callRenderEngine: function(funcName, args){
				return _renderEngine._engine[funcName].apply(_renderEngine._engine, args);
			},

			//exported properties / constants:
			/**
			 * @public
			 * @type Integer
			 * @constant
			 * @memberOf mmir.PresentationManager.prototype
			 */
			pageIndex:		_pageIndex
	};//END:  _instance = {...

	//export constants:
	_instance.DEFAULT_LAYOUT_NAME = DEFAULT_LAYOUT_NAME;
	_instance.CONFIG_DEFAULT_LAYOUT_NAME = CONFIG_DEFAULT_LAYOUT_NAME;


	return _instance;
});

//});