+ {
+ // 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 46b1be0240..c97b40c7dd 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
{
- et2_createWidget('template', tab.widget_options, tab.contentDiv);
+ loadWebComponent('et2-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 c2825fdf29..e1b9ea00f9 100644
--- a/api/js/etemplate/et2_widget_template.ts
+++ b/api/js/etemplate/et2_widget_template.ts
@@ -8,266 +8,8 @@
* @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";
/**
- * 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.
+ * @deprecated use Et2Template
*/
-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
+export type et2_template = Et2Template;
\ No newline at end of file
diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts
index e06be19314..bd37b0f27b 100644
--- a/api/js/etemplate/etemplate2.ts
+++ b/api/js/etemplate/etemplate2.ts
@@ -86,6 +86,7 @@ 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';
@@ -156,6 +157,7 @@ import './et2_extension_nextmatch';
import './et2_extension_customfields';
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
+import {Et2Template} from "./Et2Template/Et2Template";
/**
@@ -183,7 +185,7 @@ export class etemplate2
private uniqueId : void | string;
private template_base_url : string;
- private _widgetContainer : et2_container;
+ private _widgetContainer : Et2Template;
private _DOMContainer : HTMLElement;
private resize_timeout : number | boolean;
@@ -216,7 +218,6 @@ export class etemplate2
/**
* Preset the object variable
- * @type {et2_container}
*/
this._widgetContainer = null;
@@ -675,10 +676,17 @@ export class etemplate2
}
// Create the basic widget container and attach it to the DOM
- this._widgetContainer = new et2_container(null);
- this._widgetContainer.setApiInstance(egw(currentapp, egw.elemWindow(this._DOMContainer)));
+ this._widgetContainer = new Et2Template(egw(currentapp, egw.elemWindow(this._DOMContainer)));
this._widgetContainer.setInstanceManager(this);
- this._widgetContainer.setParentDOMNode(this._DOMContainer);
+ 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);
// store the id to submit it back to server
if(_data)
@@ -696,14 +704,6 @@ 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,16 +718,6 @@ 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)
{
@@ -747,27 +737,8 @@ export class etemplate2
// to run.
setTimeout(() =>
{
- 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(() =>
+ this._widgetContainer.updateComplete.then(() =>
{
-
- console.timeEnd("deferred");
- console.timeStamp("Deferred done");
// Clear dirty now that it's all loaded
this.widgetContainer.iterateOver((_widget) =>
{
@@ -784,7 +755,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();
}
@@ -855,9 +826,6 @@ 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;
@@ -876,14 +844,10 @@ 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++)
{