- {
- // Find template name
- const parts = (this.template || this.id).split('?');
- const cache_buster = parts.length > 1 ? parts.pop() : null;
- let template_name = parts.pop();
-
- console.groupCollapsed("Loading template " + template_name);
- if(egw.debug_level() >= 4 && console.timeStamp)
- {
- console.timeStamp("Begin rendering template");
- console.time("Template load");
- }
-
- // Check to see if the template is already known / loaded into global ETemplate cache
- let xml = etemplate2.templates[template_name];
-
- // Check to see if ID is short form --> prepend parent/top-level name
- if(!xml && template_name.indexOf('.') < 0)
- {
- const root = this.getRoot();
- const top_name = root && root.getInstanceManager() ? root.getInstanceManager().name : null;
- if(top_name && template_name.indexOf('.') < 0)
- {
- template_name = top_name + '.' + template_name
- xml = etemplate2.templates[template_name];
- }
- }
-
- // Ask the server for the template
- if(!xml)
- {
- let templates = await et2_loadXMLFromURL(this.getUrl(), null, this, this.loadFailed);
-
- // Scan the file for templates and store them
- let fallback;
- for(let i = 0; i < templates.childNodes.length; i++)
- {
- const template = templates.childNodes[i];
- if(template.nodeName.toLowerCase() != "template")
- {
- continue;
- }
- etemplate2.templates[template.getAttribute("id")] = template;
- if(template.getAttribute("id") == template_name)
- {
- xml = template;
- }
- fallback = template;
- }
- // Take last template in the file if we had no name
- if(!xml && !template_name)
- {
- xml = fallback;
- }
- }
- return xml;
- }
-
- /**
- * The template has been loaded, wait for child widgets to be complete.
- *
- * For webComponents, we wait for the widget's updateComplete.
- * For legacy widgets, we let them finish and wait for their doLoadingFinished Promise
- *
- * @protected
- */
- protected loadFinished()
- {
- console.time("loadFinished");
- // List of Promises from widgets that are not quite fully loaded
- let deferred = [];
-
- // Inform the widget tree that it has been successfully loaded.
- super.loadingFinished(deferred);
-
- // Don't wait for ourselves, it will never happen
- deferred = deferred.filter((d) => { return d.widget !== this});
-
- let ready = false;
-
- // Wait for everything to be loaded, then finish it up. Use timeout to give anything else a chance
- // to run.
- return Promise.race([
- Promise.all(deferred).then(() => ready = true),
- // If loading takes too long, give some feedback so we can try to track down why
- new Promise((resolve) =>
- {
- setTimeout(() =>
- {
- if(ready)
- {
- return;
- }
- this.loadFailed("Load timeout");
- console.debug("This is the deferred widget list. Look for widgets still pending to find the problem", deferred);
- resolve();
- }, 10000
- );
- })
- ]).then(() =>
- {
- console.timeEnd("loadFinished");
- });
- }
-
- loadFailed(reason? : any)
- {
- this.egw().debug("error", "Loading failed '" + (this.template ?? this.id) + "' @ " + this.getUrl() + (reason ? " \n" + reason : ""));
- }
-
- protected getUrl()
- {
- if(this.url)
- {
- return this.url;
- }
-
- const parts = (this.template || this.id).split('?');
- const cache_buster = parts.length > 1 ? parts.pop() : null;
- let template_name = parts.pop();
-
- const splitted = template_name.split('.');
- const app = splitted.shift();
- let url = this.egw().link(
- '/' + app + "/templates/default/" + splitted.join('.') + ".xet",
- {download: cache_buster ? cache_buster : (new Date).valueOf()}
- );
-
- // if we have no cache-buster, reload daily
- if(url.indexOf('?') === -1)
- {
- url += '?download=' + ((new Date).valueOf() / 86400 | 0).toString();
- }
- return url;
- }
-
- public get app()
- {
- const parts = (this.template || this.id).split('?');
- const cache_buster = parts.length > 1 ? parts.pop() : null;
- let template_name = parts.pop();
-
- const splitted = template_name.split('.');
- return splitted.shift() || "";
- }
-
- /**
- * Override parent to support content attribute
- * Templates always have ID set, but seldom do we want them to
- * create a namespace based on their ID.
- */
- checkCreateNamespace()
- {
- if(this.content)
- {
- const old_id = this.id;
- this.id = this.content;
- super.checkCreateNamespace.apply(this, arguments);
- this.id = old_id;
- }
- }
-
- _createNamespace() : boolean
- {
- return true;
- }
-
- handleLoad(event)
- {
- if(this.onload && typeof this.onload == "function")
- {
- // Make sure function gets a reference to the widget
- let args = Array.prototype.slice.call(arguments);
- if(args.indexOf(this) == -1)
- {
- args.push(this);
- }
-
- return this.onload.apply(this, args);
- }
- }
-
- loadingTemplate()
- {
- let loading = html`
- `;
-
-
- return html`
- ${loading}
`;
- }
-
- render()
- {
- const classes = {
- template: true,
- 'template--disabled': this.disabled,
- 'template--readonly': this.readonly
- };
- if(this.app)
- {
- classes["template--app-" + this.app] = true;
- }
- if(this.layout != "none")
- {
- classes["layout-" + this.layout] = true;
- classes["template--layout-" + this.layout] = true;
- }
- return html`
-
- ${until(this.loading.then(() => nothing), this.loadingTemplate())}
-
-
`
- }
-}
\ No newline at end of file
diff --git a/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts b/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts
index c97b40c7dd..46b1be0240 100644
--- a/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts
+++ b/api/js/etemplate/Layout/Et2Tabs/Et2Tabs.ts
@@ -12,10 +12,10 @@ import {loadWebComponent} from "../../Et2Widget/Et2Widget";
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "../../et2_core_xml";
import {css, PropertyValues} from "lit";
import shoelace from "../../Styles/shoelace";
+import {et2_createWidget} from "../../et2_core_widget";
import {colorsDefStyles} from "../../Styles/colorsDefStyles";
import {Et2InputWidget} from "../../Et2InputWidget/Et2InputWidget";
import {et2_IResizeable} from "../../et2_core_interfaces";
-import {Et2Template} from "../../Et2Template/Et2Template";
export class Et2Tabs extends Et2InputWidget(SlTabGroup) implements et2_IResizeable
@@ -456,7 +456,7 @@ export class Et2Tabs extends Et2InputWidget(SlTabGroup) implements et2_IResizeab
}
else
{
- loadWebComponent('et2-template', tab.widget_options, tab.contentDiv);
+ et2_createWidget('template', tab.widget_options, tab.contentDiv);
}
return tab.contentDiv;
diff --git a/api/js/etemplate/et2_widget_template.ts b/api/js/etemplate/et2_widget_template.ts
index e1b9ea00f9..c2825fdf29 100644
--- a/api/js/etemplate/et2_widget_template.ts
+++ b/api/js/etemplate/et2_widget_template.ts
@@ -8,8 +8,266 @@
* @author Andreas Stöckel
*/
+/*egw:uses
+ et2_core_xml;
+ et2_core_DOMWidget;
+*/
+
+import './et2_core_interfaces';
+import {et2_DOMWidget} from './et2_core_DOMWidget';
+import {ClassWithAttributes} from "./et2_core_inheritance";
+import {et2_register_widget, WidgetConfig} from "./et2_core_widget";
+import {etemplate2} from "./etemplate2";
+import {et2_cloneObject, et2_no_init} from "./et2_core_common";
+import {et2_loadXMLFromURL} from "./et2_core_xml";
+import {egw} from "../jsapi/egw_global";
/**
- * @deprecated use Et2Template
+ * Class which implements the "template" XET-Tag. When the id parameter is set,
+ * the template class checks whether another template with this id already
+ * exists. If yes, this template is removed from the DOM tree, copied and
+ * inserted in place of this template.
*/
-export type et2_template = Et2Template;
\ No newline at end of file
+export class et2_template extends et2_DOMWidget
+{
+ static readonly _attributes : any = {
+ "template": {
+ "name": "Template",
+ "type": "string",
+ "description": "Name / ID of template with optional cache-buster ('?'+filemtime of template on server)",
+ "default": et2_no_init
+ },
+ "group": {
+ // TODO: Not implemented
+ "name": "Group",
+ "description":"Not implemented",
+ //"default": 0
+ "default": et2_no_init
+ },
+ "version": {
+ "name": "Version",
+ "type": "string",
+ "description": "Version of the template"
+ },
+ "lang": {
+ "name": "Language",
+ "type": "string",
+ "description": "Language the template is written in"
+ },
+ "content": {
+ "name": "Content index",
+ "default": et2_no_init,
+ "description": "Used for passing in specific content to the template other than what it would get by ID."
+ },
+ url: {
+ name: "URL of template",
+ type: "string",
+ description: "full URL to load template incl. cache-buster"
+ },
+ "onload": {
+ "name": "onload",
+ "type": "js",
+ "default": et2_no_init,
+ "description": "JS code which is executed after the template is loaded."
+ }
+ };
+
+ content : string;
+ div : HTMLDivElement;
+ loading : Promise;
+
+ /**
+ * Constructor
+ */
+ constructor(_parent, _attrs? : WidgetConfig, _child? : object)
+ {
+ // Call the inherited constructor
+ super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_template._attributes, _child || {}));
+
+ // Set this early, so it's available for creating namespace
+ if(_attrs.content)
+ {
+ this.content = _attrs.content;
+ }
+ // constructor was called here before!
+
+ this.div = document.createElement("div");
+
+ // Deferred object so we can load via AJAX
+ this.loading = new Promise((resolve, reject) =>
+ {
+
+ // run transformAttributes now, to get server-side modifications (url!)
+ if(_attrs.template)
+ {
+ this.id = _attrs.template;
+ this.transformAttributes(_attrs);
+ this.options = et2_cloneObject(_attrs);
+ _attrs = {};
+ }
+ if((this.id != "" || this.options.template) && !this.options.disabled)
+ {
+ 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]))
+ {
+ // Check to see if ID is short form --> prepend parent/top-level name
+ if(template_name.indexOf('.') < 0)
+ {
+ 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)
+ {
+ var splitted = template_name.split('.');
+ var app = splitted.shift();
+ url = egw.link('/'+ app + "/templates/default/" +
+ splitted.join('.')+ ".xet", {download:cache_buster? cache_buster :(new Date).valueOf()});
+ }
+ // 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)
+ {
+ // Scan for templates and store them
+ for(var i = 0; i < _xmldoc.childNodes.length; i++)
+ {
+ var template = _xmldoc.childNodes[i];
+ 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]);
+ }
+
+ // Update flag
+ resolve();
+ }.bind(this);
+
+ et2_loadXMLFromURL(url, fetch_url_callback, this, function( error) {
+ url = egw.link('/'+ app + "/templates/default/" +
+ splitted.join('.')+ ".xet", {download:cache_buster? cache_buster :(new Date).valueOf()});
+
+ et2_loadXMLFromURL(url, fetch_url_callback, this);
+ });
+ }
+ return;
+ }
+ }
+ if(xml !== null && typeof xml !== "undefined")
+ {
+ this.egw().debug("log", "Loading template from XML: ", template_name);
+ this.loadFromXML(xml);
+ // Don't call this here - done by caller, or on whole widget tree
+ //this.loadingFinished();
+
+ // But resolve the promise
+ resolve();
+ }
+ else
+ {
+ this.egw().debug("warn", "Unable to find XML for ", template_name);
+ reject("Unable to find XML for " + template_name);
+ }
+ }
+ else
+ {
+ // No actual template - not an error, just nothing to do
+ resolve();
+ }
+ });
+ }
+
+ /**
+ * Override parent to support content attribute
+ * Templates always have ID set, but seldom do we want them to
+ * create a namespace based on their ID.
+ */
+ checkCreateNamespace(_attrs)
+ {
+ if(_attrs.content)
+ {
+ var old_id = _attrs.id;
+ this.id = _attrs.content;
+ super.checkCreateNamespace.apply(this, arguments);
+ this.id = old_id;
+ }
+ }
+
+ _createNamespace() : boolean
+ {
+ return true;
+ }
+
+ getDOMNode()
+ {
+ return this.div;
+ }
+
+ attachToDOM()
+ {
+ if (this.div)
+ {
+ jQuery(this.div)
+ .off('.et2_template')
+ .bind("load.et2_template", this, function(e) {
+ e.data.load.call(e.data, this);
+ });
+ }
+
+ return super.attachToDOM();
+ }
+
+ /**
+ * Called after the template is fully loaded to handle any onload handlers
+ */
+ load()
+ {
+ if(typeof this.options.onload == 'function')
+ {
+ // Make sure function gets a reference to the widget
+ var args = Array.prototype.slice.call(arguments);
+ if(args.indexOf(this) == -1) args.push(this);
+
+ return this.options.onload.apply(this, args);
+ }
+ }
+
+ /**
+ * Override to return the promise for deferred loading
+ */
+ doLoadingFinished()
+ {
+ // Apply parent now, which actually puts into the DOM
+ super.doLoadingFinished();
+
+ // Fire load event when done loading
+ this.loading.then(function() {jQuery(this).trigger("load");}.bind(this.div));
+
+ // Not done yet, but widget will let you know
+ return this.loading;
+ }
+}
+et2_register_widget(et2_template, ["template"]);
\ No newline at end of file
diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts
index bd37b0f27b..e06be19314 100644
--- a/api/js/etemplate/etemplate2.ts
+++ b/api/js/etemplate/etemplate2.ts
@@ -86,7 +86,6 @@ import './Et2Select/Tag/Et2ThumbnailTag';
import './Et2Spinner/Et2Spinner';
import './Et2Switch/Et2Switch';
import './Et2Switch/Et2SwitchIcon';
-import './Et2Template/Et2Template';
import './Et2Textarea/Et2Textarea';
import './Et2Textarea/Et2TextareaReadonly';
import './Et2Textbox/Et2Textbox';
@@ -157,7 +156,6 @@ import './et2_extension_nextmatch';
import './et2_extension_customfields';
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
-import {Et2Template} from "./Et2Template/Et2Template";
/**
@@ -185,7 +183,7 @@ export class etemplate2
private uniqueId : void | string;
private template_base_url : string;
- private _widgetContainer : Et2Template;
+ private _widgetContainer : et2_container;
private _DOMContainer : HTMLElement;
private resize_timeout : number | boolean;
@@ -218,6 +216,7 @@ export class etemplate2
/**
* Preset the object variable
+ * @type {et2_container}
*/
this._widgetContainer = null;
@@ -676,17 +675,10 @@ export class etemplate2
}
// Create the basic widget container and attach it to the DOM
- this._widgetContainer = new Et2Template(egw(currentapp, egw.elemWindow(this._DOMContainer)));
+ this._widgetContainer = new et2_container(null);
+ this._widgetContainer.setApiInstance(egw(currentapp, egw.elemWindow(this._DOMContainer)));
this._widgetContainer.setInstanceManager(this);
- this._widgetContainer.template = this.name;
- if(_url)
- {
- this._widgetContainer.url = _url;
- }
- // Set array managers first, or errors will happen
- this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
- // Template starts loading when added
- this.DOMContainer.append(this._widgetContainer);
+ this._widgetContainer.setParentDOMNode(this._DOMContainer);
// store the id to submit it back to server
if(_data)
@@ -704,6 +696,14 @@ export class etemplate2
const _load = function()
{
+ egw.debug("log", "Loading template...");
+ if(egw.debug_level() >= 4 && console.timeStamp)
+ {
+ console.timeStamp("Begin rendering template");
+ console.time("Template load");
+ console.time("loadFromXML");
+ }
+
// Add into indexed list - do this before, so anything looking can find it,
// even if it's not loaded
if(typeof etemplate2._byTemplate[_name] == "undefined")
@@ -718,6 +718,16 @@ export class etemplate2
this.DOMContainer.setAttribute("slot", etemplate2.templates[this.name].getAttribute("slot"));
}
+ this._widgetContainer.loadFromXML(etemplate2.templates[this.name]);
+ console.timeEnd("loadFromXML");
+ console.time("deferred");
+
+ // List of Promises from widgets that are not quite fully loaded
+ const deferred = [];
+
+ // Inform the widget tree that it has been successfully loaded.
+ this._widgetContainer.loadingFinished(deferred);
+
// Connect to the window resize event
jQuery(window).on("resize." + this.uniqueId, this, function(e)
{
@@ -737,8 +747,27 @@ export class etemplate2
// to run.
setTimeout(() =>
{
- this._widgetContainer.updateComplete.then(() =>
+ Promise.race([Promise.all(deferred),
+ // If loading takes too long, give some feedback so we can try to track down why
+ new Promise((resolve) =>
+ {
+ setTimeout(() =>
+ {
+ if(this.ready)
+ {
+ return;
+ }
+ egw.debug("error", "Loading timeout");
+ console.debug("Deferred widget list, look for widgets still pending.", deferred);
+ resolve()
+ }, 10000
+ );
+ })
+ ]).then(() =>
{
+
+ console.timeEnd("deferred");
+ console.timeStamp("Deferred done");
// Clear dirty now that it's all loaded
this.widgetContainer.iterateOver((_widget) =>
{
@@ -755,7 +784,7 @@ export class etemplate2
this.resize();
// Automatically set focus to first visible input for popups
- if(this._widgetContainer.egw().is_popup() && jQuery('[autofocus]', this._DOMContainer).focus().length == 0)
+ if(this._widgetContainer._egw.is_popup() && jQuery('[autofocus]', this._DOMContainer).focus().length == 0)
{
this.focusOnFirstInput();
}
@@ -826,6 +855,9 @@ export class etemplate2
{
if(etemplate2.templates[_name])
{
+ // Set array managers first, or errors will happen
+ this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
+
// Already have it
_load.apply(this, []);
return;
@@ -844,10 +876,14 @@ export class etemplate2
throw e;
}
}
+ // Split the given data into array manager objects and pass those to the
+ // widget container - do this here because file is loaded async
+ this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
// Asynchronously load the XET file
return et2_loadXMLFromURL(_url, function(_xmldoc)
{
+
// Scan for templates and store them
for(let i = 0; i < _xmldoc.childNodes.length; i++)
{