diff --git a/etemplate/js/et2_core_widget.js b/etemplate/js/et2_core_widget.js index 73f856d5ce..e424d4177b 100644 --- a/etemplate/js/et2_core_widget.js +++ b/etemplate/js/et2_core_widget.js @@ -730,26 +730,68 @@ var et2_widget = Class.extend( * function is called, the DOM-Tree is created. loadingFinished is * recursively called for all child elements. Do not directly override this * function but the doLoadingFinished function which is executed before - * descending deeper into the DOM-Tree + * descending deeper into the DOM-Tree. + * + * Some widgets (template) do not load immediately because they request + * additional resources via AJAX. They will return a Deferred Promise object. + * If you call loadingFinished(promises) after creating such a widget + * programmatically, you might need to wait for it to fully complete its + * loading before proceeding. In that case use: + * + * var promises = []; + * widget.loadingFinished(promises); + * jQuery.when.apply(null, promises).done( doneCallback ); + * + * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} + * + * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. */ - loadingFinished: function() { + loadingFinished: function(promises) { // Call all availble setters this.initAttributes(this.options); + + // Make sure promises is defined to avoid errors. + // We'll warn (below) if programmer should have passed it. + if(typeof promises == "undefined") + { + promises = []; + var warn_if_deferred = true; + } - if (this.doLoadingFinished()) + var loadChildren = function() { // Descend recursively into the tree for (var i = 0; i < this._children.length; i++) { try { - this._children[i].loadingFinished(); + this._children[i].loadingFinished(promises); } catch (e) { egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o",e.valueOf(),this._children[i],e.stack); } } + }; + + var result = this.doLoadingFinished(); + if(typeof result == "boolean" && result) + { + // Simple widget finishes nicely + loadChildren.apply(this, arguments); + } + else if (typeof result == "object" && result.done) + { + // Warn if list was not provided + if(warn_if_deferred) + { + // Might not be a problem, but if you need the widget to be really loaded, it could be + egw.debug("warning", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished()."); + } + // Widget is waiting. Add to the list + promises.push(result); + // Fihish loading when it's finished + result.done(jQuery.proxy(loadChildren, this)); } }, @@ -779,6 +821,15 @@ var et2_widget = Class.extend( } }, + /** + * Does specific post-processing after the widget is loaded. Most widgets should not + * need to do anything here, it should all be done before. + * + * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children, + * or a Promise if loading is not actually finished (eg. waiting for AJAX) + * + * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} + */ doLoadingFinished: function() { return true; }, diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index bdc0a73a07..a316dc55ba 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -1205,7 +1205,7 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], { // Keep the name of the template, as we'll free up the widget after parsing this.template = _value; - template.loadingFinished(); + // Fetch the grid element and parse it var definitionGrid = template.getChildren()[0]; if (definitionGrid && definitionGrid instanceof et2_grid) @@ -1242,7 +1242,11 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], }; // Template might not be loaded yet, defer parsing - $j(template.getDOMNode()).on("load", + var promise = [] + template.loadingFinished(promise); + + // Wait until template (& children) are done + jQuery.when.apply(null, promise).done( jQuery.proxy(function() { parse.call(this, template); this.dynheight.initialized = false; @@ -1656,7 +1660,7 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, _build_left_right: function(left_or_right, template_name) { var existing = this.headers[left_or_right == "left" ? 0 : 1]; - if(existing) + if(existing && existing._type) { if(existing.id == template_name) return; existing.free(); @@ -1666,11 +1670,12 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, // Load the template var header = et2_createWidget("template", {"id": template_name}, this); jQuery(header.getDOMNode()).addClass(left_or_right == "left" ? "et2_hbox_left":"et2_hbox_right").addClass("nm_header"); - this.headers.push(header); + this.headers[left_or_right == "left" ? 0 : 1] = header; $j(header.getDOMNode()).on("load", jQuery.proxy(function() { - header.loadingFinished(); + //header.loadingFinished(); this._bindHeaderInput(header); },this)); + header.loadingFinished(); }, /** @@ -1873,9 +1878,12 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, return this.filters[0]; } } - for(var i = 0; i < this.headers.length; i++) + if(_sender && _sender._type == "template") { - if(_sender.id == this.headers[i].id && _sender._parent == this) return this.header_div[0]; + for(var i = 0; i < this.headers.length; i++) + { + if(_sender.id == this.headers[i].id && _sender._parent == this) return this.header_div[0]; + } } return null; }, diff --git a/etemplate/js/et2_widget_template.js b/etemplate/js/et2_widget_template.js index b839452d22..e479bf04c4 100644 --- a/etemplate/js/et2_widget_template.js +++ b/etemplate/js/et2_widget_template.js @@ -77,8 +77,8 @@ var et2_template = et2_DOMWidget.extend( this.div = document.createElement("div"); - // Flag to indicate that loading is finished - this.loading = false; + // Deferred object so we can load via AJAX + this.loading = jQuery.Deferred(); if (this.id != "" || this.options.template) { @@ -108,8 +108,6 @@ var et2_template = et2_DOMWidget.extend( if(splitted.length) { - // Still loading, don't fire loading finished - this.loading = true; et2_loadXMLFromURL(path, function(_xmldoc) { var templates = this.getInstanceManager().templates || {}; // Scan for templates and store them @@ -123,13 +121,7 @@ var et2_template = et2_DOMWidget.extend( this.loadFromXML(templates[template_name]); // Update flag - this.loading = false; - - // Fire the load event (after) - var self = this; - window.setTimeout(function() { - $j(self.getDOMNode()).trigger('load'); - },0); + this.loading.resolve(); }, this); } @@ -140,17 +132,16 @@ var et2_template = et2_DOMWidget.extend( { this.egw().debug("log", "Loading template from XML: ", template_name); this.loadFromXML(xml); - // Don't call this here - premature + // Don't call this here - done by caller, or on whole widget tree //this.loadingFinished(); - // Fire the load event (after) - var self = this; - window.setTimeout(function() { - $j(self.getDOMNode()).trigger('load'); - },0); + + // But resolve the promise + this.loading.resolve(); } else { this.egw().debug("warn", "Unable to find XML for ", template_name); + this.loading.reject(); } } }, @@ -175,13 +166,17 @@ var et2_template = et2_DOMWidget.extend( }, /** - * Override to trigger a load event, to facilitate processing when the xml file - * is loaded asyncronously + * Override to return the promise for deferred loading */ doLoadingFinished: function() { - if(this.loading) return false; + // Apply parent now, which actually puts into the DOM this._super.apply(this, arguments); - return true; + + // Fire load event when done loading + this.loading.done(jQuery.proxy(function() {$j(this).trigger("load");},this.div)); + + // Not done yet, but widget will let you know + return this.loading.promise(); } }); et2_register_widget(et2_template, ["template"]); diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js index 86e935d185..10bffddcad 100644 --- a/etemplate/js/etemplate2.js +++ b/etemplate/js/etemplate2.js @@ -245,8 +245,11 @@ etemplate2.prototype.load = function(_name, _url, _data, _callback) // Read the XML structure of the requested template this.widgetContainer.loadFromXML(this.templates[_name || missing_name]); + // List of Promises from widgets that are not quite fully loaded + var deferred = []; + // Inform the widget tree that it has been successfully loaded. - this.widgetContainer.loadingFinished(); + this.widgetContainer.loadingFinished(deferred); // Insert the document fragment to the DOM Container this.DOMContainer.appendChild(frag); @@ -257,20 +260,24 @@ etemplate2.prototype.load = function(_name, _url, _data, _callback) etemplate2._byTemplate[_name] = []; } etemplate2._byTemplate[_name].push(this); - - // Trigger the "resize" event - this.resize(); - - if(typeof _callback == "function") - { - _callback.call(window,this); - } - if(_callback != app_callback) - { - app_callback.call(window,this); - } - - $j(this.DOMContainer).trigger('load', this); + + // Wait for everything to be loaded, then finish it up + jQuery.when.apply(null, deferred).done(jQuery.proxy(function() { + // Trigger the "resize" event + this.resize(); + + // Tell others about it + if(typeof _callback == "function") + { + _callback.call(window,this); + } + if(_callback != app_callback) + { + app_callback.call(window,this); + } + + $j(this.DOMContainer).trigger('load', this); + },this)); };