mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-31 02:21:28 +01:00
Change et2_template to Et2Template webComponent (#169)
* Change template to webcomponent
This commit is contained in:
parent
e5b6a8edc3
commit
d86d26cc24
@ -17,7 +17,7 @@ const ADD_ET2_PREFIX_REGEXP = '#<((/?)([vh]?box)|vfs-select)(/?|\s[^>]*)>#m';
|
|||||||
const ADD_ET2_PREFIX_LAST_GROUP = 4;
|
const ADD_ET2_PREFIX_LAST_GROUP = 4;
|
||||||
|
|
||||||
// unconditional of legacy add et2- prefix to this widgets
|
// unconditional of legacy add et2- prefix to this widgets
|
||||||
const ADD_ET2_PREFIX_LEGACY_REGEXP = '#<((/?)(tabbox|description|searchbox|textbox|label|avatar|lavatar|image|appicon|colorpicker|checkbox|url(-email|-phone|-fax)?|vfs-mime|vfs-uid|vfs-gid|vfs-select|vfs-name|link|link-[a-z]+|favorites))(/?|\s[^>]*)>#m';
|
const ADD_ET2_PREFIX_LEGACY_REGEXP = '#<((/?)(template|tabbox|description|searchbox|textbox|label|avatar|lavatar|image|appicon|colorpicker|checkbox|url(-email|-phone|-fax)?|vfs-mime|vfs-uid|vfs-gid|vfs-select|vfs-name|link|link-[a-z]+|favorites))(/?|\s[^>]*)>#m';
|
||||||
const ADD_ET2_PREFIX_LEGACY_LAST_GROUP = 5;
|
const ADD_ET2_PREFIX_LEGACY_LAST_GROUP = 5;
|
||||||
|
|
||||||
// switch evtl. set output-compression off, as we can't calculate a Content-Length header with transparent compression
|
// switch evtl. set output-compression off, as we can't calculate a Content-Length header with transparent compression
|
||||||
|
81
api/js/egw_action/EgwEt2WidgetObject.ts
Normal file
81
api/js/egw_action/EgwEt2WidgetObject.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import {EgwActionObjectInterface} from "./EgwActionObjectInterface";
|
||||||
|
import {Element} from "parse5";
|
||||||
|
import {Function} from "estree";
|
||||||
|
import {EGW_AO_STATE_NORMAL, EGW_AO_STATE_VISIBLE} from "./egw_action_constants";
|
||||||
|
import {Et2Widget} from "../etemplate/Et2Widget/Et2Widget";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic interface object so any webComponent can participate in action system.
|
||||||
|
* This interface can be extended if special handling is needed, but it should work
|
||||||
|
* for any widget.
|
||||||
|
*/
|
||||||
|
export class EgwEt2WidgetObject implements EgwActionObjectInterface
|
||||||
|
{
|
||||||
|
node = null;
|
||||||
|
|
||||||
|
constructor(node)
|
||||||
|
{
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
_state : number = EGW_AO_STATE_NORMAL || EGW_AO_STATE_VISIBLE;
|
||||||
|
handlers : { [p : string] : any };
|
||||||
|
|
||||||
|
reconnectActionsCallback(p0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectActionsContext : any;
|
||||||
|
|
||||||
|
stateChangeCallback(p0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
stateChangeContext : any;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
getDOMNode() : Element
|
||||||
|
{
|
||||||
|
return this.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWidget() : typeof Et2Widget
|
||||||
|
{
|
||||||
|
return this.node
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() : number
|
||||||
|
{
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeVisible() : void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectActions() : void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
setReconnectActionsCallback(_callback : Function, _context : any) : void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(_state : any) : void
|
||||||
|
{
|
||||||
|
this._state = _state
|
||||||
|
}
|
||||||
|
|
||||||
|
setStateChangeCallback(_callback : Function, _context : any) : void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvent(_event : any, _data : any) : boolean
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState(_stateBit : number, _set : boolean, _shiftState : boolean) : void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -746,7 +746,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
|||||||
}
|
}
|
||||||
this._template_widget = new etemplate2(this._contentNode);
|
this._template_widget = new etemplate2(this._contentNode);
|
||||||
|
|
||||||
// Fire an event so consumers can do their thing - etemplate will fire its own load event when its done
|
// Fire an event so consumers can do their thing - etemplate will fire its own load event when it's done
|
||||||
if(!this.dispatchEvent(new CustomEvent("before-load", {
|
if(!this.dispatchEvent(new CustomEvent("before-load", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
@ -756,38 +756,14 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.__template.indexOf('.xet') > 0)
|
this._template_widget.load(this.__template, '', this.__value || {},
|
||||||
{
|
|
||||||
let template = this.__template;
|
|
||||||
// inject preprocessor, if not already in template-url
|
|
||||||
const webserverUrl = this.egw().webserverUrl;
|
|
||||||
if(!template.match(new RegExp(webserverUrl + '/api/etemplate.php')))
|
|
||||||
{
|
|
||||||
template = template.replace(new RegExp(webserverUrl), webserverUrl + '/api/etemplate.php');
|
|
||||||
}
|
|
||||||
// if we have no cache-buster, reload daily
|
|
||||||
if(template.indexOf('?') === -1)
|
|
||||||
{
|
|
||||||
template += '?' + ((new Date).valueOf() / 86400 | 0).toString();
|
|
||||||
}
|
|
||||||
// File name provided, fetch from server
|
|
||||||
this._template_widget.load("", template, this.__value || {content: {}},)
|
|
||||||
.then(() =>
|
|
||||||
{
|
|
||||||
this._templateResolver(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Just template name, it better be loaded already
|
|
||||||
this._template_widget.load(this.__template, '', this.__value || {},
|
|
||||||
// true: do NOT call et2_ready, as it would overwrite this.et2 in app.js
|
// true: do NOT call et2_ready, as it would overwrite this.et2 in app.js
|
||||||
undefined, undefined, true)
|
undefined, undefined, true)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
this._templateResolver(true);
|
this._templateResolver(true);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Don't let dialog closing destroy the parent session
|
// Don't let dialog closing destroy the parent session
|
||||||
if(this._template_widget.etemplate_exec_id && this._template_widget.app)
|
if(this._template_widget.etemplate_exec_id && this._template_widget.app)
|
||||||
|
102
api/js/etemplate/Et2Template/Et2Template.md
Normal file
102
api/js/etemplate/Et2Template/Et2Template.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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 template="template"></et2-template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
Since template files are auto-fetched from the server, actual examples here would not work.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
Use `template` attribute with `<app>.<template>` format to specify which template to load.
|
||||||
|
|
||||||
|
This will fetch
|
||||||
|
`/<app>/templates/<interface>/<template>.xet`, where `<interface>` is the user's current interface, or default.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
|
||||||
|
<et2-template template="infolog.edit"></et2-template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sub-templates
|
||||||
|
|
||||||
|
If the template file contains more than one template definition, you can load any of the other templates defined after
|
||||||
|
the file has been loaded using either their full ID or a shortened form. This is useful for breaking a template into
|
||||||
|
smaller parts.
|
||||||
|
|
||||||
|
multiple.xml:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
|
||||||
|
<overlay>
|
||||||
|
<template id="multiple.one" class="one">...</template>
|
||||||
|
<template id="multiple.two" class="two">...</template>
|
||||||
|
<template id="multiple" class="multiple">
|
||||||
|
...
|
||||||
|
<et2-template template="multiple.one"></et2-template>
|
||||||
|
...
|
||||||
|
<et2-template template="two"></et2-template>
|
||||||
|
</template>
|
||||||
|
</overlay>
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL
|
||||||
|
|
||||||
|
If you need to bypass the autoloading based on template ID, you can specify the full URL to the template file. If there
|
||||||
|
are multiple templates defined in the file and you did not specify `template`, the last template in the file will be
|
||||||
|
loaded.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
When loading `Et2Template` will use its array managers (content, select_options, readonlys & modification) to set the
|
||||||
|
child widget attributes as it loads them.
|
||||||
|
|
||||||
|
Use `content` to create a namespace, loading the template using only a sub-section of the content arrays.
|
||||||
|
|
||||||
|
Content data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"address_one": {
|
||||||
|
"street": "123 example street",
|
||||||
|
"city": "Testville"
|
||||||
|
},
|
||||||
|
"address_two": {
|
||||||
|
"street": "321 Industrial Ave",
|
||||||
|
"city": "Testville"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
client/default/view.xet:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
|
||||||
|
<overlay>
|
||||||
|
<template id="address">
|
||||||
|
<et2-textbox id="street" label="Street"></et2-textbox>
|
||||||
|
<et2-textbox id="city" label="City"></et2-textbox>
|
||||||
|
...
|
||||||
|
</template>
|
||||||
|
<template id="client.view">
|
||||||
|
...
|
||||||
|
<et2-template template="address" content="address_one"></et2-template>
|
||||||
|
<et2-template template="address" content="address_two"></et2-template>
|
||||||
|
...
|
||||||
|
</template>
|
||||||
|
</overlay>
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
![Content example](/assets/components/template_example_content.png)
|
29
api/js/etemplate/Et2Template/Et2Template.styles.ts
Normal file
29
api/js/etemplate/Et2Template/Et2Template.styles.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {css} from 'lit';
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
628
api/js/etemplate/Et2Template/Et2Template.ts
Normal file
628
api/js/etemplate/Et2Template/Et2Template.ts
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
/**
|
||||||
|
* 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, PropertyValues, render} 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 {et2_loadXMLFromURL} from "../et2_core_xml";
|
||||||
|
import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||||
|
import type {IegwAppLocal} from "../../jsapi/egw_global";
|
||||||
|
import {until} from "lit/directives/until.js";
|
||||||
|
import {classMap} from "lit/directives/class-map.js";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
/**
|
||||||
|
* @summary Load & populate a template (.xet file) into the DOM
|
||||||
|
*
|
||||||
|
* @slot - The template's contents
|
||||||
|
* @event load - Emitted when all elements are loaded
|
||||||
|
*
|
||||||
|
* @csspart template - Wrapper around template content
|
||||||
|
* @csspart loader - Displayed while the template contents are being loaded
|
||||||
|
*/
|
||||||
|
@customElement("et2-template")
|
||||||
|
export class Et2Template extends Et2Widget(LitElement)
|
||||||
|
{
|
||||||
|
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
shoelace,
|
||||||
|
super.styles,
|
||||||
|
styles
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name / ID of template.
|
||||||
|
* It can be full [app].[template_file].[template] form or just the last part. Templates will be loaded from /[app]/templates/_interface_/[template_file].xet
|
||||||
|
* To use the short form, the file must already be loaded.
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
template : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A full URL to load template, optionally including cache-buster ('?'+filemtime of template on server)
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
url : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as index into content array for passing in specific content to the template other than what it would get by its ID
|
||||||
|
*/
|
||||||
|
@property()
|
||||||
|
content : string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of known templates
|
||||||
|
* @type {{[name : string] : Element}}
|
||||||
|
*/
|
||||||
|
public static templateCache : { [name : string] : Element } = {};
|
||||||
|
protected loading : Promise<void>;
|
||||||
|
|
||||||
|
private __egw : IegwAppLocal = null;
|
||||||
|
|
||||||
|
// Internal flag to indicate loading is in progress, since we can't monitor a promise
|
||||||
|
private __isLoading = false;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(egw? : IegwAppLocal)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
if(egw)
|
||||||
|
{
|
||||||
|
this.__egw = egw;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() : void
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener("load", this.handleLoad);
|
||||||
|
|
||||||
|
// If we can, start loading immediately
|
||||||
|
if(this.template || this.id || this.url)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProperties : PropertyValues)
|
||||||
|
{
|
||||||
|
// If content index was changed, re-check / create namespace
|
||||||
|
if(changedProperties.has("content"))
|
||||||
|
{
|
||||||
|
this.checkCreateNamespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load if template (template, id or URL) or content index changed
|
||||||
|
// (as long as we're not currently already loading, to prevent loops if load changes an attribute)
|
||||||
|
if(!this.__isLoading && ["template", "id", "url", "content"].filter(v => changedProperties.has(v)).length > 0)
|
||||||
|
{
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param value new value to set
|
||||||
|
* @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. This is normally called automatically when the
|
||||||
|
* template is added to the DOM, but if you want to re-load or not put it in the DOM you need to call load() yourself.
|
||||||
|
*
|
||||||
|
* If you need to set more than just content (select options, readonly or modifications), set it in the array manager
|
||||||
|
* before calling load:
|
||||||
|
* ```
|
||||||
|
* template.setArrayMgr("readonlys", template.getArrayMgr("readonlys").openPerspective(template, newReadonlys));
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
public async load(newContent? : object)
|
||||||
|
{
|
||||||
|
// @ts-ignore can't find disabled, it's in Et2Widget
|
||||||
|
if(this.disabled)
|
||||||
|
{
|
||||||
|
this.loading = Promise.resolve();
|
||||||
|
return this.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof newContent != "undefined")
|
||||||
|
{
|
||||||
|
// @ts-ignore ArrayMgr still expects et2_widgets
|
||||||
|
this.setArrayMgr("content", this.getArrayMgr("content").openPerspective(this, newContent));
|
||||||
|
}
|
||||||
|
this.__isLoading = true;
|
||||||
|
this.loading = new Promise(async(resolve, reject) =>
|
||||||
|
{
|
||||||
|
// Empty in case load was called again
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
// Get template XML
|
||||||
|
let xml : Element;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
xml = await this.findTemplate();
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Read the XML structure of the requested template
|
||||||
|
if(typeof xml != 'undefined')
|
||||||
|
{
|
||||||
|
// Get any template attributes from XML template node
|
||||||
|
const attrs = {};
|
||||||
|
xml.getAttributeNames().forEach(attribute =>
|
||||||
|
{
|
||||||
|
attrs[attribute] = xml.getAttribute(attribute);
|
||||||
|
});
|
||||||
|
// Don't change ID, keep what we've got
|
||||||
|
delete attrs["id"];
|
||||||
|
this.transformAttributes(attrs);
|
||||||
|
|
||||||
|
// Load children into template
|
||||||
|
this.loadFromXML(xml);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject("Could not find template");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for widgets to be complete
|
||||||
|
await this.loadFinished();
|
||||||
|
console.groupEnd();
|
||||||
|
this.__isLoading = false;
|
||||||
|
|
||||||
|
// Resolve promise, this.updateComplete now resolved
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
// Yield to give anything else a chance to run
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
// Notification event
|
||||||
|
this.dispatchEvent(new CustomEvent("load", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: this
|
||||||
|
}));
|
||||||
|
}, 0);
|
||||||
|
}).catch(reason =>
|
||||||
|
{
|
||||||
|
this.loadFailed(reason);
|
||||||
|
});
|
||||||
|
return this.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the template XML node, either from the local cache or the server
|
||||||
|
*
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async findTemplate() : Promise<Element>
|
||||||
|
{
|
||||||
|
// Find template name
|
||||||
|
const parts = (this.template || this.id).split('?');
|
||||||
|
const cache_buster = parts.length > 1 ? parts.pop() : null;
|
||||||
|
let template_name = parts.pop();
|
||||||
|
|
||||||
|
// Check to see if the template is already known / loaded into global ETemplate cache
|
||||||
|
let xml = Et2Template.templateCache[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 = Et2Template.templateCache[template_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the server for the template
|
||||||
|
if(!xml)
|
||||||
|
{
|
||||||
|
const url = this.getUrl();
|
||||||
|
let templates = <Element>{};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
templates = await this.loadFromFile(url);
|
||||||
|
if(!templates)
|
||||||
|
{
|
||||||
|
throw new Error("No templates found in template file " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
throw new Error("Could not load template file " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan the file for templates and store them
|
||||||
|
let fallback;
|
||||||
|
for(let i = 0; i < templates.childNodes?.length; i++)
|
||||||
|
{
|
||||||
|
const template = <Element>templates.childNodes[i];
|
||||||
|
if(!["template", "et2-template"].includes(template.nodeName.toLowerCase()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Et2Template.templateCache[template.getAttribute("id")] = <Element>template;
|
||||||
|
if(template.getAttribute("id") == template_name)
|
||||||
|
{
|
||||||
|
xml = template;
|
||||||
|
}
|
||||||
|
fallback = template;
|
||||||
|
}
|
||||||
|
// Take last template in the file if we had no better match
|
||||||
|
if(!xml)
|
||||||
|
{
|
||||||
|
xml = fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the xml from the given file
|
||||||
|
*
|
||||||
|
* Broken out here so it can be stubbed for testing
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @returns {Promise<Element | void>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected loadFromFile(path)
|
||||||
|
{
|
||||||
|
return et2_loadXMLFromURL(path, null, this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
{
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected clear()
|
||||||
|
{
|
||||||
|
// Clear
|
||||||
|
while(this.firstChild) this.removeChild(this.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFailed(reason? : any)
|
||||||
|
{
|
||||||
|
const message = (this.templateName) + " @ " + this.getUrl() + (reason ? " \n" + reason : "");
|
||||||
|
render(this.errorTemplate(message), this);
|
||||||
|
this.egw().debug("warn", "Loading failed: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getUrl()
|
||||||
|
{
|
||||||
|
if(this.url)
|
||||||
|
{
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = "";
|
||||||
|
const parts = (this.template || this.id).split('?');
|
||||||
|
const cache_buster = parts.length > 1 ? parts.pop() : ((new Date).valueOf() / 86400 | 0).toString();
|
||||||
|
let template_name = this.templateName;
|
||||||
|
|
||||||
|
// Full URL passed as template?
|
||||||
|
if(template_name.startsWith(this.egw().webserverUrl) && template_name.endsWith("xet"))
|
||||||
|
{
|
||||||
|
url = template_name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
const splitted = template_name.split('.');
|
||||||
|
const app = splitted.shift();
|
||||||
|
url = this.egw().link(
|
||||||
|
'/' + app + "/templates/default/" + splitted.join('.') + ".xet",
|
||||||
|
{download: cache_buster}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no cache-buster, reload daily
|
||||||
|
if(url.indexOf('?') === -1)
|
||||||
|
{
|
||||||
|
url += '?download=' + cache_buster;
|
||||||
|
}
|
||||||
|
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() || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get templateName()
|
||||||
|
{
|
||||||
|
const parts = (this.template || this.id).split('?');
|
||||||
|
const cache_buster = parts.length > 1 ? parts.pop() : null;
|
||||||
|
let template_name = parts.pop() || "";
|
||||||
|
|
||||||
|
return template_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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._widget_id = this.content;
|
||||||
|
super.checkCreateNamespace.apply(this, arguments);
|
||||||
|
this._widget_id = old_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createNamespace() : boolean
|
||||||
|
{
|
||||||
|
return this.content && this.content != this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 part="loader" class="template--loading">${loading}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorTemplate(errorMessage = "")
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<sl-alert variant="warning" open>
|
||||||
|
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
|
||||||
|
<strong>${this.egw().lang("Loading failed")}</strong><br/>
|
||||||
|
${errorMessage}
|
||||||
|
</sl-alert>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`
|
||||||
|
}
|
||||||
|
}
|
BIN
api/js/etemplate/Et2Template/doc/template_example_content.png
Normal file
BIN
api/js/etemplate/Et2Template/doc/template_example_content.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
225
api/js/etemplate/Et2Template/test/Load.test.ts
Normal file
225
api/js/etemplate/Et2Template/test/Load.test.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import {assert, elementUpdated, fixture, html, nextFrame, oneEvent} from "@open-wc/testing";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import {Et2Template} from "../Et2Template";
|
||||||
|
import {Et2Description} from "../../Et2Description/Et2Description";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test file for Template webComponent
|
||||||
|
*
|
||||||
|
* In here we test just basics and simple loading to avoid as few additional dependencies as possible.
|
||||||
|
*/
|
||||||
|
// Stub global egw
|
||||||
|
// @ts-ignore
|
||||||
|
window.egw = {
|
||||||
|
debug: () => {},
|
||||||
|
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||||
|
lang: i => i + "*",
|
||||||
|
link: i => i,
|
||||||
|
tooltipUnbind: () => { },
|
||||||
|
webserverUrl: ""
|
||||||
|
};
|
||||||
|
let element : Et2Template;
|
||||||
|
let keepImport : Et2Description = new Et2Description();
|
||||||
|
|
||||||
|
async function before()
|
||||||
|
{
|
||||||
|
// Create an element to test with, and wait until it's ready
|
||||||
|
// @ts-ignore
|
||||||
|
element = await fixture(html`
|
||||||
|
<et2-template>
|
||||||
|
</et2-template>
|
||||||
|
`);
|
||||||
|
// Stub egw()
|
||||||
|
sinon.stub(element, "egw").returns(window.egw);
|
||||||
|
await elementUpdated(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakedTemplate(template_text)
|
||||||
|
{
|
||||||
|
const parser = new window.DOMParser();
|
||||||
|
return parser.parseFromString(template_text, "text/xml").children[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIMPLE_EMPTY = `<overlay><template id="simple.empty"></template></overlay>`;
|
||||||
|
const SIMPLE = `<overlay><template id="simple">
|
||||||
|
<et2-description id="static" value="Static value"></et2-description>
|
||||||
|
<et2-description id="test"></et2-description>
|
||||||
|
</template></overlay>`;
|
||||||
|
const TEMPLATE_ATTRIBUTES = `<overlay><template id="attributes" class="gotClass" slot="gotSlot"></template></overlay>`;
|
||||||
|
const MULTIPLE = `<overlay>
|
||||||
|
<template id="multiple.one" class="one"/>
|
||||||
|
<template id="multiple.two" class="two"/>
|
||||||
|
<template id="multiple" class="multiple"></template>
|
||||||
|
</overlay>`;
|
||||||
|
const INVALID = `<overlay><template id="invalid"><overlay>`;
|
||||||
|
|
||||||
|
// Pre-fill cache
|
||||||
|
Et2Template.templateCache["simple.empty"] = <Element>fakedTemplate(SIMPLE_EMPTY).childNodes.item(0);
|
||||||
|
Et2Template.templateCache["simple"] = <Element>fakedTemplate(SIMPLE).childNodes.item(0);
|
||||||
|
Et2Template.templateCache["attributes"] = <Element>fakedTemplate(TEMPLATE_ATTRIBUTES).childNodes.item(0);
|
||||||
|
|
||||||
|
describe("Template widget basics", () =>
|
||||||
|
{
|
||||||
|
// Setup run before each test
|
||||||
|
beforeEach(before);
|
||||||
|
// Make sure it works
|
||||||
|
it('is defined', () =>
|
||||||
|
{
|
||||||
|
assert.instanceOf(element, Et2Template);
|
||||||
|
});
|
||||||
|
it("starts empty", () =>
|
||||||
|
{
|
||||||
|
assert.notExists(element.querySelectorAll("*"), "Not-loaded template has content. It should be empty.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Loading", () =>
|
||||||
|
{
|
||||||
|
beforeEach(before);
|
||||||
|
it("loads from file", async() =>
|
||||||
|
{
|
||||||
|
// Stub the url to point to the fixture
|
||||||
|
let xml = fakedTemplate(SIMPLE_EMPTY);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
sinon.stub(element, "loadFromFile").returns(xml);
|
||||||
|
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple.empty";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
assert.exists(loadEvent);
|
||||||
|
})
|
||||||
|
it("loads from cache", async() =>
|
||||||
|
{
|
||||||
|
// Cache was pre-filled above
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple.empty";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
assert.exists(loadEvent);
|
||||||
|
});
|
||||||
|
it("loads with short name (from cache)", async() =>
|
||||||
|
{
|
||||||
|
// Cache was pre-filled above
|
||||||
|
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
// @ts-ignore
|
||||||
|
sinon.stub(element, "getRoot").returns({
|
||||||
|
getInstanceManager: () => {return {name: "simple"}}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the template to start load, but use "short" name
|
||||||
|
element.template = "empty";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
assert.exists(loadEvent);
|
||||||
|
});
|
||||||
|
it("takes template attributes", async() =>
|
||||||
|
{
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "attributes";
|
||||||
|
|
||||||
|
// Wait for load
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
assert.isTrue(element.classList.contains("gotClass"), "Did not get class from template");
|
||||||
|
assert.isTrue(element.hasAttribute("slot"), "Did not get slot from template");
|
||||||
|
assert.equal(element.getAttribute("slot"), "gotSlot", "Did not get slot from template");
|
||||||
|
});
|
||||||
|
it("loads last template in file when it has no template otherwise", async() =>
|
||||||
|
{
|
||||||
|
// Stub the url to point to the fixture
|
||||||
|
let xml = fakedTemplate(MULTIPLE);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
sinon.stub(element, "loadFromFile").returns(xml);
|
||||||
|
|
||||||
|
// We don't set the template, just give the URL
|
||||||
|
element.url = "load a file that has several template"
|
||||||
|
|
||||||
|
// Wait for load
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
assert.isTrue(element.classList.contains("multiple"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows loader while loading", async() =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
sinon.stub(element, "findTemplate").returns(new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
// It's not good to wait in the test, but...
|
||||||
|
setTimeout(() => resolve(Et2Template.templateCache["simple.empty"]), 100);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple.empty";
|
||||||
|
|
||||||
|
// Wait for load to start
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Check for loader
|
||||||
|
let loader = element.shadowRoot.querySelector(".template--loading");
|
||||||
|
assert.isNotNull(loader, "Loader (shown while loading) not found")
|
||||||
|
|
||||||
|
// Wait for load, check the loader is gone
|
||||||
|
await element.updateComplete;
|
||||||
|
loader = element.shadowRoot.querySelector(".template--loading");
|
||||||
|
assert.isNull(loader, "Loader still there after load");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("actually creates children", async() =>
|
||||||
|
{
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple";
|
||||||
|
|
||||||
|
// Wait for load
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Should be not be empty
|
||||||
|
assert.isNotEmpty(element.querySelectorAll("*"));
|
||||||
|
assert.isNotNull(element.querySelector("#static"), "Missing template element");
|
||||||
|
assert.isNotNull(element.querySelector("#test"), "Missing template element");
|
||||||
|
})
|
||||||
|
it("does not load when disabled", async() =>
|
||||||
|
{
|
||||||
|
// Disable
|
||||||
|
// @ts-ignore can't find disabled attribute, though it's inherited from Et2Widget
|
||||||
|
element.disabled = true;
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple";
|
||||||
|
|
||||||
|
// Wait for load
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Should be empty
|
||||||
|
assert.isEmpty(element.querySelectorAll("*"));
|
||||||
|
});
|
||||||
|
it("shows a message when it can't find the template", async() =>
|
||||||
|
{
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "fail";
|
||||||
|
|
||||||
|
// Wait for load
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
// Should be not be empty, it has some error text
|
||||||
|
assert.isNotEmpty(element.querySelectorAll("*"));
|
||||||
|
assert.isTrue(element.innerText.includes("failed"));
|
||||||
|
})
|
||||||
|
});
|
144
api/js/etemplate/Et2Template/test/Namespace.test.ts
Normal file
144
api/js/etemplate/Et2Template/test/Namespace.test.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import {Et2Template} from "../Et2Template";
|
||||||
|
import {Et2Description} from "../../Et2Description/Et2Description";
|
||||||
|
import {assert, elementUpdated, fixture, html, oneEvent} from "@open-wc/testing";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import {et2_arrayMgr} from "../../et2_core_arrayMgr";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test file for Template webComponent
|
||||||
|
*
|
||||||
|
* In here we test just basics and simple loading to avoid as few additional dependencies as possible.
|
||||||
|
*/
|
||||||
|
// Stub global egw
|
||||||
|
// @ts-ignore
|
||||||
|
window.egw = {
|
||||||
|
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||||
|
lang: i => i + "",
|
||||||
|
link: i => i,
|
||||||
|
tooltipUnbind: () => { },
|
||||||
|
webserverUrl: ""
|
||||||
|
};
|
||||||
|
let element : Et2Template;
|
||||||
|
let keepImport : Et2Description = new Et2Description();
|
||||||
|
|
||||||
|
async function before()
|
||||||
|
{
|
||||||
|
// Create an element to test with, and wait until it's ready
|
||||||
|
// @ts-ignore
|
||||||
|
element = await fixture(html`
|
||||||
|
<et2-template>
|
||||||
|
</et2-template>
|
||||||
|
`);
|
||||||
|
// Stub egw()
|
||||||
|
sinon.stub(element, "egw").returns(window.egw);
|
||||||
|
await elementUpdated(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakedTemplate(template_text)
|
||||||
|
{
|
||||||
|
const parser = new window.DOMParser();
|
||||||
|
return parser.parseFromString(template_text, "text/xml").children[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIMPLE = `<overlay><template id="simple">
|
||||||
|
<et2-description id="static" value="Static value"></et2-description>
|
||||||
|
<et2-description id="test"></et2-description>
|
||||||
|
</template></overlay>`;
|
||||||
|
|
||||||
|
// Pre-fill cache
|
||||||
|
Et2Template.templateCache["simple"] = <Element>fakedTemplate(SIMPLE).childNodes.item(0);
|
||||||
|
|
||||||
|
describe("Namespaces", () =>
|
||||||
|
{
|
||||||
|
// Setup run before each test
|
||||||
|
beforeEach(before);
|
||||||
|
it("Does not create a namespace with no 'content' attribute", async() =>
|
||||||
|
{
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
element.setArrayMgr("content", new et2_arrayMgr({
|
||||||
|
test: "Test"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
const staticElement : HTMLElement = element.querySelector(":scope > *:first-of-type");
|
||||||
|
assert.isNotNull(staticElement, "Did not find test element");
|
||||||
|
assert.equal(staticElement.getAttribute("id"), "static", "Static child ID was wrong");
|
||||||
|
assert.equal(staticElement.innerText, "Static value");
|
||||||
|
|
||||||
|
const dynamicElement : HTMLElement = element.querySelector(":scope > *:last-of-type");
|
||||||
|
assert.isNotNull(dynamicElement, "Did not find test element");
|
||||||
|
assert.equal(dynamicElement.getAttribute("id"), "test", "Dynamic child ID was wrong");
|
||||||
|
assert.equal(dynamicElement.innerText, "Test");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test creating a namespace on the template.
|
||||||
|
* This means we expect all child elements to include the template ID as part of their ID,
|
||||||
|
* and they should be given the correct part of the content array.
|
||||||
|
*/
|
||||||
|
it("Creates a namespace when content is set", async() =>
|
||||||
|
{
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
element.setArrayMgr("content", new et2_arrayMgr({
|
||||||
|
test: "Top level",
|
||||||
|
sub: {
|
||||||
|
test: "Namespaced"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
element.content = "sub";
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
const staticElement : HTMLElement = element.querySelector(":scope > *:first-of-type");
|
||||||
|
assert.isNotNull(staticElement, "Did not find test element");
|
||||||
|
assert.equal(staticElement.getAttribute("id"), "sub_static", "Child ID was not namespaced");
|
||||||
|
assert.equal(staticElement.innerText, "Static value");
|
||||||
|
|
||||||
|
const dynamicElement : HTMLElement = element.querySelector(":scope > *:last-of-type");
|
||||||
|
assert.isNotNull(dynamicElement, "Did not find test element");
|
||||||
|
assert.notEqual(dynamicElement.getAttribute("id"), "Top level");
|
||||||
|
assert.equal(dynamicElement.innerText, "Namespaced");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can replace data when loading", async() =>
|
||||||
|
{
|
||||||
|
const listener = oneEvent(element, "load");
|
||||||
|
element.setArrayMgr("content", new et2_arrayMgr({
|
||||||
|
test: "Test"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set the template to start load
|
||||||
|
element.template = "simple";
|
||||||
|
|
||||||
|
// Wait for load & load event
|
||||||
|
await element.updateComplete;
|
||||||
|
const loadEvent = await listener;
|
||||||
|
|
||||||
|
const staticElement : HTMLElement = element.querySelector(":scope > *:first-of-type");
|
||||||
|
assert.isNotNull(staticElement, "Did not find test element");
|
||||||
|
assert.equal(staticElement.innerText, "Static value");
|
||||||
|
|
||||||
|
let dynamicElement : HTMLElement = element.querySelector(":scope > *:last-of-type");
|
||||||
|
assert.isNotNull(dynamicElement, "Did not find test element");
|
||||||
|
assert.equal(dynamicElement.innerText, "Test");
|
||||||
|
|
||||||
|
// Now load new data
|
||||||
|
await element.load({test: "Success"});
|
||||||
|
|
||||||
|
// Old element was destroyed, get the new one
|
||||||
|
dynamicElement = element.querySelector(":scope > *:last-of-type");
|
||||||
|
assert.equal(dynamicElement.innerText, "Success", "Element did not get new value when template was loaded with new data");
|
||||||
|
});
|
||||||
|
});
|
@ -13,6 +13,12 @@ import {dedupeMixin} from "@open-wc/dedupe-mixin";
|
|||||||
import type {et2_container} from "../et2_core_baseWidget";
|
import type {et2_container} from "../et2_core_baseWidget";
|
||||||
import type {et2_DOMWidget} from "../et2_core_DOMWidget";
|
import type {et2_DOMWidget} from "../et2_core_DOMWidget";
|
||||||
import bootstrapIcons from "../Styles/bootstrap-icons";
|
import bootstrapIcons from "../Styles/bootstrap-icons";
|
||||||
|
import {EgwAction} from "../../egw_action/EgwAction";
|
||||||
|
import {property} from "lit/decorators/property.js";
|
||||||
|
import {egw_getActionManager, egw_getAppObjectManager} from "../../egw_action/egw_action";
|
||||||
|
import {EgwEt2WidgetObject} from "../../egw_action/EgwEt2WidgetObject";
|
||||||
|
import {EgwActionObject} from "../../egw_action/EgwActionObject";
|
||||||
|
import {EgwActionObjectInterface} from "../../egw_action/EgwActionObjectInterface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This mixin will allow any LitElement to become an Et2Widget
|
* This mixin will allow any LitElement to become an Et2Widget
|
||||||
@ -101,6 +107,8 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
|
|||||||
*/
|
*/
|
||||||
protected _deferred_properties : { [key : string] : string } = {};
|
protected _deferred_properties : { [key : string] : string } = {};
|
||||||
|
|
||||||
|
protected _actionManager : EgwAction = null;
|
||||||
|
|
||||||
|
|
||||||
/** WebComponent **/
|
/** WebComponent **/
|
||||||
static get styles()
|
static get styles()
|
||||||
@ -267,6 +275,10 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
|
|||||||
data: {
|
data: {
|
||||||
type: String,
|
type: String,
|
||||||
reflect: false
|
reflect: false
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
type: Object
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -497,6 +509,156 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
|
|||||||
return data.join(",");
|
return data.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Actions on the widget
|
||||||
|
*
|
||||||
|
* Each action is defined as an object:
|
||||||
|
*
|
||||||
|
* move: {
|
||||||
|
* type: "drop",
|
||||||
|
* acceptedTypes: "mail",
|
||||||
|
* icon: "move",
|
||||||
|
* caption: "Move to"
|
||||||
|
* onExecute: javascript:mail_move"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped,
|
||||||
|
* the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the
|
||||||
|
* dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The
|
||||||
|
* etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler
|
||||||
|
* can operate in the widget context easily. The location varies depending on your action though. It
|
||||||
|
* might be action.parent.parent.data.widget
|
||||||
|
*
|
||||||
|
* To customise how the actions are handled for a particular widget, override _link_actions(). It handles
|
||||||
|
* the more widget-specific parts.
|
||||||
|
*
|
||||||
|
* @param {object} actions {ID: {attributes..}+} map of egw action information
|
||||||
|
* @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method
|
||||||
|
*/
|
||||||
|
@property({type: Object})
|
||||||
|
set actions(actions : EgwAction[] | { [id : string] : object })
|
||||||
|
{
|
||||||
|
if(!(Array.isArray(actions) && actions.length > 0 || Object.entries(actions).length > 0))
|
||||||
|
{
|
||||||
|
// Not trying to clear actions, just called automatic
|
||||||
|
if(!this._actionManager)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.id == "" || typeof this.id == "undefined")
|
||||||
|
{
|
||||||
|
this.egw().debug("warn", "Widget should have an ID if you want actions", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the action manager and add some actions to it
|
||||||
|
if(this._actionManager == null)
|
||||||
|
{
|
||||||
|
// Find the apropriate parent action manager
|
||||||
|
let parent_am = null;
|
||||||
|
let widget = <Et2WidgetClass | et2_widget>this;
|
||||||
|
while(widget.getParent() && !parent_am)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
if(widget._actionManager)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
parent_am = widget._actionManager;
|
||||||
|
}
|
||||||
|
widget = widget.getParent();
|
||||||
|
}
|
||||||
|
if(!parent_am)
|
||||||
|
{
|
||||||
|
// Only look 1 level deep
|
||||||
|
parent_am = egw_getActionManager(this.egw().appName, true, 1);
|
||||||
|
}
|
||||||
|
if(parent_am.getActionById(this.getInstanceManager().uniqueId, 1) !== null)
|
||||||
|
{
|
||||||
|
parent_am = parent_am.getActionById(this.getInstanceManager().uniqueId, 1);
|
||||||
|
}
|
||||||
|
if(parent_am.getActionById(this.id, 1) != null)
|
||||||
|
{
|
||||||
|
this._actionManager = parent_am.getActionById(this.id, 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._actionManager = parent_am.addAction("actionManager", this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._actionManager.updateActions(actions, this.egw().appName);
|
||||||
|
|
||||||
|
// Put a reference to the widget into the action stuff, so we can
|
||||||
|
// easily get back to widget context from the action handler
|
||||||
|
this._actionManager.data = {widget: this};
|
||||||
|
|
||||||
|
// Link the actions to the DOM
|
||||||
|
this._link_actions(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
get actions()
|
||||||
|
{
|
||||||
|
return this._actionManager?.children || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all action-links / id's of 1.-level actions from a given action object
|
||||||
|
*
|
||||||
|
* This can be overwritten to not allow all actions, by not returning them here.
|
||||||
|
*
|
||||||
|
* @param actions
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
protected _get_action_links(actions)
|
||||||
|
{
|
||||||
|
const action_links = [];
|
||||||
|
for(let i in actions)
|
||||||
|
{
|
||||||
|
let action = actions[i];
|
||||||
|
action_links.push(typeof action.id != 'undefined' ? action.id : i);
|
||||||
|
}
|
||||||
|
return action_links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link the actions to the DOM nodes / widget bits.
|
||||||
|
*
|
||||||
|
* @param {object} actions {ID: {attributes..}+} map of egw action information
|
||||||
|
*/
|
||||||
|
protected _link_actions(actions)
|
||||||
|
{
|
||||||
|
// Get the top level element for the tree
|
||||||
|
let objectManager = egw_getAppObjectManager(true);
|
||||||
|
let widget_object = objectManager.getObjectById(this.id);
|
||||||
|
|
||||||
|
if(widget_object == null)
|
||||||
|
{
|
||||||
|
// Add a new container to the object manager which will hold the widget
|
||||||
|
// objects
|
||||||
|
widget_object = objectManager.insertObject(false, new EgwActionObject(
|
||||||
|
this.id, objectManager, this.createWidgetObjectInterface(),
|
||||||
|
this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
widget_object.setAOI(this.createWidgetObjectInterface());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all old objects
|
||||||
|
widget_object.clear();
|
||||||
|
widget_object.unregisterActions();
|
||||||
|
|
||||||
|
// Go over the widget & add links - this is where we decide which actions are
|
||||||
|
// 'allowed' for this widget at this time
|
||||||
|
widget_object.updateActionLinks(this._get_action_links(actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createWidgetObjectInterface() : EgwActionObjectInterface
|
||||||
|
{
|
||||||
|
return <EgwActionObjectInterface><unknown>(new EgwEt2WidgetObject(this));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A property has changed, and we want to make adjustments to other things
|
* A property has changed, and we want to make adjustments to other things
|
||||||
* based on that
|
* based on that
|
||||||
@ -1010,7 +1172,7 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
|
|||||||
if(!this._parent_node && this.getParent() instanceof et2_widget && (<et2_DOMWidget>this.getParent()).getDOMNode(this) != this.parentNode)
|
if(!this._parent_node && this.getParent() instanceof et2_widget && (<et2_DOMWidget>this.getParent()).getDOMNode(this) != this.parentNode)
|
||||||
{
|
{
|
||||||
// @ts-ignore this is not an et2_widget, and Et2Widget is not a Node
|
// @ts-ignore this is not an et2_widget, and Et2Widget is not a Node
|
||||||
(<et2_DOMWidget>this.getParent()).getDOMNode(this).append(this);
|
(<et2_DOMWidget>this.getParent()).getDOMNode(this)?.append(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// An empty text node causes problems with legacy widget children
|
// An empty text node causes problems with legacy widget children
|
||||||
@ -1589,6 +1751,11 @@ function transformAttributes(widget, mgr : et2_arrayMgr, attributes)
|
|||||||
widget.style.setProperty("flex", "0 0 auto");
|
widget.style.setProperty("flex", "0 0 auto");
|
||||||
delete attributes.width;
|
delete attributes.width;
|
||||||
}
|
}
|
||||||
|
if(attributes.height)
|
||||||
|
{
|
||||||
|
widget.style.setProperty("height", attributes.height);
|
||||||
|
delete attributes.height;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply any set attributes - widget will do its own coercion
|
// Apply any set attributes - widget will do its own coercion
|
||||||
for(let attribute in attributes)
|
for(let attribute in attributes)
|
||||||
@ -1712,7 +1879,10 @@ function transformAttributes(widget, mgr : et2_arrayMgr, attributes)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Set as property
|
// Set as property
|
||||||
|
const old_value = widget[attribute];
|
||||||
widget[attribute] = attrValue;
|
widget[attribute] = attrValue;
|
||||||
|
// Due to reactive properties not updating properly, make sure to trigger an update
|
||||||
|
widget.requestUpdate(attribute, old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widget_class.getPropertyOptions("value") && widget.set_value)
|
if(widget_class.getPropertyOptions("value") && widget.set_value)
|
||||||
|
@ -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
|
||||||
@ -465,7 +465,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;
|
||||||
|
@ -360,7 +360,8 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
|
|||||||
do {
|
do {
|
||||||
template = template.getParent();
|
template = template.getParent();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} while (template !== tabbox && template.getType() !== 'template');
|
}
|
||||||
|
while(template !== tabbox && ['template', 'ET2-TEMPLATE'].indexOf(template.getType()) == -1);
|
||||||
for (var i = tabbox.tabData.length - 1; i >= 0; i--)
|
for (var i = tabbox.tabData.length - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if(template && template.id &&
|
if(template && template.id &&
|
||||||
|
@ -76,6 +76,8 @@ import {Et2AccountFilterHeader} from "./Et2Nextmatch/Headers/AccountFilterHeader
|
|||||||
import {Et2SelectCategory} from "./Et2Select/Select/Et2SelectCategory";
|
import {Et2SelectCategory} from "./Et2Select/Select/Et2SelectCategory";
|
||||||
import {Et2Searchbox} from "./Et2Textbox/Et2Searchbox";
|
import {Et2Searchbox} from "./Et2Textbox/Et2Searchbox";
|
||||||
import type {LitElement} from "lit";
|
import type {LitElement} from "lit";
|
||||||
|
import {Et2Template} from "./Et2Template/Et2Template";
|
||||||
|
import {waitForEvent} from "./Et2Widget/event";
|
||||||
|
|
||||||
//import {et2_selectAccount} from "./et2_widget_SelectAccount";
|
//import {et2_selectAccount} from "./et2_widget_SelectAccount";
|
||||||
let keep_import : Et2AccountFilterHeader
|
let keep_import : Et2AccountFilterHeader
|
||||||
@ -1304,9 +1306,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
|||||||
// Bind a resize while we're here
|
// Bind a resize while we're here
|
||||||
if(tab.flagDiv)
|
if(tab.flagDiv)
|
||||||
{
|
{
|
||||||
tab.flagDiv.addEventListener("click", (e) =>
|
tab.flagDiv.addEventListener("click", async(e) =>
|
||||||
{
|
{
|
||||||
window.setTimeout(() => this.resize(), 1);
|
// Wait for the tab to be done being shown
|
||||||
|
await waitForEvent(tab.flagDiv.parentElement, "sl-tab-show");
|
||||||
|
// then resize
|
||||||
|
this.resize();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new et2_dynheight(tab.contentDiv, this.innerDiv, 100);
|
return new et2_dynheight(tab.contentDiv, this.innerDiv, 100);
|
||||||
@ -2413,7 +2418,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
|||||||
*/
|
*/
|
||||||
set_template(template_name : string)
|
set_template(template_name : string)
|
||||||
{
|
{
|
||||||
const template = et2_createWidget("template", {"id": template_name}, this);
|
const template = <Et2Template>loadWebComponent("et2-template", {"id": template_name, class: "hideme"}, this);
|
||||||
if(this.template)
|
if(this.template)
|
||||||
{
|
{
|
||||||
// Stop early to prevent unneeded processing, and prevent infinite
|
// Stop early to prevent unneeded processing, and prevent infinite
|
||||||
@ -2482,10 +2487,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free the template again, but don't remove it
|
// Free the template and remove it
|
||||||
setTimeout(function()
|
setTimeout(function()
|
||||||
{
|
{
|
||||||
template.destroy();
|
template.destroy();
|
||||||
|
template.remove();
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
// Call the "setNextmatch" function of all registered
|
// Call the "setNextmatch" function of all registered
|
||||||
@ -2525,13 +2531,9 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
|||||||
this._set_autorefresh(this._get_autorefresh());
|
this._set_autorefresh(this._get_autorefresh());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Template might not be loaded yet, defer parsing
|
|
||||||
const promise = [];
|
|
||||||
template.loadingFinished(promise);
|
|
||||||
|
|
||||||
// Wait until template (& children) are done
|
// Wait until template (& children) are done
|
||||||
// Keep promise so we can return it from doLoadingFinished
|
// Keep promise so we can return it from doLoadingFinished
|
||||||
this.template_promise = Promise.all(promise).then(() =>
|
this.template_promise = template.updateComplete.then(() =>
|
||||||
{
|
{
|
||||||
parse.call(this, template);
|
parse.call(this, template);
|
||||||
if(!this.dynheight)
|
if(!this.dynheight)
|
||||||
@ -2558,6 +2560,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
).finally(() => this.template_promise = null);
|
).finally(() => this.template_promise = null);
|
||||||
|
this.template_promise.widget = this;
|
||||||
|
|
||||||
|
// Explictly add template to DOM since it won't happen otherwise, and webComponents need to be in the DOM
|
||||||
|
// to complete
|
||||||
|
this.div.append(template);
|
||||||
|
template.load();
|
||||||
|
|
||||||
return this.template_promise;
|
return this.template_promise;
|
||||||
}
|
}
|
||||||
@ -3650,47 +3658,37 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext
|
|||||||
|
|
||||||
// Load the template
|
// Load the template
|
||||||
const self = this;
|
const self = this;
|
||||||
const header = <et2_template>et2_createWidget("template", {"id": template_name}, this);
|
const header = <Et2Template>loadWebComponent("et2-template", {"id": template_name}, this);
|
||||||
this.headers[id] = header;
|
this.headers[id] = header;
|
||||||
const deferred = [];
|
|
||||||
header.loadingFinished(deferred);
|
// fix order in DOM by reattaching templates in correct position
|
||||||
|
switch(id)
|
||||||
|
{
|
||||||
|
case 0: // header_left: prepend
|
||||||
|
jQuery(header.getDOMNode()).prependTo(self.header_div);
|
||||||
|
break;
|
||||||
|
case 1: // header_right: before favorites and count
|
||||||
|
window.setTimeout(() =>
|
||||||
|
jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')));
|
||||||
|
break;
|
||||||
|
case 2: // header_row: after search
|
||||||
|
window.setTimeout(function()
|
||||||
|
{ // otherwise we might end up after filters
|
||||||
|
jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search'));
|
||||||
|
}, 1);
|
||||||
|
break;
|
||||||
|
case 3: // header_row2: below everything
|
||||||
|
window.setTimeout(function()
|
||||||
|
{ // otherwise we might end up after filters
|
||||||
|
jQuery(header.getDOMNode()).insertAfter(self.header_div);
|
||||||
|
}, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait until all child widgets are loaded, then bind
|
// Wait until all child widgets are loaded, then bind
|
||||||
Promise.all(deferred).then(() =>
|
header.updateComplete.then(() =>
|
||||||
{
|
{
|
||||||
// fix order in DOM by reattaching templates in correct position
|
self._bindHeaderInput(header);
|
||||||
switch(id)
|
|
||||||
{
|
|
||||||
case 0: // header_left: prepend
|
|
||||||
jQuery(header.getDOMNode()).prependTo(self.header_div);
|
|
||||||
break;
|
|
||||||
case 1: // header_right: before favorites and count
|
|
||||||
window.setTimeout(() =>
|
|
||||||
jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')));
|
|
||||||
break;
|
|
||||||
case 2: // header_row: after search
|
|
||||||
window.setTimeout(function()
|
|
||||||
{ // otherwise we might end up after filters
|
|
||||||
jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search'));
|
|
||||||
}, 1);
|
|
||||||
break;
|
|
||||||
case 3: // header_row2: below everything
|
|
||||||
window.setTimeout(function()
|
|
||||||
{ // otherwise we might end up after filters
|
|
||||||
jQuery(header.getDOMNode()).insertAfter(self.header_div);
|
|
||||||
}, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Give child templates a chance to load before we bind inputs
|
|
||||||
let children = [];
|
|
||||||
header.iterateOver((_widget) =>
|
|
||||||
{
|
|
||||||
children.push(_widget.loading);
|
|
||||||
}, this, et2_template);
|
|
||||||
Promise.all(children).then(() =>
|
|
||||||
{
|
|
||||||
self._bindHeaderInput(header);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,266 +8,12 @@
|
|||||||
* @author Andreas Stöckel
|
* @author Andreas Stöckel
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*egw:uses
|
|
||||||
et2_core_xml;
|
|
||||||
et2_core_DOMWidget;
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './et2_core_interfaces';
|
import {Et2Template} from "./Et2Template/Et2Template";
|
||||||
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 class et2_template extends 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"]);
|
|
@ -16,9 +16,6 @@ import {EgwApp} from "../jsapi/egw_app";
|
|||||||
import {et2_IInput, et2_IPrint, et2_IResizeable, et2_ISubmitListener} from "./et2_core_interfaces";
|
import {et2_IInput, et2_IPrint, et2_IResizeable, et2_ISubmitListener} from "./et2_core_interfaces";
|
||||||
import {egw} from "../jsapi/egw_global";
|
import {egw} from "../jsapi/egw_global";
|
||||||
import {et2_arrayMgr, et2_readonlysArrayMgr} from "./et2_core_arrayMgr";
|
import {et2_arrayMgr, et2_readonlysArrayMgr} from "./et2_core_arrayMgr";
|
||||||
import {et2_checkType} from "./et2_core_common";
|
|
||||||
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
|
|
||||||
import {et2_loadXMLFromURL} from "./et2_core_xml";
|
|
||||||
import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch";
|
import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch";
|
||||||
import '../jsapi/egw_json.js';
|
import '../jsapi/egw_json.js';
|
||||||
import {egwIsMobile} from "../egw_action/egw_action_common";
|
import {egwIsMobile} from "../egw_action/egw_action_common";
|
||||||
@ -88,6 +85,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';
|
||||||
@ -158,6 +156,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";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,7 +184,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;
|
||||||
@ -218,7 +217,6 @@ export class etemplate2
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Preset the object variable
|
* Preset the object variable
|
||||||
* @type {et2_container}
|
|
||||||
*/
|
*/
|
||||||
this._widgetContainer = null;
|
this._widgetContainer = null;
|
||||||
|
|
||||||
@ -227,15 +225,15 @@ export class etemplate2
|
|||||||
// We share list of templates with iframes and popups
|
// We share list of templates with iframes and popups
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(opener && opener.etemplate2)
|
if(opener && opener.Et2Template)
|
||||||
{
|
{
|
||||||
etemplate2.templates = opener.etemplate2.templates;
|
Et2Template.templateCache = opener.Et2Template.templateCache;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
else if(top.etemplate2)
|
else if(top.Et2Template)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
etemplate2.templates = top.etemplate2.templates;
|
Et2Template.templateCache = top.Et2Template.templateCache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
@ -357,7 +355,7 @@ export class etemplate2
|
|||||||
jQuery(this._DOMContainer).empty();
|
jQuery(this._DOMContainer).empty();
|
||||||
|
|
||||||
// Remove self from the index
|
// Remove self from the index
|
||||||
for(const name in etemplate2.templates)
|
for(const name in Et2Template.templateCache)
|
||||||
{
|
{
|
||||||
if(typeof etemplate2._byTemplate[name] == "undefined")
|
if(typeof etemplate2._byTemplate[name] == "undefined")
|
||||||
{
|
{
|
||||||
@ -677,10 +675,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)
|
||||||
@ -698,14 +703,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")
|
||||||
@ -714,22 +711,6 @@ export class etemplate2
|
|||||||
}
|
}
|
||||||
etemplate2._byTemplate[_name].push(this);
|
etemplate2._byTemplate[_name].push(this);
|
||||||
|
|
||||||
// Read the XML structure of the requested template
|
|
||||||
if(etemplate2.templates[this.name].hasAttribute("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)
|
||||||
{
|
{
|
||||||
@ -749,27 +730,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) =>
|
||||||
{
|
{
|
||||||
@ -786,7 +748,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();
|
||||||
}
|
}
|
||||||
@ -817,16 +779,6 @@ export class etemplate2
|
|||||||
detail: this
|
detail: this
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if(etemplate2.templates[this.name].attributes.onload)
|
|
||||||
{
|
|
||||||
let onload = et2_checkType(etemplate2.templates[this.name].attributes.onload.value, 'js', 'onload', {});
|
|
||||||
if(typeof onload === 'string')
|
|
||||||
{
|
|
||||||
onload = et2_compileLegacyJS(onload, this, this._widgetContainer);
|
|
||||||
}
|
|
||||||
onload.call(this._widgetContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profiling
|
// Profiling
|
||||||
if(egw.debug_level() >= 4)
|
if(egw.debug_level() >= 4)
|
||||||
{
|
{
|
||||||
@ -851,58 +803,9 @@ export class etemplate2
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Load & process
|
// Load & process
|
||||||
try
|
_load.apply(this, []);
|
||||||
{
|
}).then(async() => await this._widgetContainer.updateComplete);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
// weird security exception in IE denying access to template cache in opener
|
|
||||||
if(e.message == 'Permission denied')
|
|
||||||
{
|
|
||||||
etemplate2.templates = {};
|
|
||||||
}
|
|
||||||
// other error eg. in app.js et2_ready or event handlers --> rethrow it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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++)
|
|
||||||
{
|
|
||||||
const template = _xmldoc.childNodes[i];
|
|
||||||
if(template.nodeName.toLowerCase() != "template")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
etemplate2.templates[template.getAttribute("id")] = template;
|
|
||||||
if(!_name)
|
|
||||||
{
|
|
||||||
this.name = template.getAttribute("id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_load.apply(this, []);
|
|
||||||
}, this);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public focusOnFirstInput()
|
public focusOnFirstInput()
|
||||||
|
@ -318,7 +318,7 @@ window.app = {classes: {}};
|
|||||||
{
|
{
|
||||||
var $main_div = jQuery('#popupMainDiv');
|
var $main_div = jQuery('#popupMainDiv');
|
||||||
let $et2 = jQuery('.et2_container');
|
let $et2 = jQuery('.et2_container');
|
||||||
let $layoutTable = jQuery(".et2_container > div > table", $main_div);
|
let $layoutTable = jQuery(".et2_container > * > table", $main_div);
|
||||||
if ($layoutTable.length && $et2.width() < $layoutTable.width())
|
if ($layoutTable.length && $et2.width() < $layoutTable.width())
|
||||||
{
|
{
|
||||||
// Still using a layout table, and it's bigger.
|
// Still using a layout table, and it's bigger.
|
||||||
@ -344,7 +344,7 @@ window.app = {classes: {}};
|
|||||||
delta_height = 0;
|
delta_height = 0;
|
||||||
}
|
}
|
||||||
if((delta_width != 0 || delta_height != 0) &&
|
if((delta_width != 0 || delta_height != 0) &&
|
||||||
(delta_width >2 || delta_height >2 || delta_width<-2 || delta_height < -2) && (scrollHeight>0 || scrollWidth>0))
|
(delta_width > 2 || delta_height > 2 || delta_width < -2 || delta_height < -2))
|
||||||
{
|
{
|
||||||
|
|
||||||
if (window.framework && typeof window.framework.resize_popup != 'undefined')
|
if (window.framework && typeof window.framework.resize_popup != 'undefined')
|
||||||
@ -376,7 +376,14 @@ window.app = {classes: {}};
|
|||||||
const data = JSON.parse(node.getAttribute('data-etemplate')) || {};
|
const data = JSON.parse(node.getAttribute('data-etemplate')) || {};
|
||||||
if (popup || window.opener && !egwIsMobile()) {
|
if (popup || window.opener && !egwIsMobile()) {
|
||||||
// Resize popup when et2 load is done
|
// Resize popup when et2 load is done
|
||||||
jQuery(node).on('load', () => window.setTimeout(resize_popup, 50));
|
jQuery(node).on('load', (event) =>
|
||||||
|
{
|
||||||
|
// Only the top-level template, not any sub-templates
|
||||||
|
if (event.target == node)
|
||||||
|
{
|
||||||
|
window.setTimeout(resize_popup, 50)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const et2 = new etemplate2(node, data.data.menuaction);
|
const et2 = new etemplate2(node, data.data.menuaction);
|
||||||
et2.load(data.name, data.url, data.data);
|
et2.load(data.name, data.url, data.data);
|
||||||
|
@ -67,11 +67,11 @@
|
|||||||
max-width: 100%
|
max-width: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
.et2_container > div:not([class]) {
|
.et2_container > *:not([class]) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.et2_container > div:not([class]) > table {
|
.et2_container > *:not([class]) > table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -2044,7 +2044,7 @@ egw-validation-feedback[type] {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.et2_nextmatch .nextmatch_header_row > div {
|
.et2_nextmatch .nextmatch_header_row > div, .et2_nextmatch .nextmatch_header_row et2-template::part(base) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1ex;
|
gap: 1ex;
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
#calendar-toolbar {
|
#calendar-toolbar {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
#calendar-toolbar > div {
|
#calendar-toolbar > et2-template::part(base) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -178,7 +178,7 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
form#calendar-toolbar > div {
|
form#calendar-toolbar > * {
|
||||||
column-gap: 1ex;
|
column-gap: 1ex;
|
||||||
}
|
}
|
||||||
#calendar-todo {
|
#calendar-todo {
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
#calendar-toolbar {
|
#calendar-toolbar {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
#calendar-toolbar > div {
|
#calendar-toolbar > et2-template::part(base) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -190,7 +190,7 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
form#calendar-toolbar > div {
|
form#calendar-toolbar > * {
|
||||||
column-gap: 1ex;
|
column-gap: 1ex;
|
||||||
}
|
}
|
||||||
#calendar-todo {
|
#calendar-todo {
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
#calendar-toolbar {
|
#calendar-toolbar {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
#calendar-toolbar > div {
|
#calendar-toolbar > et2-template::part(base) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -179,7 +179,7 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
form#calendar-toolbar > div {
|
form#calendar-toolbar > * {
|
||||||
column-gap: 1ex;
|
column-gap: 1ex;
|
||||||
}
|
}
|
||||||
#calendar-todo {
|
#calendar-todo {
|
||||||
|
@ -70,6 +70,7 @@ module.exports = function (eleventyConfig)
|
|||||||
eleventyConfig.addPassthroughCopy({
|
eleventyConfig.addPassthroughCopy({
|
||||||
"../../vendor/bower-asset/jquery/dist/jquery.min.js": "assets/scripts/vendor/bower-asset/jquery/dist/jquery.min.js",
|
"../../vendor/bower-asset/jquery/dist/jquery.min.js": "assets/scripts/vendor/bower-asset/jquery/dist/jquery.min.js",
|
||||||
"../../vendor/bower-asset/cropper/dist/cropper.min.js": "assets/scripts/vendor/bower-asset/cropper/dist/cropper.min.js",
|
"../../vendor/bower-asset/cropper/dist/cropper.min.js": "assets/scripts/vendor/bower-asset/cropper/dist/cropper.min.js",
|
||||||
|
"../../vendor/bower-asset/diff2html/dist/diff2html.min.js": "assets/scripts/vendor/bower-asset/diff2html/dist/diff2html.min.js",
|
||||||
"../../vendor/tinymce/tinymce/tinymce.min.js": "assets/scripts/vendor/tinymce/tinymce/tinymce.min.js",
|
"../../vendor/tinymce/tinymce/tinymce.min.js": "assets/scripts/vendor/tinymce/tinymce/tinymce.min.js",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -77,6 +78,7 @@ module.exports = function (eleventyConfig)
|
|||||||
eleventyConfig.addPassthroughCopy({"../../chunks": "assets/scripts/chunks"});
|
eleventyConfig.addPassthroughCopy({"../../chunks": "assets/scripts/chunks"});
|
||||||
eleventyConfig.addPassthroughCopy({"../../api/js/etemplate/etemplate2.js": "assets/scripts/sub/dir/etemplate/etemplate2.js"});
|
eleventyConfig.addPassthroughCopy({"../../api/js/etemplate/etemplate2.js": "assets/scripts/sub/dir/etemplate/etemplate2.js"});
|
||||||
eleventyConfig.addPassthroughCopy({"../../node_modules/bootstrap-icons/font/bootstrap-icons.min.css": "assets/styles/bootstrap-icons.min.css"});
|
eleventyConfig.addPassthroughCopy({"../../node_modules/bootstrap-icons/font/bootstrap-icons.min.css": "assets/styles/bootstrap-icons.min.css"});
|
||||||
|
eleventyConfig.addPassthroughCopy({"../../api/js/etemplate/*/doc/*": "assets/components/"});
|
||||||
eleventyConfig.addPassthroughCopy({"../../node_modules/diff2html/bundles/css/diff2html.min.css": "assets/styles/diff2html.min.css"});
|
eleventyConfig.addPassthroughCopy({"../../node_modules/diff2html/bundles/css/diff2html.min.css": "assets/styles/diff2html.min.css"});
|
||||||
|
|
||||||
//eleventyConfig.addPassthroughCopy({"../../vendor/**/*min.js": "assets/scripts/vendor/"});
|
//eleventyConfig.addPassthroughCopy({"../../vendor/**/*min.js": "assets/scripts/vendor/"});
|
||||||
|
Loading…
Reference in New Issue
Block a user