/**
* Example for a simplified view/rendering engine:
*
* uses the standard document API for rendering / inserting views into the current document.
*
*
* @example
* mmir.present.render('theController', 'theView');
*
* @class
* @name SimpleViewEngine
* @memberOf mmir.env.view
* @static
* @hideconstructor
*
* @depends document (DOM object)
* @depends HTMLElement.parentElement
* @depends HTMLElement.removeChild
*
*/
define(['mmirf/loadCss', 'mmirf/logger', 'mmirf/util/deferred', 'module', 'require'],function(loadCss, Logger, Deferred, module, require){
var log = Logger.create(module);
var modConfig = module.config(module);
//load CSS, if one is set/configured:
var SVE_CSS_ID = modConfig.cssId;
var SVE_CSS_HREF = modConfig.cssUrl;
if(SVE_CSS_HREF){
//include default styles for simpleViewEngine in webpack build
if(typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD){
//FIXME detect, if mmirf/simpleViewEngine is used/included & if cssUrl-config-value points to the default styles -> only include css-file if both apply
SVE_CSS_HREF = require('../../vendor/styles/simpleViewLayout.css');
}
loadCss({href: SVE_CSS_HREF, id: SVE_CSS_ID});
}
var promise = new Deferred();
require(['mmirf/commonUtils', 'mmirf/renderUtils', 'mmirf/languageManager', 'mmirf/controllerManager', 'mmirf/waitDialog', 'mmirf/util/forEach'],
function(commonUtils, renderUtils, languageManager, controllerManager, dlg, forEach
){
var nodeFind = function(node, tagType, attrName, attrVal){
var list = node.getElementsByTagName(tagType);
var result = [], el;
for(var i=0, size = list.length; i < size; ++i){
el = list[i];
if(el[attrName] == attrVal){
result.push(el);
}
}
return result;
};
//prepare resources for standalone-wait-dialog
dlg._loadStyle();
var _viewEngineWaitId = 'view-wait-dlg';
/**
* delay in case a Layout that is rendered includes a CSS file:
* signal "on_page_load" after this delay so that (hopefully) the CSS has been loaded
*
* NOTE: the onload listener for LINK-tags does not work in all browsers, so it cannot be used for checking if CSS has been loaded
*
* TODO make configurable?
*/
var CSS_LOAD_DELAY = typeof window.CSS_LOAD_DELAY === 'number'? window.CSS_LOAD_DELAY : 10;//ms
/**
* The ID attribute for the content / page-elements.
*
* <p>
* This is jQuery Mobile specific:
* pages are contained in an element with <code>data-role="page"</code>.
*
* These elements must have an ID attribute with the value of this constant
* (the actual value will be created and set on rendering the view / layout).
*
* @property CONTENT_ID
* @type String
* @public
* @constant
*/
var CONTENT_ID = "pageContainer";
//property names for passing the respected objects from doRenderView() to doRemoveElementsAfterViewLoad()
var FIELD_NAME_RESOLVE = '__renderResolve';
var FIELD_NAME_VIEW = '__view';
var FIELD_NAME_DATA = '__renderData';
var FIELD_NAME_CONTROLLER = '__ctrl';
var FIELD_NAME_MANAGER = '__present';
/**
* Reference to the layout that was rendered last.
*
* This is updated in doRenderView() before the view is actually rendered.
*
* @type mmir.view.Layout
* @private
* @memberOf SimpleJqViewEngine#
*/
var lastLayout = null;
//function for removing "old" content from DOM (-> remove old, un-used page content)
var doRemoveElementsAfterViewLoad = function(_event, data){
var presentMgr = data[FIELD_NAME_MANAGER];
var ctrl = data[FIELD_NAME_CONTROLLER];
var view = data[FIELD_NAME_VIEW];
var renderData = data[FIELD_NAME_DATA];
var defer = data[FIELD_NAME_RESOLVE];
//FIX handle missing ctrl/view parameter gracefully
// this may occur when doRemoveElementsAfterViewLoad is
// triggered NOT through doRenderView but by some automatic
// mechanism, e.g. BACK history event that was not handled
// by the framework (which ideally should not happen ...)
var viewName;
if(view){
viewName = view.getName();
}
if(!ctrl){
console.error('PresentationManager[simpleViewEngine].__doRemoveElementsAfterViewLoad: missing controller (and view)!',data.options);
return;
}
//trigger "after page loading" hooks on controller:
// the hook for all views of the controller MAY be present/implemented:
presentMgr._fireRenderEvent(ctrl, 'on_page_load', renderData, viewName, renderData);
//... the hook for single/specific view MAY be present/implemented:
if(view){
presentMgr._fireRenderEvent(ctrl, 'on_page_load_'+viewName, renderData);
}
defer.resolve();
};
/**
* HELPER generate a marker for inserted LINK and STYLE elements
* (so that they can easily removed)
*
* @function
* @private
* @memberOf SimpleJqViewEngine#
*/
var getLayoutMarkerAttr = function(layout){
return 'rendered_layout_' + layout.getName();
};
/**
* HELPER remove layout resources (before loading a new layout).
*
* @param {mmir.view.Layout} layout
* the layout which's resources should be removed
*
* @function
* @private
* @memberOf SimpleJqViewEngine#
*/
var doRemoveLayoutResources = function(layout){
// jquery('head .' + getLayoutMarkerAttr(layout)).remove();
var list = document.head.getElementsByClassName(getLayoutMarkerAttr(layout));
var el;
for(var i = list.length-1; i >= 0; --i){
el = list[i];
if(el && el.parentElement){
el.parentElement.removeChild(el);
}
}
};
/**
* Prepares the layout, before loading a view:
* loads referenced SCRIPTs, LINKs, and STYLEs.
*
* This function should not be called, if the layout is already loaded,
* i.e. SCRIPTs etc. are meant to be load-once (not load-on-every-page-rendering)
*
* @param {mmir.view.Layout} layout
* the layout which's resources should be prepared
* @returns {Promise}
* a deferred promise that gets resolved when the resources have been prepared
*
* @function
* @private
* @memberOf SimpleJqViewEngine#
*/
var doPrepareLayout = function(layout){
//initialize with layout contents:
/** @type Array<TagElement> */
var headerContents = layout.headerElements;
var scriptList = [];
var layoutMarker = getLayoutMarkerAttr(layout);
var isLinkLoading = false;
var head, htmlStyle;
forEach(headerContents, function(elem){
if( elem.isScript()){
scriptList.push(elem.src);
} else if(elem.isLink()){
isLinkLoading = true;
loadCss({'href': elem.href, 'class': layoutMarker, 'type': elem.type || 'text/css'});
} else if(elem.isStyle()){
if(!head){
head = document.head;
}
htmlStyle = ['<style class="', layoutMarker, '">', elem.html(), '</style>' ].join('');
head.innerHTML = htmlStyle;
} else {
console.warn('simpleViewEngine.doPrepareLayout: unknown header element type: '+ elem.tagName, elem);
}
});
if(scriptList.length === 0){
var defer = new Deferred();
if(isLinkLoading){
//if css file is loading, resolve with a small delay, so that (most/some of) the CSS is loaded by then
setTimeout(function(){defer.resolve();}, CSS_LOAD_DELAY);
} else {
defer.resolve();
}
return defer;/////////////////////////// EARLY EXIT ///////////////////////////////
} else {
if(!head){
head = document.head;
}
var defer = new Deferred();
var resolved = 0;//<- counter for resolved async-executions
var setResolved = function(){++resolved; checkResolve();}
var checkResolve = function(){
if(resolved === 2){//<- expected async-executions: 2
defer.resolve();
}
}
//wait until all referenced scripts form LAYOUT have been loaded (may be used/required when views get rendered)
commonUtils.loadImpl(
scriptList,
true,//<- load serially, since scripts may depend on each other
null,//<- use returned promise instead of callback
function checkIsAlreadyLoadedFunc(fileName){
//if script is already loaded, do not load again:
// return head.find('script[src="'+fileName+'"]').length > 0;
var list = nodeFind(head, 'script', 'src', fileName);
return list.length > 0;
}
).then(setResolved);
if(isLinkLoading){
//if css file is loading, resolve with a small delay, so that (most/some of) the CSS is loaded by then
setTimeout(setResolved, CSS_LOAD_DELAY);
} else {
setResolved();
}
return defer;/////////////////////////// EARLY EXIT ///////////////////////////////
}
};
/**
* Actually renders the View.<br>
* Fetches the layout for the controller, then fills the
* layout-template with the view content, while incorporating
* partials and contents that helper methods have provided. Then
* Dialogs are created and the pageContainer id is updated. At last
* all the content is localized using
* {@link mmir.LanguageManager#translateHTML}, and appended to
* the HTML document of the application, while the old one is
* removed.<br>
* At the end the <b>on_page_load</b> action is performed.
*
* @function doRenderView
*
* @param {String}
* ctrlName Name of the controller
* @param {String}
* viewName Name of the view to render
* @param {Object}
* view View object that is to be rendered
* @param {Object}
* ctrl Controller object of the view to render
* @param {Object}
* [data] optional data for the view.
* @returns {Promise}
* a Promise that gets resolved when rendering is finished
*
* @private
* @memberOf mmir.env.view.SimpleViewEngine#
*/
var doRenderView = function(ctrlName, viewName, view, ctrl, data){
//if set to FALSE by one of the hooks (ie. before_page_prepare / before_page_load)
// will prevent rendering of the view!
var isContinue;
//trigger "before page preparing" hooks on controller, if present/implemented:
isContinue = this._fireRenderEvent(ctrl, 'before_page_prepare', data, viewName);
if(isContinue === false){
return;/////////////////////// EARLY EXIT ////////////////////////
}
isContinue = this._fireRenderEvent(ctrl, 'before_page_prepare_'+viewName, data);
if(isContinue === false){
return;/////////////////////// EARLY EXIT ////////////////////////
}
var layout = this.getLayout(ctrlName, true);
var renderResolve = new Deferred();
var presentMgr = this;
var renderFunc = function(){
var title = layout.title;
if(title){
document.title = title;
}
var layoutBody = renderUtils.renderViewContent(layout.bodyContentElement, null, view.contentFors, data);
var layoutDialogs = renderUtils.renderViewDialogs(layout.getDialogsContents(), layout.getYields(), view.contentFors, data);
var dialogs = document.getElementById("applications_dialogs");//<- TODO make this ID a CONST & export/collect all CONSTs in one place
if(!dialogs){
dialogs = document.getElementsByTagName('dialog');
var len = dialogs.length;
if(len === 0){
log.warn('PresentationManager[simpleViewEngine].doRenderView: no dialogs container, inserting new one into document.body.');
dialogs = document.createElement('div');
dialogs.id = 'applications_dialogs';
dialogs.innerHTML = layoutDialogs;
document.body.appendChild(dialogs);
} else if(len > 1){
log.warn('PresentationManager[simpleViewEngine].doRenderView: too many <dialog> definitions ('+dialogs.length+').');
}
dialogs = dialogs[0];//<- dialogs is set to undefined, if no <dialog>-elements were found
}
if(dialogs){
dialogs.innerHTML = layoutDialogs;
}
var pg = new RegExp(CONTENT_ID, "ig");
var oldId = CONTENT_ID + presentMgr.pageIndex;
// get old content from page
var oldContent = document.getElementById(oldId);
if(!oldContent && oldId == CONTENT_ID+'0'){
//the ID of the first page (pageIndex 0) may have no number postfix
// -> try without number:
if(log.isVerbose()) log.debug('PresentationManager[simpleViewEngine].doRenderView: removing old content: no old centent found for old ID "'+oldId+'", trying "#'+CONTENT_ID+'" instead...');//debug
oldId = CONTENT_ID;
oldContent = document.getElementById(oldId);
}
++ presentMgr.pageIndex;
var newId = CONTENT_ID + presentMgr.pageIndex;
//TODO detect ID-attribute of content-TAG when layout is initialized instead of here
layoutBody = layoutBody.replace(pg, newId);
var newPage = layoutBody;
//provide "change" data for before_page_load calls:
var pageEvtData = {
name: viewName,
id: newId,
oldSel: oldId,
content: newPage
};
//trigger "before page loading" hooks on controller, if present/implemented:
isContinue = presentMgr._fireRenderEvent(ctrl, 'before_page_load', data, pageEvtData);//<- this is triggered for every view in the corresponding controller
if(isContinue === false){
return;/////////////////////// EARLY EXIT ////////////////////////
}
isContinue = presentMgr._fireRenderEvent(ctrl, 'before_page_load_'+viewName, data, pageEvtData);
if(isContinue === false){
return;/////////////////////// EARLY EXIT ////////////////////////
}
//pass controller- and view-instance to "after page change" handler
var changeOptions = {};
changeOptions[FIELD_NAME_RESOLVE] = renderResolve;
changeOptions[FIELD_NAME_VIEW] = view;
changeOptions[FIELD_NAME_DATA] = data;
changeOptions[FIELD_NAME_CONTROLLER] = ctrl;
changeOptions[FIELD_NAME_MANAGER] = presentMgr;
//change visible page from old to new one (using simple jQuery replace function)
var pageContainer = oldContent;
if(pageContainer && pageContainer.parentElement){
pageContainer.parentElement.innerHTML = newPage;
} else {
log.error('PresentationManager[simpleViewEngine].doRenderView: could not find parent for pageContainer, inserting new one into document.body!');
var pageContainerWrapper = document.createElement('div');
pageContainerWrapper.id = 'pageContainer';
pageContainerWrapper.innerHTML = newPage;
document.body.appendChild(pageContainerWrapper);
}
newPage = document.getElementById(newId);
doRemoveElementsAfterViewLoad.call(newPage,{},changeOptions);
};
if(layout !== lastLayout){
//if lastLayout is not null: unload its SCRIPTs, LINKs, and STYLEs?
if(lastLayout){
doRemoveLayoutResources(lastLayout);
}
lastLayout = layout;
doPrepareLayout(layout).then(renderFunc);
} else {
renderFunc();
}
return renderResolve;
};
promise.resolve({
/**
* @copydoc mmir.env.view.SimpleViewEngine#doRenderView
* @function render
* @public
*
* @memberOf mmir.env.view.SimpleViewEngine.prototype
* @see mmir.PresentationManager#render
*/
render: doRenderView,
/**
* Closes a modal window / dialog.<br>
*
* @depends jQuery Mobile SimpleModal
*
* @function hideCurrentDialog
* @public
*
* @memberOf mmir.env.view.SimpleViewEngine.prototype
* @see mmir.PresentationManager#hideCurrentDialog
*/
hideCurrentDialog : function() {
//TODO implement this!
// if (jq.modal != null) {
// //TODO implement this!
// }
// else {
// console.warn('PresentationManager[simpleViewEngine].hideCurrentDialog: could not find SimpleModal plugin: jQuery.modal is '+(typeof jq.modal));
// }
},
/**
* Opens the requested dialog.<br>
*
* @depends jQuery Mobile SimpleModal
* @depends mmir.ControllerManager
*
*
* @function showDialog
* @param {String}
* ctrlName Name of the controller
* @param {String}
* _dialogId Id of the dialog
* @param {Object}
* _data Optionally data - not used
*
* @returns {Object} the instance of the current dialog that was opened
*
* @public
* @memberOf mmir.env.view.SimpleViewEngine.prototype
* @see mmir.PresentationManager#showDialog
*/
showDialog : function(ctrlName, _dialogId, _data) {
this.hideCurrentDialog();
var ctrl = controllerManager.getController(ctrlName);
if (ctrl != null) {
//TODO implement something!?!
} else {
console.error("PresentationManager[simpleViewEngine].showDialog: Could not find Controller for '" + ctrlName + "'");
}
},
/**
* Shows a "wait" dialog, i.e. "work in progress" notification.
*
* @function showWaitDialog
*
* @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 {String} [theme] OPTIONAL
* set the jQuery Mobile theme to be used for the wait-dialog
* (e.g. "a" or "b").
* NOTE: if this argument is used, then the <code>text</code>
* must also be supplied.
*
* @public
*
* @depends stdlne-wait-dlg (Standalone Wait Dialog)
* @depends mmir.LanguageManager
*
* @see #hideWaitDialog
*
* @memberOf mmir.env.view.SimpleViewEngine.prototype
* @see mmir.PresentationManager#showWaitDialog
*/
showWaitDialog : function(text, theme) {
var loadingText = typeof text === 'undefined'? languageManager.getText('loadingText') : text;
if(typeof theme !== 'undefined'){
dlg.defaultStyle = theme;
//TODO
}
dlg.show(loadingText, _viewEngineWaitId);
},
/**
* Hides / closes the "wait" dialog.
*
* @function hideWaitDialog
* @public
*
* @depends stdlne-wait-dlg (Standalone Wait Dialog)
*
* @see #showWaitDialog
*
* @memberOf mmir.env.view.SimpleViewEngine.prototype
* @see mmir.PresentationManager#hideWaitDialog
*/
hideWaitDialog : function() {
dlg.hide(_viewEngineWaitId);
},
/////////////////////////////////// Additional non-standard functions & properties /////////////
styleTagId: SVE_CSS_ID,
styleTagHref: SVE_CSS_HREF,
isStylePresent: function(){
if(!SVE_CSS_HREF){
//if no css URL was configured: always return state as if stylesheet was already loaded
return true;
}
return document.getElementById(this.styleTagId);
},
loadStyle: function(){
if(!this.isStylePresent()){
loadCss({href: this.styleTagHref, id: this.styleTagId});
}
}
});
});
return promise;
});