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