1 
  2 
  3 define(['jquery', 'loadCss'],
  4 /**
  5  * View engine that uses jQuery Mobile for loading the views as new jQM pages.
  6  * 
  7  * <p>
  8  * 
  9  * The render-functions supports the jQM page-transitions.
 10  * The default (jQuery Mobile) transition is <code>none</code>.
 11  * 
 12  * <p>
 13  * 
 14  * NOTE: loading the module will in effect load all jQuery Mobile functionality
 15  *       (and its side effects, such as auto-enhancement of HTML elements, e.g. for input/text)
 16  * 
 17  * <h3>Side Effects</h3>
 18  * <ul>
 19  * 	<li>loads the jQuery Mobile CSS file</li>
 20  * 	<li>loads the RequireJS module "jqm" (i.e.: jQuery Mobile)</li>
 21  * 	<li>loads the RequireJS module "jqmSimpleModal" (i.e.: jQuery Mobile plugin SimpleModal)</li>
 22  * </ul>
 23  * 
 24  * <p>
 25  * 
 26  * <h3>Replacing the Default ViewEngine</h3>
 27  * 
 28  * This render engine can be replaced by alternative rendering engines, by
 29  * 
 30  * <ul>
 31  * 	<li>implementing the interface for rendering engine: the requirejs module should export
 32  * 		an object with the public functions as described by {@link mmir.PresentationManager#_renderEngine}.</li>
 33  * 	<li>register the engine with its module ID and configure MMIR to use it:
 34  * 		<ul>
 35  * 			<li>register the new rendering-engine file:<br>
 36  * 				<code>mmir.config({paths: {'module ID': 'path/to/file/name'}})</code>
 37  * 			</li>
 38  * 			<li>configure MMIR to use it by setting the new module ID to {@link mmir.viewEngine}:<br>
 39  * 				<code>mmir.viewEngine = 'module ID';</code>
 40  * 			</li>
 41  * 			<li>NOTE: the call to <code>mmir.config</code> and setting the module ID to <code>viewEngine</code>
 42  * 				must happen <em>after</em> the <code>mmirf/core.js</code>
 43  * 				file is loaded, but <em>before</em> <code>mmirf/vendor/libs/require.js</code>
 44  * 				is loaded! (see <code>index.html</code>)
 45  * 			</li>
 46  * 			<li>NOTE: the path to the app's root directory is <code>../</code></li>
 47  * 			<li>NOTE: the <code>file name</code> must be <strong>without</strong> the file extension</li>
 48  * 		</ul>
 49  *  </li>
 50  * </ul>
 51  * 
 52  * 
 53  * @example
 54  * //use page-transition with effect 'slide' (animated as not-reversed motion)
 55  * mmir.DialogManager.render('theController', 'theView', {transition: 'slide', reverse: false});
 56  * 
 57  * 
 58  * 
 59  * @class
 60  * @name JqmViewEngine
 61  * @memberOf mmir.env.view
 62  * @static 
 63  *  
 64  * Libraries:
 65  *  - jQuery (>= v1.6.2)
 66  *  - jQuery Mobile (jQuery plugin, >= 1.2.0); $.mobile
 67  *  - SimpleModal (jQuery plugin, >= v1.4.2); $.modal
 68  *  
 69  *  @requires document (DOM object)
 70  *  
 71  *  @requires jQuery.Deferred
 72  *  
 73  *  @requires jQuery.parseHTML
 74  *  @requires jQuery.appendTo
 75  *  @requires jQuery#selector
 76  *  
 77  *  @requires jQueryMobile.defaultPageTransition
 78  *  @requires jQueryMobile.pageContainer
 79  *  @requires jQueryMobile.loading
 80  *  @requires jQueryMobile.pageContainer
 81  *  
 82  *  @requires jQuerySimpleModalDialog
 83  *  
 84  *  @see mmir.PresentationManager#setRenderEngine
 85  *  @see mmir.PresentationManager#callRenderEngine
 86  *  @see mmir.viewEngine
 87  */
 88 function(jquery, loadCss){
 89 
 90 	//load CSS for jQuery Mobile:
 91 	loadCss('mmirf/vendor/styles/jquery.mobile-1.4.3.min.css');
 92 	
 93 	/**
 94 	 * Deferred object that will be returned; for async-initialization:
 95 	 * the deferred object will be resolved, when this module has been initialized.
 96 	 * 
 97 	 * @private
 98 	 * @type Deferred
 99 	 * @memberOf JqmViewEngine#
100 	 */
101 	var promise = jquery.Deferred();
102 	
103 	require(['jquery', 'renderUtils', 'languageManager', 'controllerManager',
104 	         'jqm','jqmSimpleModal'],
105 	    function(jq, renderUtils, languageManager, controllerManager
106 	){
107 
108 		/**
109 		 * List of elements (jQuery objects) that should be remove from DOM
110 		 * after a page has loaded (loaded: after all contents inserted into the
111 		 * DOM and after all page transitions have been executed).
112 		 * 
113 		 * @private
114 		 * @type Array<jQueryObject>
115 		 * @memberOf JqmViewEngine#
116 		 */
117 		var afterViewLoadRemoveList = [];
118 
119 		/**
120 		 * The ID attribute for the content / page-elements.
121 		 * 
122 		 * <p>
123 		 * This is jQuery Mobile specific:
124 		 * pages are contained in an element with <code>data-role="page"</code>.
125 		 * 
126 		 * These elements must have an ID attribute with the value of this constant
127 		 * (the actual value will be created and set on rendering the view / layout).
128 		 * 
129 		 * @type String
130 		 * @public
131 		 * @constant
132 		 * @memberOf JqmViewEngine#
133 		 */
134 		var CONTENT_ID = "pageContainer";
135 
136 		//property names for passing the respected objects from doRenderView() to doRemoveElementsAfterViewLoad()
137 		/**
138 		 * Property name for passing the respected objects from
139 		 * {@link #doRenderView} to {@link #doRemoveElementsAfterViewLoad}:
140 		 * 
141 		 * Internal ID for field name that holds the {@link View}.
142 		 * 
143 		 * @type String
144 		 * @private
145 		 * @constant
146 		 * @memberOf JqmViewEngine#
147 		 */
148 		var FIELD_NAME_VIEW 		 = '__view';
149 		/**
150 		 * Property name for passing the respected objects from
151 		 * {@link #doRenderView} to {@link #doRemoveElementsAfterViewLoad}:
152 		 * 
153 		 * Internal ID for field name that holds the rendering data object.
154 		 * 
155 		 * @type String
156 		 * @private
157 		 * @constant
158 		 * @memberOf JqmViewEngine#
159 		 * 
160 		 * @see mmir.PresentationManager#render
161 		 * @see mmir.DialogManager#render
162 		 */
163 		var FIELD_NAME_DATA 		 = '__renderData';
164 		/**
165 		 * Property name for passing the respected objects from
166 		 * {@link #doRenderView} to {@link #doRemoveElementsAfterViewLoad}:
167 		 * 
168 		 * Internal ID for field name that holds the {@link Controller}.
169 		 * 
170 		 * @type String
171 		 * @private
172 		 * @constant
173 		 * @memberOf JqmViewEngine#
174 		 */
175 		var FIELD_NAME_CONTROLLER 	 = '__ctrl';
176 
177 		//
178 		/**
179 		 * Function for removing "old" content from DOM (-> remove old, un-used page content).
180 		 * 
181 		 * This function is registered to jQuery Mobile's onpagechange event and will be executed
182 		 * after each page-change (i.e. render-call).
183 		 * 
184 		 * This function
185 		 * <ul>
186 		 * 	<li>calls <code>on_page_load</code> on the view's controller</li>
187 		 * 	<li>calls <code>on_page_load<VIEW NAME></code> on the view's controller (if it exists)</li>
188 		 * 	<li>removes the DOM content of the previous view from the document</li>
189 		 * <ul>
190 		 * 
191 		 * @param {Event} event
192 		 * 				the event triggered by a (jQuery Mobile) page-change
193 		 * @data {PlainObject} data
194 		 * 				the data object. The data object holds the property
195 		 * 				<code>data.options</code> that is set by {@link doRenderView}
196 		 * 				when triggering the page change.
197 		 * 				this options object holds 3 properties:
198 		 * 				<pre>{
199 		 * 					FIELD_NAME_CONTROLLER: Controller,  //the Controller of the View which is rendered (if NULL, an error will be printed to the console!)
200 		 * 					FIELD_NAME_VIEW:       View,        //View which is rendered
201 		 * 					FIELD_NAME_DATA:       Object,      //the data object with which render() was invoked
202 		 * 				}</pre>
203 		 *  
204 		 * @function
205 		 * @private
206 		 * @memberOf JqmViewEngine#
207 		 */
208 		var doRemoveElementsAfterViewLoad = function(event, data){
209 			//data.toPage: {String|Object} page to which view was changed
210 			//data.options: the configuration for the page change
211 
212 			//do remove previous/old content from page:
213 			var size = afterViewLoadRemoveList.length;
214 			for(var i=size-1; i >= 0; --i){
215 				//remove element from DOM via jQuery method:
216 				afterViewLoadRemoveList[i].remove();
217 			}
218 			if(size > 0){
219 				//remove all elements from array
220 				afterViewLoadRemoveList.splice(0, size);
221 			}
222 
223 			var ctrl = data.options[FIELD_NAME_CONTROLLER];
224 			var view = data.options[FIELD_NAME_VIEW];
225 			var renderData = data.options[FIELD_NAME_DATA];
226 
227 			//FIX handle missing ctrl/view parameter gracefully 
228 			//     this may occur when doRemoveElementsAfterViewLoad is 
229 			//     triggered NOT through doRenderView but by some automatic
230 			//	   mechanism, e.g. BACK history event that was not handled
231 			//	   by the framework (which ideally should not happen ...)
232 			var viewName;
233 			if(view){
234 				viewName = view.getName();
235 			}
236 
237 			if(!ctrl){
238 				console.error('PresentationManager[jqmViewEngine].__doRemoveElementsAfterViewLoad: missing controller (and view)!',data.options);
239 				return;
240 			}
241 
242 			//trigger "after page loading" hooks on controller:
243 			// the hook for all views of the controller MUST be present/implemented:
244 			ctrl.perform('on_page_load', renderData, viewName);
245 			//... the hook for single/specific view MAY be present/implemented:
246 			if(view){
247 				ctrl.performIfPresent('on_page_load_'+viewName, renderData);
248 			}
249 
250 		};
251 
252 		// set jQuery Mobile's default transition to "none":
253 		// TODO make this configurable (through mmir.ConfigurationManager)?
254 		jq.mobile.defaultPageTransition = 'none';
255 
256 		/**
257 		 * Actually renders the View.<br>
258 		 * Fetches the layout for the controller, then fills the
259 		 * layout-template with the view content, while incorporating
260 		 * partials and contents that helper methods have provided. Then
261 		 * Dialogs are created and the pageContainer id is updated. At last
262 		 * all the content is localized using
263 		 * {@link mmir.LanguageManager#translateHTML}, and appended to
264 		 * the HTML document of the application, while the old one is
265 		 * removed.<br>
266 		 * At the end the <b>on_page_load</b> action is performed.
267 		 * 
268 		 * @function
269 		 * 
270 		 * @param {String}
271 		 *            ctrlName Name of the controller
272 		 * @param {String}
273 		 *            viewName Name of the view to render
274 		 * @param {Object}
275 		 *            view View object that is to be rendered
276 		 * @param {Object}
277 		 *            ctrl Controller object of the view to render
278 		 * @param {Object}
279 		 *            [data] optional data for the view.
280 		 *            Currently same jQuery Mobile specific properties are supported: <br>
281 		 *            When these are present, they will be used for animating the 
282 		 *            page transition upon rendering.
283 		 *            
284 		 *            <pre>{transition: STRING, reverse: BOOLEAN}</pre>
285 		 *            where<br>
286 		 *            <code>transition</code>: the name for the transition (see jQuery Mobile Doc for possible values)
287 		 *            							DEFAULT: "none".
288 		 *            <code>reverse</code>: whether the animation should in "forward" (FALSE) direction, or "backwards" (TRUE)
289 		 *            						DEFAULT: FALSE
290 		 *
291 		 * @function
292 		 * @private
293 		 * @memberOf JqmViewEngine#
294 		 */
295 		var doRenderView = function(ctrlName, viewName, view, ctrl, data){
296 
297 			//if set to FALSE by one of the hooks (ie. before_page_prepare / before_page_load)
298 			//   will prevent rendering of the view! 
299 			var isContinue;
300 
301 			//trigger "before page preparing" hooks on controller, if present/implemented: 
302 			isContinue = ctrl.performIfPresent('before_page_prepare', data, viewName);
303 			if(isContinue === false){
304 				return;/////////////////////// EARLY EXIT ////////////////////////
305 			}
306 
307 			isContinue = ctrl.performIfPresent('before_page_prepare_'+viewName, data);
308 			if(isContinue === false){
309 				return;/////////////////////// EARLY EXIT ////////////////////////
310 			}
311 
312 			var layout = this.getLayout(ctrlName, true);
313 
314 			var layoutBody = layout.getBodyContents();
315 			var layoutDialogs = layout.getDialogsContents();
316 			//TODO var layoutHeader = layout.getHeaderContents();
317 
318 			layoutBody = renderUtils.renderViewContent(layoutBody, layout.getYields(), view.contentFors, data );
319 			layoutDialogs = renderUtils.renderViewDialogs(layoutDialogs, layout.getYields(), view.contentFors, data );
320 
321 			//TODO handle additional template syntax e.g. for BLOCK, STATEMENT (previously: partials)
322 			var dialogs = jq("#applications_dialogs");//<- TODO make this ID a CONST & export/collect all CONSTs in one place 
323 			dialogs.empty();
324 
325 			dialogs.append(layoutDialogs);
326 
327 //			// Translate the Keywords or better: localize it... 
328 //			NOTE: this is now done during rendering of body-content                  	layoutBody = mmir.LanguageManager.translateHTML(layoutBody);
329 			//TODO do localization rendering for layout (i.e. none-body- or dialogs-content)
330 
331 			var pg = new RegExp(CONTENT_ID, "ig");
332 			var oldId = "#" + CONTENT_ID + this.pageIndex;
333 
334 			// get old content from page
335 			var oldContent = jq(oldId);
336 			if(oldContent.length < 1 && oldId == '#'+CONTENT_ID+'0'){
337 				//the ID of the first page (pageIndex 0) may have no number postfix
338 				// -> try without number:
339 				oldContent = jq('#' + CONTENT_ID);
340 			}
341 
342 			//mark old content for removal
343 			afterViewLoadRemoveList.push(oldContent);
344 
345 			++ this.pageIndex;
346 			var newId = CONTENT_ID + this.pageIndex;
347 
348 			//TODO detect ID-attribute of content-TAG when layout is initialized instead of here
349 			layoutBody = layoutBody.replace(pg, newId);
350 
351 			if(typeof jq.parseHTML !== 'undefined'){
352 				layoutBody = jq.parseHTML(layoutBody);
353 			}
354 			var newPage = jq(layoutBody);
355 
356 
357 			//trigger "before page loading" hooks on controller, if present/implemented: 
358 			isContinue = ctrl.performIfPresent('before_page_load', data, viewName);//<- this is triggered for every view in the corresponding controller
359 			if(isContinue === false){
360 				return;/////////////////////// EARLY EXIT ////////////////////////
361 			}
362 
363 			isContinue = ctrl.performIfPresent('before_page_load_'+viewName, data);
364 			if(isContinue === false){
365 				return;/////////////////////// EARLY EXIT ////////////////////////
366 			}
367 
368 			//'load' new content into the page (using jQuery mobile)
369 			newPage.appendTo(jq.mobile.pageContainer);
370 
371 			//pass controller- and view-instance to "after page change" handler (jQuery Mobile specific!)
372 			var changeOptions = {};
373 			changeOptions[FIELD_NAME_VIEW] = view;
374 			changeOptions[FIELD_NAME_DATA] = data;
375 			changeOptions[FIELD_NAME_CONTROLLER] = ctrl;
376 
377 
378 			//set transition options, if present (jQuery Mobile specific!):
379 			if(data && typeof data.transition !== 'undefined'){
380 
381 				changeOptions.transition= data.transition;
382 			}
383 			if(data && typeof data.reverse !== 'undefined'){
384 
385 				changeOptions.reverse = data.reverse;
386 			}
387 
388 
389 			//change visible page from old to new one (using jQuery mobile)
390 
391 			//jQuery Mobile 1.4 API:
392 			var pageContainer = jq(':mobile-pagecontainer');
393 			//add handler that removes old page, after the new one was loaded:
394 			pageContainer.pagecontainer({change: doRemoveElementsAfterViewLoad});
395 			//actually change the (visible) page to the new one:
396 			pageContainer.pagecontainer('change', '#' + newId, changeOptions);
397 
398 
399 			//FIX moved into doRemoveElementsAfterViewLoad()-handler (if transition-animation is used, these must be called from handler!)
400 //			//trigger "after page loading" hooks on controller:
401 //			// the hook for all views of the controller MUST be present/implemented:
402 //			ctrl.perform('on_page_load', data);
403 //			//... the hook for single/specific view MAY be present/implemented:
404 //			ctrl.performIfPresent('on_page_load_'+viewName, data);
405 
406 		};
407 		
408 		//the exported functions (i.e. the rendering-engine interface):
409 		promise.resolve({
410 			
411 			/** @scope JqmViewEngine.prototype */
412 			
413 			/**
414 			 * Public render function - see {@link #doRenderView}
415 			 *  
416 			 * @public
417 			 * @memberOf mmir.env.view.JqmViewEngine.prototype
418 			 * 
419 			 * @function
420 			 * @borrows #doRenderView
421 			 * 
422 			 * @see #doRenderView
423 			 */
424 			render: doRenderView,
425 			/**
426              * Closes a modal window / dialog.<br>
427              * 
428              * @requires jQuery Mobile SimpleModal
429              * 
430              * @function
431              * @public
432 			 * @memberOf mmir.env.view.JqmViewEngine.prototype
433              * 
434 			 * @see #showDialog
435 			 * @see mmir.PresentationManager#showDialog
436              */
437             hideCurrentDialog : function() {
438                 
439                 if (jq.modal != null) {
440                     jq.modal.close();
441                 }
442                 else {
443                 	console.warn('PresentationManager[jqmViewEngine].hideCurrentDialog: could not find SimpleModal plugin: jQuery.modal is '+(typeof jq.modal));
444                 }
445             },
446             /**
447              * Opens the requested dialog.<br>
448              * 
449              * @requires jQuery Mobile SimpleModal
450              * @requires mmir.ControllerManager
451              * 
452              * 
453              * @function
454              * @param {String}
455              *            ctrlName Name of the controller
456              * @param {String}
457              *            dialogId Id of the dialog
458              * @param {Object}
459              *            data Optionally data - not used
460              *            
461              * @returns {Object} the instance of the current dialog that was opened
462              * 
463              * @public
464 			 * @memberOf mmir.env.view.JqmViewEngine.prototype
465              * 
466 			 * @see #hideCurrentDialog
467 			 * @see mmir.PresentationManager#hideCurrentDialog
468              */
469             showDialog : function(ctrlName, dialogId, data) {
470 
471 				this.hideCurrentDialog();
472 
473 				var ctrl = controllerManager.getController(ctrlName);
474 				
475 				if (ctrl != null) {
476 
477 					return jq("#" + dialogId).modal({
478 						
479 						overlayId : 'recorder-overlay',
480 						containerId : 'recorder-container',
481 						//$("#"+dialogId).modal({overlayId: dialogId+"overlay",containerId: dialogId+"container",  
482           					  
483           				//closeHTML: null,opacity: 65, position: ['0',],overlayClose: true,onOpen: this.open,onClose: this.close
484 						closeHTML : null,
485 						opacity : 65,
486 						position : [ '0' ],
487 						overlayClose : false//,
488 //						onOpen: current_dialog.open,
489 //						onClose: current_dialog.close
490 
491 					}); /////////////////////////////////// EARLY EXIT ////////////////////////
492 
493 
494 					//DISABLED: this would require jqtransform.js / jqtransform.css
495 //					jq('.transformed-checkbox').jqTransform({
496 //						imgPath : 'jqtransformplugin/img/'
497 //					});
498 					
499 				} else {
500 					console.error("PresentationManager[jqmViewEngine].showDialog: Could not find Controller for '" + ctrlName + "'");
501 				}
502 			},
503 			
504 			/**
505 			 * Shows a "wait" dialog, i.e. "work in progress" notification.
506 			 * 
507 			 * @function
508 			 * 
509 			 * @param {String} [text] OPTIONAL
510 			 * 				the text that should be displayed.
511 			 * 				If omitted the language setting for <code>loadingText</code>
512 			 * 				will be used instead (from dictionary.json)
513 			 * @param {String} [theme] OPTIONAL
514 			 * 				set the jQuery Mobile theme to be used for the wait-dialog
515 			 * 				(e.g. "a" or "b").
516 			 * 				NOTE: if this argument is used, then the <code>text</code>
517 			 * 					  must also be supplied.
518 			 * 
519 			 * @public
520 			 * @memberOf mmir.env.view.JqmViewEngine.prototype
521 			 * 
522 			 * @requires jQuery Mobile: <code>$.mobile.loading</code>
523 			 * @requires mmir.LanguageManager
524 			 * 
525 			 * @see #hideWaitDialog
526 			 * @see mmir.PresentationManager#hideWaitDialog
527 			 */
528 			showWaitDialog : function(text, theme) {
529 
530 				var loadingText = typeof text === 'undefined'? languageManager.getText('loadingText') : text;
531 				var themeSwatch = typeof theme === 'undefined'? 'b' : text;//TODO define a default & make configurable (-> mmir.ConfigurationManager) 
532 				
533 				if (loadingText !== null && loadingText.length > 0) {
534 //					console.log('[DEBUG] setting loading text to: "'+loadingText+'"');
535 					jq.mobile.loading('show', {
536 						text : loadingText,
537 						theme: themeSwatch,
538 						textVisible : true
539 					});
540 				}
541 				else {
542 					jq.mobile.loading('show',{
543 						theme: themeSwatch,
544 						textVisible : false
545 					});
546 				}
547 			},
548 
549 			/**
550 			 * Hides / closes the "wait" dialog.
551 			 * 
552 			 * @function
553 			 * @public
554 			 * @memberOf mmir.env.view.JqmViewEngine.prototype
555 			 * 
556 			 * @requires jQuery Mobile: <code>$.mobile.loading</code>
557 			 * 
558 			 * @see #showWaitDialog
559 			 * @see mmir.PresentationManager#showWaitDialog
560 			 */
561 			hideWaitDialog : function() {
562 
563 				jq.mobile.loading('hide');
564 
565 			}
566 		});
567 	});
568 	
569 	return promise;
570 });
571