mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-02 20:19:27 +01:00
Change template to webcomponent
This commit is contained in:
parent
336927f9c8
commit
e107b48c3a
11
api/js/etemplate/Et2Template/Et2Template.md
Normal file
11
api/js/etemplate/Et2Template/Et2Template.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
The template displays a loader while it is loading the file, and is replaced with the actual content once all widgets
|
||||||
|
are ready.
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<style>
|
||||||
|
et2-template {
|
||||||
|
min-height: 5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<et2-template></et2-template>
|
||||||
|
```
|
26
api/js/etemplate/Et2Template/Et2Template.styles.ts
Normal file
26
api/js/etemplate/Et2Template/Et2Template.styles.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {css} from 'lit';
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template--loading {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: var(--sl-panel-background-color);
|
||||||
|
color: var(--application-color, var(--primary-color));
|
||||||
|
|
||||||
|
z-index: var(--sl-z-index-dialog);
|
||||||
|
|
||||||
|
font-size: 5rem;
|
||||||
|
}
|
||||||
|
`;
|
512
api/js/etemplate/Et2Template/Et2Template.ts
Normal file
512
api/js/etemplate/Et2Template/Et2Template.ts
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
/**
|
||||||
|
* EGroupware eTemplate2 - Email WebComponent
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
* @package api
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
*/
|
||||||
|
import {html, LitElement, nothing} from "lit";
|
||||||
|
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||||
|
import shoelace from "../Styles/shoelace";
|
||||||
|
import styles from "./Et2Template.styles";
|
||||||
|
import {property} from "lit/decorators/property.js";
|
||||||
|
import {customElement} from "lit/decorators/custom-element.js";
|
||||||
|
import {etemplate2} from "../etemplate2";
|
||||||
|
import {et2_loadXMLFromURL} from "../et2_core_xml";
|
||||||
|
import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||||
|
import {egw, IegwAppLocal} from "../../jsapi/egw_global";
|
||||||
|
import {until} from "lit/directives/until.js";
|
||||||
|
import {classMap} from "lit/directives/class-map.js";
|
||||||
|
import {et2_arrayMgr} from "../et2_core_arrayMgr";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
/**
|
||||||
|
* @summary Load & populate a template (.xet file)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @event load - Emitted when all elements are loaded
|
||||||
|
*
|
||||||
|
* @csspart template - Wrapper around template content
|
||||||
|
*
|
||||||
|
* @cssproperty [--height=5] - The maximum height of the widget, to limit size when you have a lot of addresses. Set by rows property, when set.
|
||||||
|
*/
|
||||||
|
@customElement("et2-template")
|
||||||
|
export class Et2Template extends Et2Widget(LitElement)
|
||||||
|
{
|
||||||
|
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
shoelace,
|
||||||
|
super.styles,
|
||||||
|
styles
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name / ID of template with optional cache-buster ('?'+filemtime of template on server)
|
||||||
|
* Template can be <app>.<template_file>.<template> form or short <template>.
|
||||||
|
* To use the short form, the file must already be loaded.
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
template : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Url of template
|
||||||
|
* A full URL to load template, including cache-buster ('?'+filemtime of template on server)
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
url : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content index
|
||||||
|
*
|
||||||
|
* Used for passing in specific content to the template other than what it would get by ID.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
content : string;
|
||||||
|
|
||||||
|
protected loading : Promise<void>;
|
||||||
|
private __egw : IegwAppLocal = null;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(egw? : IegwAppLocal)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
if(egw)
|
||||||
|
{
|
||||||
|
this.__egw = egw;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() : void
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener("load", this.handleLoad);
|
||||||
|
|
||||||
|
if(this.template || this.id)
|
||||||
|
{
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() : void
|
||||||
|
{
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener("load", this.handleLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUpdateComplete() : Promise<boolean>
|
||||||
|
{
|
||||||
|
const result = await super.getUpdateComplete();
|
||||||
|
await this.loading;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a DOM widget by id in the tree, descending into the child levels.
|
||||||
|
*
|
||||||
|
* @param _id is the id you're searching for
|
||||||
|
*/
|
||||||
|
getDOMWidgetById(_id) : typeof Et2Widget | null
|
||||||
|
{
|
||||||
|
let widget = this.getWidgetById(_id);
|
||||||
|
if(widget && (widget instanceof HTMLElement || widget.instanceOf(Et2Widget)))
|
||||||
|
{
|
||||||
|
return <typeof Et2Widget>widget;
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a Value widget by id in the tree, descending into the child levels.
|
||||||
|
*
|
||||||
|
* @param _id is the id you're searching for
|
||||||
|
*/
|
||||||
|
getInputWidgetById(_id) : Et2InputWidgetInterface | null
|
||||||
|
{
|
||||||
|
let widget = <any>this.getWidgetById(_id);
|
||||||
|
// instead of checking widget to be an instance of valueWidget (which would create a circular dependency)
|
||||||
|
// we check for the interface/methods of valueWidget
|
||||||
|
if(widget && typeof widget.get_value === 'function' && typeof widget.set_value === 'function')
|
||||||
|
{
|
||||||
|
return <Et2InputWidgetInterface>widget;
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value for a child widget, specified by the given ID
|
||||||
|
*
|
||||||
|
* @param id string The ID you're searching for
|
||||||
|
* @param value Value for the widget
|
||||||
|
*
|
||||||
|
* @return Returns the result of widget's set_value(), though this is usually undefined
|
||||||
|
*
|
||||||
|
* @throws Error If the widget cannot be found or it does not have a set_value() function
|
||||||
|
*/
|
||||||
|
setValueById(id : string, value) : any
|
||||||
|
{
|
||||||
|
let widget = this.getWidgetById(id);
|
||||||
|
if(!widget)
|
||||||
|
{
|
||||||
|
throw 'Could not find widget ' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about what class it is, just that it has the function
|
||||||
|
// @ts-ignore
|
||||||
|
if(typeof widget.set_value !== 'function')
|
||||||
|
{
|
||||||
|
throw 'Widget ' + id + ' does not have a set_value() function';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return widget.set_value(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current value of a child widget, specified by the given ID
|
||||||
|
*
|
||||||
|
* This is the current value of the widget, which may be different from the original value given in content
|
||||||
|
*
|
||||||
|
* @param id string The ID you're searching for
|
||||||
|
* @throws Error If the widget cannot be found or it does not have a set_value() function
|
||||||
|
*/
|
||||||
|
getValueById(id : string)
|
||||||
|
{
|
||||||
|
let widget = this.getWidgetById(id);
|
||||||
|
if(!widget)
|
||||||
|
{
|
||||||
|
throw 'Could not find widget ' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about what class it is, just that it has the function
|
||||||
|
// @ts-ignore
|
||||||
|
if(typeof widget.get_value !== 'function' && typeof widget.value == "undefined")
|
||||||
|
{
|
||||||
|
throw 'Widget ' + id + ' does not have a get_value() function';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return typeof widget.get_value == "function" ? widget.get_value() : widget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value for a child widget, specified by the given ID
|
||||||
|
*
|
||||||
|
* @param id string The ID you're searching for
|
||||||
|
* @throws Error If the widget cannot be found, or it does not have a set_value() function
|
||||||
|
*/
|
||||||
|
setDisabledById(id : string, value : boolean)
|
||||||
|
{
|
||||||
|
let widget = this.getWidgetById(id);
|
||||||
|
if(!widget)
|
||||||
|
{
|
||||||
|
throw 'Could not find widget ' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about what class it is, just that it has the function
|
||||||
|
// @ts-ignore
|
||||||
|
if(typeof widget.set_disabled !== 'function')
|
||||||
|
{
|
||||||
|
throw 'Widget ' + id + ' does not have a set_disabled() function';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return widget.set_disabled(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public egw() : IegwAppLocal
|
||||||
|
{
|
||||||
|
if(this.__egw)
|
||||||
|
{
|
||||||
|
return this.__egw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return super.egw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the template XML and create widgets from it
|
||||||
|
*
|
||||||
|
* Asks the server if we don't have that template on the client yet, then takes the template
|
||||||
|
* node and goes through it, creating widgets.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async load(newContent? : object)
|
||||||
|
{
|
||||||
|
if(typeof newContent != "undefined")
|
||||||
|
{
|
||||||
|
this.setArrayMgr("content", new et2_arrayMgr(newContent));
|
||||||
|
}
|
||||||
|
this.loading = new Promise(async(resolve, reject) =>
|
||||||
|
{
|
||||||
|
let xml = await this.findTemplate();
|
||||||
|
// Read the XML structure of the requested template
|
||||||
|
if(typeof xml != 'undefined')
|
||||||
|
{
|
||||||
|
console.time("loadFromXML");
|
||||||
|
this.loadFromXML(xml);
|
||||||
|
console.timeEnd("loadFromXML");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject("Could not find template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for widgets to be complete
|
||||||
|
await this.loadFinished();
|
||||||
|
if(egw.debug_level() >= 4 && console.timeStamp)
|
||||||
|
{
|
||||||
|
console.timeEnd("Template load");
|
||||||
|
}
|
||||||
|
console.groupEnd();
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("load", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: this
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
return this.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the template XML node, either from the local cache or the server
|
||||||
|
*
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async findTemplate() : Promise<Node>
|
||||||
|
{
|
||||||
|
// 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`
|
||||||
|
<sl-spinner></sl-spinner>`;
|
||||||
|
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="template--loading">${loading}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<div
|
||||||
|
part="base"
|
||||||
|
class=${classMap(classes)}
|
||||||
|
>
|
||||||
|
${until(this.loading.then(() => nothing), this.loadingTemplate())}
|
||||||
|
<slot></slot>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
}
|
@ -12,10 +12,10 @@ import {loadWebComponent} from "../../Et2Widget/Et2Widget";
|
|||||||
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "../../et2_core_xml";
|
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "../../et2_core_xml";
|
||||||
import {css, PropertyValues} from "lit";
|
import {css, PropertyValues} from "lit";
|
||||||
import shoelace from "../../Styles/shoelace";
|
import shoelace from "../../Styles/shoelace";
|
||||||
import {et2_createWidget} from "../../et2_core_widget";
|
|
||||||
import {colorsDefStyles} from "../../Styles/colorsDefStyles";
|
import {colorsDefStyles} from "../../Styles/colorsDefStyles";
|
||||||
import {Et2InputWidget} from "../../Et2InputWidget/Et2InputWidget";
|
import {Et2InputWidget} from "../../Et2InputWidget/Et2InputWidget";
|
||||||
import {et2_IResizeable} from "../../et2_core_interfaces";
|
import {et2_IResizeable} from "../../et2_core_interfaces";
|
||||||
|
import {Et2Template} from "../../Et2Template/Et2Template";
|
||||||
|
|
||||||
|
|
||||||
export class Et2Tabs extends Et2InputWidget(SlTabGroup) implements et2_IResizeable
|
export class Et2Tabs extends Et2InputWidget(SlTabGroup) implements et2_IResizeable
|
||||||
@ -456,7 +456,7 @@ export class Et2Tabs extends Et2InputWidget(SlTabGroup) implements et2_IResizeab
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
et2_createWidget('template', tab.widget_options, tab.contentDiv);
|
<Et2Template>loadWebComponent('et2-template', tab.widget_options, tab.contentDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tab.contentDiv;
|
return tab.contentDiv;
|
||||||
|
@ -8,266 +8,8 @@
|
|||||||
* @author Andreas Stöckel
|
* @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,
|
* @deprecated use Et2Template
|
||||||
* 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 class et2_template extends et2_DOMWidget
|
export type et2_template = Et2Template;
|
||||||
{
|
|
||||||
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<any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<any>((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"]);
|
|
@ -86,6 +86,7 @@ import './Et2Select/Tag/Et2ThumbnailTag';
|
|||||||
import './Et2Spinner/Et2Spinner';
|
import './Et2Spinner/Et2Spinner';
|
||||||
import './Et2Switch/Et2Switch';
|
import './Et2Switch/Et2Switch';
|
||||||
import './Et2Switch/Et2SwitchIcon';
|
import './Et2Switch/Et2SwitchIcon';
|
||||||
|
import './Et2Template/Et2Template';
|
||||||
import './Et2Textarea/Et2Textarea';
|
import './Et2Textarea/Et2Textarea';
|
||||||
import './Et2Textarea/Et2TextareaReadonly';
|
import './Et2Textarea/Et2TextareaReadonly';
|
||||||
import './Et2Textbox/Et2Textbox';
|
import './Et2Textbox/Et2Textbox';
|
||||||
@ -156,6 +157,7 @@ import './et2_extension_nextmatch';
|
|||||||
import './et2_extension_customfields';
|
import './et2_extension_customfields';
|
||||||
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
|
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
|
||||||
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
|
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
|
||||||
|
import {Et2Template} from "./Et2Template/Et2Template";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,7 +185,7 @@ export class etemplate2
|
|||||||
private uniqueId : void | string;
|
private uniqueId : void | string;
|
||||||
private template_base_url : string;
|
private template_base_url : string;
|
||||||
|
|
||||||
private _widgetContainer : et2_container;
|
private _widgetContainer : Et2Template;
|
||||||
private _DOMContainer : HTMLElement;
|
private _DOMContainer : HTMLElement;
|
||||||
|
|
||||||
private resize_timeout : number | boolean;
|
private resize_timeout : number | boolean;
|
||||||
@ -216,7 +218,6 @@ export class etemplate2
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Preset the object variable
|
* Preset the object variable
|
||||||
* @type {et2_container}
|
|
||||||
*/
|
*/
|
||||||
this._widgetContainer = null;
|
this._widgetContainer = null;
|
||||||
|
|
||||||
@ -675,10 +676,17 @@ export class etemplate2
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the basic widget container and attach it to the DOM
|
// Create the basic widget container and attach it to the DOM
|
||||||
this._widgetContainer = new et2_container(null);
|
this._widgetContainer = new Et2Template(egw(currentapp, egw.elemWindow(this._DOMContainer)));
|
||||||
this._widgetContainer.setApiInstance(egw(currentapp, egw.elemWindow(this._DOMContainer)));
|
|
||||||
this._widgetContainer.setInstanceManager(this);
|
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
|
// store the id to submit it back to server
|
||||||
if(_data)
|
if(_data)
|
||||||
@ -696,14 +704,6 @@ export class etemplate2
|
|||||||
|
|
||||||
const _load = function()
|
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,
|
// Add into indexed list - do this before, so anything looking can find it,
|
||||||
// even if it's not loaded
|
// even if it's not loaded
|
||||||
if(typeof etemplate2._byTemplate[_name] == "undefined")
|
if(typeof etemplate2._byTemplate[_name] == "undefined")
|
||||||
@ -718,16 +718,6 @@ export class etemplate2
|
|||||||
this.DOMContainer.setAttribute("slot", etemplate2.templates[this.name].getAttribute("slot"));
|
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
|
// Connect to the window resize event
|
||||||
jQuery(window).on("resize." + this.uniqueId, this, function(e)
|
jQuery(window).on("resize." + this.uniqueId, this, function(e)
|
||||||
{
|
{
|
||||||
@ -747,27 +737,8 @@ export class etemplate2
|
|||||||
// to run.
|
// to run.
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
Promise.race([Promise.all(deferred),
|
this._widgetContainer.updateComplete.then(() =>
|
||||||
// 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
|
// Clear dirty now that it's all loaded
|
||||||
this.widgetContainer.iterateOver((_widget) =>
|
this.widgetContainer.iterateOver((_widget) =>
|
||||||
{
|
{
|
||||||
@ -784,7 +755,7 @@ export class etemplate2
|
|||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
// Automatically set focus to first visible input for popups
|
// 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();
|
this.focusOnFirstInput();
|
||||||
}
|
}
|
||||||
@ -855,9 +826,6 @@ export class etemplate2
|
|||||||
{
|
{
|
||||||
if(etemplate2.templates[_name])
|
if(etemplate2.templates[_name])
|
||||||
{
|
{
|
||||||
// Set array managers first, or errors will happen
|
|
||||||
this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
|
|
||||||
|
|
||||||
// Already have it
|
// Already have it
|
||||||
_load.apply(this, []);
|
_load.apply(this, []);
|
||||||
return;
|
return;
|
||||||
@ -876,14 +844,10 @@ export class etemplate2
|
|||||||
throw e;
|
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
|
// Asynchronously load the XET file
|
||||||
return et2_loadXMLFromURL(_url, function(_xmldoc)
|
return et2_loadXMLFromURL(_url, function(_xmldoc)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Scan for templates and store them
|
// Scan for templates and store them
|
||||||
for(let i = 0; i < _xmldoc.childNodes.length; i++)
|
for(let i = 0; i < _xmldoc.childNodes.length; i++)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user