From f7f4053d1afb184367da563fc95b18d62606b688 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 8 Mar 2022 15:11:32 -0700 Subject: [PATCH] Swap Promise for jQuery.Promise in loadingFinished() & doLoadingFinished() This lets us add LitElement's updateComplete Promise into the list of things to wait for, and solves the problem of app init code being run before widgets are complete. https://lit.dev/docs/components/lifecycle/#reactive-update-cycle-completing --- api/js/etemplate/Et2Widget/Et2Widget.ts | 14 ++- api/js/etemplate/et2_core_DOMWidget.ts | 12 +- api/js/etemplate/et2_core_inputWidget.ts | 4 +- api/js/etemplate/et2_core_widget.ts | 10 +- api/js/etemplate/et2_extension_nextmatch.ts | 28 +++-- api/js/etemplate/et2_widget_historylog.ts | 5 +- api/js/etemplate/et2_widget_tabs.ts | 50 ++++---- api/js/etemplate/et2_widget_template.ts | 123 +++++++++++--------- api/js/etemplate/etemplate2.ts | 19 +-- 9 files changed, 145 insertions(+), 120 deletions(-) diff --git a/api/js/etemplate/Et2Widget/Et2Widget.ts b/api/js/etemplate/Et2Widget/Et2Widget.ts index 679a270226..18732c8499 100644 --- a/api/js/etemplate/Et2Widget/Et2Widget.ts +++ b/api/js/etemplate/Et2Widget/Et2Widget.ts @@ -394,6 +394,17 @@ const Et2WidgetMixin = (superClass) => return this.classList.value; } + /** + * Set the widget class + * + * @deprecated Use this.class or this.classList instead + * @param {string} new_class + */ + set_class(new_class : string) + { + this.class = new_class; + } + /** * Event handlers */ @@ -771,8 +782,9 @@ const Et2WidgetMixin = (superClass) => child.loadingFinished(promises); } }; - doLoadingFinished(); + + promises.push(this.getUpdateComplete()); } getWidgetById(_id) diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index f32d97605c..a338d5d814 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -18,14 +18,8 @@ import {ClassWithAttributes} from './et2_core_inheritance'; import {et2_IDOMNode} from "./et2_core_interfaces"; import {et2_hasChild, et2_no_init} from "./et2_core_common"; import {et2_widget, WidgetConfig} from "./et2_core_widget"; -import { - egw_getActionManager, - egwActionObject, - egwActionObjectInterface, - egw_getAppObjectManager, - egw_getObjectManager -} from '../egw_action/egw_action.js'; -import {EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT} from '../egw_action/egw_action_constants.js'; +import {egw_getActionManager, egw_getAppObjectManager, egw_getObjectManager} from '../egw_action/egw_action.js'; +import {EGW_AI_DRAG_OUT, EGW_AI_DRAG_OVER} from '../egw_action/egw_action_constants.js'; import {egw} from "../jsapi/egw_global"; // fixing circular dependencies by only importing type import type {et2_tabbox} from "./et2_widget_tabs"; @@ -170,7 +164,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode /** * Attaches the container node of this widget to the DOM-Tree */ - doLoadingFinished() : boolean | JQueryPromise + doLoadingFinished() : boolean | Promise { // Check whether the parent implements the et2_IDOMNode interface. If // yes, grab the DOM node and create our own. diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts index 5f0a81fc63..c75f25115b 100644 --- a/api/js/etemplate/et2_core_inputWidget.ts +++ b/api/js/etemplate/et2_core_inputWidget.ts @@ -18,7 +18,7 @@ import {et2_no_init} from "./et2_core_common"; import {ClassWithAttributes} from "./et2_core_inheritance"; import {et2_widget, WidgetConfig} from "./et2_core_widget"; import {et2_valueWidget} from './et2_core_valueWidget' -import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "./et2_core_interfaces"; +import {et2_IInput, et2_ISubmitListener} from "./et2_core_interfaces"; import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; // fixing circular dependencies by only importing the type (not in compiled .js) import type {et2_tabbox} from "./et2_widget_tabs"; @@ -107,7 +107,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ /** * Make sure dirty flag is properly set */ - doLoadingFinished(): boolean | JQueryPromise + doLoadingFinished() : boolean | Promise { let result = super.doLoadingFinished(); diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index 38e7715c90..bd451b46d8 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -900,12 +900,12 @@ export class et2_widget extends ClassWithAttributes }; var result = this.doLoadingFinished(); - if (typeof result == "boolean" && result) + if(typeof result == "boolean" && result) { // Simple widget finishes nicely loadChildren.apply(this, arguments); } - else if (typeof result == "object" && result.done) + else if(typeof result == "object" && result.then) { // Warn if list was not provided if (warn_if_deferred) @@ -916,7 +916,7 @@ export class et2_widget extends ClassWithAttributes // Widget is waiting. Add to the list promises.push(result); // Fihish loading when it's finished - result.done(jQuery.proxy(loadChildren, this)); + result.then(loadChildren.bind(this)); } } @@ -956,9 +956,9 @@ export class et2_widget extends ClassWithAttributes * @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} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promise} */ - doLoadingFinished(): JQueryPromise | boolean + doLoadingFinished() : Promise | boolean { return true; } diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index 8dc677930c..952b8724ef 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -41,7 +41,13 @@ */ import {et2_csvSplit, et2_no_init} from "./et2_core_common"; -import {et2_IInput, et2_IPrint, et2_IResizeable, implements_methods, et2_implements_registry} from "./et2_core_interfaces"; +import { + et2_IInput, + et2_implements_registry, + et2_IPrint, + et2_IResizeable, + implements_methods +} from "./et2_core_interfaces"; import {ClassWithAttributes} from "./et2_core_inheritance"; import {et2_createWidget, et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; import {et2_DOMWidget} from "./et2_core_DOMWidget"; @@ -292,6 +298,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 row_selector: '', orientation_style: null }; + /** + * When loading the row template, we have to wait for the template before we try to process it. + * During the legacy load process, we need to return this from doLoadingFinished() so it can be waited on so we + * have to store it. + */ + private template_promise : Promise; /** * Constructor @@ -497,8 +509,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.controller._grid.doInvalidate = true; } }, this)); - - return true; + return this.template_promise ? this.template_promise : true; } /** @@ -2521,8 +2532,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 template.loadingFinished(promise); // Wait until template (& children) are done - jQuery.when.apply(null, promise).done( - jQuery.proxy(function() + // Keep promise so we can return it from doLoadingFinished + this.template_promise = Promise.all(promise).then(() => { parse.call(this, template); if(!this.dynheight) @@ -2531,9 +2542,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } this.dynheight.initialized = false; this.resize(); - }, this) - ); - return promise; + } + ).finally(() => this.template_promise = null); + + return this.template_promise; } // Some accessors to match conventions diff --git a/api/js/etemplate/et2_widget_historylog.ts b/api/js/etemplate/et2_widget_historylog.ts index 5e76cd70a3..6ee0ff959e 100644 --- a/api/js/etemplate/et2_widget_historylog.ts +++ b/api/js/etemplate/et2_widget_historylog.ts @@ -112,7 +112,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider this.options.status_id = _new_id; } - doLoadingFinished( ) : boolean | JQueryPromise + doLoadingFinished() : boolean | Promise { super.doLoadingFinished(); @@ -121,7 +121,8 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider if(tab) { // Bind the action to when the tab is selected - const handler = function (e) { + const handler = function(e) + { e.data.div.unbind("click.history"); // Bind on click tap, because we need to update history size // after a rezise happend and history log was not the active tab diff --git a/api/js/etemplate/et2_widget_tabs.ts b/api/js/etemplate/et2_widget_tabs.ts index 5d50aeeb96..b6f24df517 100644 --- a/api/js/etemplate/et2_widget_tabs.ts +++ b/api/js/etemplate/et2_widget_tabs.ts @@ -14,10 +14,9 @@ et2_core_valueWidget; */ -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; -import { et2_DOMWidget } from './et2_core_DOMWidget' -import { et2_valueWidget } from './et2_core_valueWidget' +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_createWidget, et2_register_widget, et2_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_valueWidget} from './et2_core_valueWidget' import {et2_nextmatch} from "./et2_extension_nextmatch"; import {et2_no_init} from "./et2_core_common"; import {et2_IInput, et2_IPrint, et2_IResizeable} from "./et2_core_interfaces"; @@ -26,7 +25,7 @@ import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithD /** * Class which implements the tabbox-tag */ -export class et2_tabbox extends et2_valueWidget implements et2_IInput,et2_IResizeable,et2_IPrint +export class et2_tabbox extends et2_valueWidget implements et2_IInput, et2_IResizeable, et2_IPrint { static readonly _attributes : any = { 'tabs': { @@ -252,15 +251,17 @@ export class et2_tabbox extends et2_valueWidget implements et2_IInput,et2_IResiz */ doLoadingFinished() { - var tab_deferred = jQuery.Deferred(); var promises = []; var tabs = this; // Specially process the selected index so it shows up right away - this._loadTab(this.selected_index,promises); + this._loadTab(this.selected_index, promises); // Avoid reloading if tabs were modified by data - if(this.isInTree() && this.isAttached()) return; + if(this.isInTree() && this.isAttached()) + { + return; + } // Apply parent now, which actually puts into the DOM // This has to be before loading the child, so the dom sub-tree is not @@ -271,20 +272,27 @@ export class et2_tabbox extends et2_valueWidget implements et2_IInput,et2_IResiz // which has special handling this._children[0].loadingFinished(promises); - // Defer parsing & loading of other tabs until later - window.setTimeout(function() { - for (var i = 0; i < tabs.tabData.length; i++) + let tab_deferred = new Promise((resolve, reject) => + { + // Defer parsing & loading of other tabs until later + window.setTimeout(function() { - if (i == tabs.selected_index) continue; - tabs._loadTab(i,promises); - } - jQuery.when.apply(jQuery,promises).then(function() { - tab_deferred.resolve(); - tabs.resetDirty(); - }); - },0); - - return tab_deferred.promise(); + for(var i = 0; i < tabs.tabData.length; i++) + { + if(i == tabs.selected_index) + { + continue; + } + tabs._loadTab(i, promises); + } + Promise.all(promises).then(function() + { + resolve(); + tabs.resetDirty(); + }); + }, 0); + }); + return tab_deferred; } /** diff --git a/api/js/etemplate/et2_widget_template.ts b/api/js/etemplate/et2_widget_template.ts index 122cf02002..09b8fcd525 100644 --- a/api/js/etemplate/et2_widget_template.ts +++ b/api/js/etemplate/et2_widget_template.ts @@ -72,9 +72,9 @@ export class et2_template extends et2_DOMWidget } }; - content: string; - div: HTMLDivElement; - loading: JQueryDeferred; + content : string; + div : HTMLDivElement; + loading : Promise; /** * Constructor @@ -94,66 +94,78 @@ export class et2_template extends et2_DOMWidget this.div = document.createElement("div"); // Deferred object so we can load via AJAX - this.loading = jQuery.Deferred(); - - // run transformAttributes now, to get server-side modifications (url!) - if (_attrs.template) + this.loading = new Promise((resolve, reject) => { - this.id = _attrs.template; - this.transformAttributes(_attrs); - this.options = et2_cloneObject(_attrs); - _attrs = {}; - } - if (this.id != "" || this.options.template) - { - var parts = (this.options.template || this.id).split('?'); - var cache_buster = parts.length > 1 ? parts.pop() : null; - var template_name = parts.pop(); - // Check to see if XML is known - var xml = null; - var templates = etemplate2.templates; // use global eTemplate cache - if(!(xml = templates[template_name])) + // run transformAttributes now, to get server-side modifications (url!) + if(_attrs.template) { - // Check to see if ID is short form --> prepend parent/top-level name - if(template_name.indexOf('.') < 0) + this.id = _attrs.template; + this.transformAttributes(_attrs); + this.options = et2_cloneObject(_attrs); + _attrs = {}; + } + if(this.id != "" || this.options.template) + { + var parts = (this.options.template || this.id).split('?'); + var cache_buster = parts.length > 1 ? parts.pop() : null; + var template_name = parts.pop(); + + // Check to see if XML is known + var xml = null; + var templates = etemplate2.templates; // use global eTemplate cache + if(!(xml = templates[template_name])) { - var root = _parent ? _parent.getRoot() : null; - var top_name = root && root._inst ? root._inst.name : null; - if (top_name && template_name.indexOf('.') < 0) template_name = top_name+'.'+template_name; - } - xml = templates[template_name]; - if(!xml) - { - // Ask server - var url = this.options.url; - if (!this.options.url) + // Check to see if ID is short form --> prepend parent/top-level name + if(template_name.indexOf('.') < 0) { - var splitted = template_name.split('.'); - var app = splitted.shift(); - // use template base url from initial template, to continue using webdav, if that was loaded via webdav - url = this.getRoot()._inst.template_base_url + app + "/templates/default/" + - splitted.join('.')+ ".xet" + (cache_buster ? '?download='+cache_buster : ''); + var root = _parent ? _parent.getRoot() : null; + var top_name = root && root._inst ? root._inst.name : null; + if(top_name && template_name.indexOf('.') < 0) + { + template_name = top_name + '.' + template_name; + } } - // if server did not give a cache-buster, fall back to current time + xml = templates[template_name]; + if(!xml) + { + // Ask server + var url = this.options.url; + if(!this.options.url) + { + var splitted = template_name.split('.'); + var app = splitted.shift(); + // use template base url from initial template, to continue using webdav, if that was loaded via webdav + url = this.getRoot()._inst.template_base_url + app + "/templates/default/" + + splitted.join('.') + ".xet" + (cache_buster ? '?download=' + cache_buster : ''); + } + // if server did not give a cache-buster, fall back to current time if (url.indexOf('?') == -1) url += '?download='+(new Date).valueOf(); if(this.options.url || splitted.length) { - var fetch_url_callback = function(_xmldoc) { + var fetch_url_callback = function(_xmldoc) + { // Scan for templates and store them - for(var i = 0; i < _xmldoc.childNodes.length; i++) { + for(var i = 0; i < _xmldoc.childNodes.length; i++) + { var template = _xmldoc.childNodes[i]; - if(template.nodeName.toLowerCase() != "template") continue; + if(template.nodeName.toLowerCase() != "template") + { + continue; + } templates[template.getAttribute("id")] = template; } // Read the XML structure of the requested template - if (typeof templates[template_name] != 'undefined') this.loadFromXML(templates[template_name]); + if(typeof templates[template_name] != 'undefined') + { + this.loadFromXML(templates[template_name]); + } // Update flag - this.loading.resolve(); - }; + resolve(); + }.bind(this); et2_loadXMLFromURL(url, fetch_url_callback, this, function( error) { url = egw.link('/'+ app + "/templates/default/" + @@ -173,19 +185,20 @@ export class et2_template extends et2_DOMWidget //this.loadingFinished(); // But resolve the promise - this.loading.resolve(); + resolve(); } else { this.egw().debug("warn", "Unable to find XML for ", template_name); - this.loading.reject(); + reject() } - } - else - { - // No actual template - this.loading.resolve(); - } + } + else + { + // No actual template + reject(); + } + }); } /** @@ -252,10 +265,10 @@ export class et2_template extends et2_DOMWidget super.doLoadingFinished(); // Fire load event when done loading - this.loading.done(jQuery.proxy(function() {jQuery(this).trigger("load");},this.div)); + this.loading.then(function() {jQuery(this).trigger("load");}.bind(this.div)); // Not done yet, but widget will let you know - return this.loading.promise(); + return this.loading; } } et2_register_widget(et2_template, ["template"]); diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 5841b19465..a70d0c852d 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -638,24 +638,9 @@ export class etemplate2 { egw.window.console.groupEnd(); } - if(deferred.length > 0) - { - let still_deferred = 0; - jQuery(deferred).each(function() - { - if(this.state() == "pending") - { - still_deferred++; - } - }); - if(still_deferred > 0) - { - egw.debug("log", "Template loaded, waiting for %d/%d deferred to finish...", still_deferred, deferred.length); - } - } // Wait for everything to be loaded, then finish it up - jQuery.when.apply(jQuery, deferred).done(jQuery.proxy(function() + Promise.all(deferred).then(() => { egw.debug("log", "Finished loading %s, triggering load event", _name); @@ -736,7 +721,7 @@ export class etemplate2 gen_time_div.find('.et2RenderTime').remove(); gen_time_div.append('' + egw.lang('eT2 rendering took %1s', '' + ((end_time - start_time) / 1000)) + ''); } - }, this)); + }); };