diff --git a/api/js/etemplate/Et2Box.ts b/api/js/etemplate/Et2Box.ts
index 3ae8489f8b..1bfefdf630 100644
--- a/api/js/etemplate/Et2Box.ts
+++ b/api/js/etemplate/Et2Box.ts
@@ -9,18 +9,18 @@
*/
-import {css, html, LitElement} from "@lion/core";
+import {css, html, LitElement} from "../../../node_modules/@lion/core/index.js";
import {Et2Widget} from "./Et2Widget";
export class Et2Box extends Et2Widget(LitElement)
{
- static get styles()
- {
- return [
- css`
+ static get styles()
+ {
+ return [
+ css`
:host {
- display: block;
- width: 100%;
+ display: block;
+ width: 100%;
}
:host > div {
display: flex;
@@ -31,53 +31,53 @@ export class Et2Box extends Et2Widget(LitElement)
::slotted(*) {
/* CSS for child elements */
}`,
- ];
- }
+ ];
+ }
- render()
- {
- return html`
+ render()
+ {
+ return html`
`;
- }
+ }
- _createNamespace(): boolean
- {
- return true;
- }
+ _createNamespace(): boolean
+ {
+ return true;
+ }
}
customElements.define("et2-box", Et2Box);
export class Et2HBox extends Et2Box
{
- static get styles()
- {
- return [
- ...super.styles,
- css`
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
:host > div {
flex-direction: row;
}`
- ];
- }
+ ];
+ }
}
customElements.define("et2-hbox", Et2HBox);
export class Et2VBox extends Et2Box
{
- static get styles()
- {
- return [
- ...super.styles,
- css`
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
:host > div {
flex-direction: column;
}`
- ];
- }
+ ];
+ }
}
customElements.define("et2-vbox", Et2VBox);
\ No newline at end of file
diff --git a/api/js/etemplate/Et2Button.ts b/api/js/etemplate/Et2Button.ts
index e9a69241f4..3ad63b0b1a 100644
--- a/api/js/etemplate/Et2Button.ts
+++ b/api/js/etemplate/Et2Button.ts
@@ -9,22 +9,22 @@
*/
-import {css, html} from "@lion/core/index.js";
-import {LionButton} from "@lion/button/index.js";
+import {css, html} from "../../../node_modules/@lion/core/index.js";
+import {LionButton} from "../../../node_modules/@lion/button/index.js";
import {Et2InputWidget} from "./et2_core_inputWidget";
import {Et2Widget} from "./Et2Widget";
export class Et2Button extends Et2InputWidget(Et2Widget(LionButton))
{
- protected _created_icon_node: HTMLImageElement;
- protected clicked: boolean = false;
- private image: string;
+ protected _created_icon_node: HTMLImageElement;
+ protected clicked: boolean = false;
+ private image: string;
- static get styles()
- {
- return [
- ...super.styles,
- css`
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
:host {
padding: 1px 8px;
/* These should probably come from somewhere else */
@@ -36,114 +36,115 @@ export class Et2Button extends Et2InputWidget(Et2Widget(LionButton))
width: 20px;
padding-right: 3px;
}`,
- ];
+ ];
+ }
+
+ static get properties()
+ {
+ return {
+ ...super.properties,
+ image: {type: String},
+ onclick: {type: Function}
+ }
+ }
+
+ constructor()
+ {
+ super();
+
+ // Property default values
+ this.image = '';
+
+ // Create icon Element since BXButton puts it as child, but we put it as attribute
+ this._created_icon_node = document.createElement("img");
+ this._created_icon_node.slot = "icon";
+ // Do not add this._icon here, no children can be added in constructor
+
+ // Define a default click handler
+ // If a different one gets set via attribute, it will be used instead
+ this.onclick = (typeof this.onclick === "function") ? this.onclick : () =>
+ {
+ return this.getInstanceManager().submit();
+ };
+ }
+
+ connectedCallback()
+ {
+ super.connectedCallback();
+
+ //this.classList.add("et2_button")
+
+ if (this.image)
+ {
+ this._created_icon_node.src = egw.image(this.image);
+ this.appendChild(this._created_icon_node);
}
- static get properties()
+ this.addEventListener("click", this._handleClick.bind(this));
+ }
+
+
+ _handleClick(event: MouseEvent): boolean
+ {
+ debugger;
+ // ignore click on readonly button
+ if (this.disabled) return false;
+
+ this.clicked = true;
+
+ // Cancel buttons don't trigger the close confirmation prompt
+ if (this.classList.contains("et2_button_cancel"))
{
- return {
- image: {type: String},
- onclick: {type: Function}
- }
+ this.getInstanceManager()?.skip_close_prompt();
}
- constructor()
+ if (!super._handleClick(event))
{
- super();
-
- // Property default values
- this.image = '';
-
- // Create icon Element since BXButton puts it as child, but we put it as attribute
- this._created_icon_node = document.createElement("img");
- this._created_icon_node.slot = "icon";
- // Do not add this._icon here, no children can be added in constructor
-
- // Define a default click handler
- // If a different one gets set via attribute, it will be used instead
- this.onclick = (typeof this.onclick === "function") ? this.onclick : () =>
- {
- return this.getInstanceManager().submit();
- };
+ this.clicked = false;
+ return false;
}
- connectedCallback()
- {
- super.connectedCallback();
+ this.clicked = false;
+ this.getInstanceManager()?.skip_close_prompt(false);
+ return true;
+ }
- //this.classList.add("et2_button")
-
- if (this.image)
- {
- this._created_icon_node.src = egw.image(this.image);
- this.appendChild(this._created_icon_node);
- }
-
- this.addEventListener("click", this._handleClick.bind(this));
- }
-
-
- _handleClick(event: MouseEvent): boolean
- {
- debugger;
- // ignore click on readonly button
- if (this.disabled) return false;
-
- this.clicked = true;
-
- // Cancel buttons don't trigger the close confirmation prompt
- if (this.classList.contains("et2_button_cancel"))
- {
- this.getInstanceManager()?.skip_close_prompt();
- }
-
- if (!super._handleClick(event))
- {
- this.clicked = false;
- return false;
- }
-
- this.clicked = false;
- this.getInstanceManager()?.skip_close_prompt(false);
- return true;
- }
-
- render()
- {
- return html`
+ render()
+ {
+ return html`
`;
- }
+ }
- /**
- * Implementation of the et2_IInput interface
- */
+ /**
+ * Implementation of the et2_IInput interface
+ */
- /**
- * Always return false as a button is never dirty
- */
- isDirty()
+ /**
+ * Always return false as a button is never dirty
+ */
+ isDirty()
+ {
+ return false;
+ }
+
+ resetDirty()
+ {
+ }
+
+ getValue()
+ {
+ if (this.clicked)
{
- return false;
+ return true;
}
- resetDirty()
- {
- }
-
- getValue()
- {
- if (this.clicked)
- {
- return true;
- }
-
- // If "null" is returned, the result is not added to the submitted
- // array.
- return null;
- }
+ // If "null" is returned, the result is not added to the submitted
+ // array.
+ return null;
+ }
}
customElements.define("et2-button", Et2Button);
diff --git a/api/js/etemplate/Et2Date.ts b/api/js/etemplate/Et2Date.ts
new file mode 100644
index 0000000000..4821f0f93c
--- /dev/null
+++ b/api/js/etemplate/Et2Date.ts
@@ -0,0 +1,163 @@
+/**
+ * EGroupware eTemplate2 - Date widget (WebComponent)
+ *
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @package etemplate
+ * @subpackage api
+ * @link https://www.egroupware.org
+ * @author Nathan Gray
+ */
+
+
+import {css, html} from "../../../node_modules/@lion/core/index.js"
+import {LionInputDatepicker} from "../../../node_modules/@lion/input-datepicker/index.js"
+import {Et2InputWidget} from "./et2_core_inputWidget";
+import {Et2Widget} from "./Et2Widget";
+
+
+/**
+ * To parse a date into the right format
+ *
+ * @param {string} dateString
+ * @returns {Date | undefined}
+ */
+export function parseDate(dateString)
+{
+ debugger;
+ let formatString = (egw.preference("dateformat") || 'Y-m-d');
+ formatString = formatString.replaceAll(/-\/\./ig, '-');
+ let parsedString = "";
+ switch (formatString)
+ {
+ case 'd-m-Y':
+ parsedString = `${dateString.slice(6, 10)}/${dateString.slice(
+ 3,
+ 5,
+ )}/${dateString.slice(0, 2)}`;
+ break;
+ case 'm-d-Y':
+ parsedString = `${dateString.slice(6, 10)}/${dateString.slice(
+ 0,
+ 2,
+ )}/${dateString.slice(3, 5)}`;
+ break;
+ case 'Y-m-d':
+ parsedString = `${dateString.slice(0, 4)}/${dateString.slice(
+ 5,
+ 7,
+ )}/${dateString.slice(8, 10)}`;
+ break;
+ case 'd-M-Y':
+ parsedString = `${dateString.slice(6, 10)}/${dateString.slice(
+ 3,
+ 5,
+ )}/${dateString.slice(0, 2)}`;
+ default:
+ parsedString = '0000/00/00';
+ }
+
+ const [year, month, day] = parsedString.split('/').map(Number);
+ const parsedDate = new Date(Date.UTC(year, month - 1, day));
+
+ // Check if parsedDate is not `Invalid Date` or that the date has changed (e.g. the not existing 31.02.2020)
+ if (
+ year > 0 &&
+ month > 0 &&
+ day > 0 &&
+ parsedDate.getDate() === day &&
+ parsedDate.getMonth() === month - 1
+ )
+ {
+ return parsedDate;
+ }
+ return undefined;
+}
+
+/**
+ * Format dates according to user preference
+ *
+ * @param {Date} date
+ * @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
+ * set 'dateFormat': "Y-m-d" to specify a particular format
+ * @returns {string}
+ */
+export function formatDate(date: Date, options): string
+{
+ debugger;
+ if (!date || !(date instanceof Date))
+ {
+ return "";
+ }
+ let _value = '';
+ // Add timezone offset back in, or formatDate will lose those hours
+ let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
+
+ let dateformat = options.dateFormat || egw.preference("dateformat") || 'Y-m-d';
+
+ var replace_map = {
+ d: "" + date.getUTCDate(),
+ m: "" + date.getUTCMonth() + 1,
+ Y: "" + date.getUTCFullYear()
+ }
+ var re = new RegExp(Object.keys(replace_map).join("|"), "gi");
+ _value = dateformat.replace(re, function (matched)
+ {
+ return replace_map[matched];
+ });
+ return _value;
+}
+
+export class Et2Date extends Et2InputWidget(Et2Widget(LionInputDatepicker))
+{
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
+ /* Custom CSS */
+ `,
+ ];
+ }
+
+ static get properties()
+ {
+ return {
+ ...super.properties,
+ value: {
+ attribute: true,
+ converter: {
+ toAttribute(value)
+ {
+ return value ? value.toJSON().replace(/\.\d{3}Z$/, 'Z') : "";
+ },
+ fromAttribute(value)
+ {
+ return new Date(value);
+ }
+ }
+ },
+ }
+ }
+
+ constructor()
+ {
+ super();
+ this.parser = parseDate;
+ this.formatter = formatDate;
+ }
+
+ connectedCallback()
+ {
+ super.connectedCallback();
+
+ }
+
+
+ getValue()
+ {
+ debugger;
+ return this.modelValue ? this.modelValue.toJSON().replace(/\.\d{3}Z$/, 'Z') : "";
+ }
+}
+
+customElements.define("et2-date", Et2Date);
diff --git a/api/js/etemplate/Et2Textbox.ts b/api/js/etemplate/Et2Textbox.ts
index 6ed2fd30e0..09d86f37e6 100644
--- a/api/js/etemplate/Et2Textbox.ts
+++ b/api/js/etemplate/Et2Textbox.ts
@@ -1,5 +1,5 @@
/**
- * EGroupware eTemplate2 - Button widget
+ * EGroupware eTemplate2 - Textbox widget (WebComponent)
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
@@ -9,45 +9,45 @@
*/
-import {css, html} from "@lion/core";
-import {LionInput} from "@lion/input";
+import {css, html} from "../../../node_modules/@lion/core/index.js"
+import {LionInput} from "../../../node_modules/@lion/input/index.js"
import {Et2InputWidget} from "./et2_core_inputWidget";
import {Et2Widget} from "./Et2Widget";
export class Et2Textbox extends Et2InputWidget(Et2Widget(LionInput))
{
- static get styles()
- {
- return [
- ...super.styles,
- css`
- /* Custom CSS */
- `,
- ];
+ static get styles()
+ {
+ return [
+ ...super.styles,
+ css`
+ /* Custom CSS */
+ `,
+ ];
+ }
+
+ static get properties()
+ {
+ return {
+ ...super.properties,
+ value: {attribute: true},
+ onclick: {type: Function}
}
+ }
- static get properties()
- {
- return {
- ...super.properties,
- value: {attribute: true},
- onclick: {type: Function}
- }
- }
+ constructor()
+ {
+ debugger;
+ super();
- constructor()
- {
- debugger;
- super();
+ }
- }
+ connectedCallback()
+ {
+ super.connectedCallback();
- connectedCallback()
- {
- super.connectedCallback();
-
- }
+ }
}
customElements.define("et2-textbox", Et2Textbox);
diff --git a/api/js/etemplate/Et2Widget.ts b/api/js/etemplate/Et2Widget.ts
index 12c0e91aeb..a4c5ad16ba 100644
--- a/api/js/etemplate/Et2Widget.ts
+++ b/api/js/etemplate/Et2Widget.ts
@@ -24,754 +24,749 @@ import {LitElement} from "@lion/core";
type Constructor = new (...args: any[]) => T;
export const Et2Widget = >(superClass: T) =>
{
- class Et2WidgetClass extends superClass implements et2_IDOMNode
+ class Et2WidgetClass extends superClass implements et2_IDOMNode
+ {
+
+ /** et2_widget compatability **/
+ protected _mgrs: et2_arrayMgr[] = [];
+ protected _parent: Et2WidgetClass | et2_widget | null = null;
+ private _inst: etemplate2 | null = null;
+ private supportedWidgetClasses = [];
+
+ /**
+ * Not actually required by et2_widget, but needed to keep track of non-webComponent children
+ */
+ private _legacy_children: et2_widget[] = [];
+
+ /**
+ * Properties
+ */
+ private label: string = "";
+ private statustext: string = "";
+
+
+ /** WebComponent **/
+ static get properties()
{
-
- /** et2_widget compatability **/
- protected _mgrs: et2_arrayMgr[] = [];
- protected _parent: Et2WidgetClass | et2_widget | null = null;
- private _inst: etemplate2 | null = null;
- private supportedWidgetClasses = [];
+ return {
+ ...super.properties,
/**
- * Not actually required by et2_widget, but needed to keep track of non-webComponent children
+ * Tooltip which is shown for this element on hover
*/
- private _legacy_children: et2_widget[] = [];
+ statustext: {type: String},
- /**
- * Properties
- */
- private label: string = "";
- private statustext: string = "";
-
-
- /** WebComponent **/
- static get properties()
- {
- return {
- ...super.properties,
-
- /**
- * Tooltip which is shown for this element on hover
- */
- statustext: {type: String},
-
- label: {type: String},
- onclick: {
- type: Function,
- converter: (value) =>
- {
- debugger;
- return et2_compileLegacyJS(value, this, this);
- }
- }
- };
- }
-
- /**
- * Widget Mixin constructor
- *
- * Note the ...args parameter and super() call
- *
- * @param args
- */
- constructor(...args: any[])
- {
- super(...args);
- }
-
- connectedCallback()
- {
- super.connectedCallback();
-
- this.set_label(this.label);
-
- if (this.statustext)
- {
- this.egw().tooltipBind(this, this.statustext);
- }
- }
-
- disconnectedCallback()
- {
- this.egw().tooltipUnbind(this);
- }
-
- /**
- * NOT the setter, since we cannot add to the DOM before connectedCallback()
- *
- * TODO: This is not best practice. Should just set property, DOM modification should be done in render
- * https://lit-element.polymer-project.org/guide/templates#design-a-performant-template
- *
- * @param value
- */
- set_label(value)
- {
- let oldValue = this.label;
-
- // Remove old
- let oldLabels = this.getElementsByClassName("et2_label");
- while (oldLabels[0])
- {
- this.removeChild(oldLabels[0]);
- }
-
- this.label = value;
- if (value)
- {
- let label = document.createElement("span");
- label.classList.add("et2_label");
- label.textContent = this.label;
- // We should have a slot in the template for the label
- //label.slot="label";
- this.appendChild(label);
- this.requestUpdate('label', oldValue);
- }
- }
-
- /**
- * Event handlers
- */
-
- /**
- * Click handler calling custom handler set via onclick attribute to this.onclick
- *
- * @param _ev
- * @returns
- */
- _handleClick(_ev: MouseEvent): boolean
- {
- if (typeof this.onclick == 'function')
- {
- // Make sure function gets a reference to the widget, splice it in as 2. argument if not
- var args = Array.prototype.slice.call(arguments);
- if (args.indexOf(this) == -1) args.splice(1, 0, this);
-
- return this.onclick.apply(this, args);
- }
-
- return true;
- }
-
- /** et2_widget compatability **/
- destroy()
- {
- // Not really needed, use the disconnectedCallback() and let the browser handle it
- }
-
- isInTree(): boolean
- {
- // TODO: Probably should watch the state or something
- return true;
- }
-
- /**
- * Loads the widget tree from an XML node
- *
- * @param _node xml node
- */
- loadFromXML(_node)
- {
- // Load the child nodes.
- for (var i = 0; i < _node.childNodes.length; i++)
- {
- var node = _node.childNodes[i];
- var widgetType = node.nodeName.toLowerCase();
-
- if (widgetType == "#comment")
- {
- continue;
- }
-
- if (widgetType == "#text")
- {
- if (node.data.replace(/^\s+|\s+$/g, ''))
- {
- this.innerText = node.data;
- }
- continue;
- }
-
- // Create the new element
- this.createElementFromNode(node);
- }
- }
-
- /**
- * Create a et2_widget from an XML node.
- *
- * First the type and attributes are read from the node. Then the readonly & modifications
- * arrays are checked for changes specific to the loaded data. Then the appropriate
- * constructor is called. After the constructor returns, the widget has a chance to
- * further initialize itself from the XML node when the widget's loadFromXML() method
- * is called with the node.
- *
- * @param _node XML node to read
- * @param _name XML node name
- *
- * @return et2_widget
- */
- createElementFromNode(_node, _name?)
- {
- var attributes = {};
+ label: {type: String},
+ onclick: {
+ type: Function,
+ converter: (value) =>
+ {
debugger;
- // Parse the "readonly" and "type" flag for this element here, as they
- // determine which constructor is used
- var _nodeName = attributes["type"] = _node.getAttribute("type") ?
- _node.getAttribute("type") : _node.nodeName.toLowerCase();
- var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
- (this.getArrayMgr("readonlys")).isReadOnly(
- _node.getAttribute("id"), _node.getAttribute("readonly"),
- typeof this.readonly !== "undefined" ? this.readonly : false) : false;
-
- // Check to see if modifications change type
- var modifications = this.getArrayMgr("modifications");
- if (modifications && _node.getAttribute("id"))
- {
- let entry: any = modifications.getEntry(_node.getAttribute("id"));
- if (entry == null)
- {
- // Try again, but skip the fancy stuff
- // TODO: Figure out why the getEntry() call doesn't always work
- entry = modifications.data[_node.getAttribute("id")];
- if (entry)
- {
- this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry);
- }
- else
- {
- // Try the root, in case a namespace got missed
- entry = modifications.getRoot().getEntry(_node.getAttribute("id"));
- }
- }
- if (entry && entry.type && typeof entry.type === 'string')
- {
- _nodeName = attributes["type"] = entry.type;
- }
- entry = null;
- }
-
- // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"),
- // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs!
- if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
- {
- _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
- }
-
- let widget = null;
- if (undefined == window.customElements.get(_nodeName))
- {
- // Get the constructor - if the widget is readonly, use the special "_ro"
- // constructor if it is available
- var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
- if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
- {
- constructor = et2_registry[_nodeName + "_ro"];
- }
-
- // Parse the attributes from the given XML attributes object
- this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
-
- // Do an sanity check for the attributes
- ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
-
- // Creates the new widget, passes this widget as an instance and
- // passes the widgetType. Then it goes on loading the XML for it.
- widget = new constructor(this, attributes);
-
- // Load the widget itself from XML
- widget.loadFromXML(_node);
- }
- else
- {
- widget = this.loadWebComponent(_nodeName, _node);
-
- if (this.addChild)
- {
- // webcomponent going into old et2_widget
- this.addChild(widget);
- }
- }
- return widget;
+ return et2_compileLegacyJS(value, this, this);
+ }
}
-
- /**
- * Load a Web Component
- * @param _nodeName
- * @param _node
- */
- loadWebComponent(_nodeName: string, _node): HTMLElement
- {
- let widget = document.createElement(_nodeName);
- widget.textContent = _node.textContent;
-
- const widget_class = window.customElements.get(_nodeName);
- if (!widget_class)
- {
- throw Error("Unknown or unregistered WebComponent '" + _nodeName + "', could not find class");
- }
- widget.setParent(this);
- var mgr = widget.getArrayMgr("content");
- debugger;
- // Apply any set attributes - widget will do its own coercion
- _node.getAttributeNames().forEach(attribute =>
- {
- let attrValue = _node.getAttribute(attribute);
-
- // If there is not attribute set, ignore it. Widget sets its own default.
- if (typeof attrValue === "undefined") return;
-
- // If the attribute is marked as boolean, parse the
- // expression as bool expression.
- if (widget_class.getPropertyOptions(attribute).type == "Boolean")
- {
- attrValue = mgr.parseBoolExpression(attrValue);
- }
- else
- {
- attrValue = mgr.expandName(attrValue);
- }
- widget.setAttribute(attribute, attrValue);
- });
-
- if (widget_class.getPropertyOptions("value") && widget.set_value)
- {
- if (mgr != null)
- {
- let val = mgr.getEntry(widget.id, false, true);
- if (val !== null)
- {
- widget.setAttribute("value", val);
- }
- }
- // Check for already inside namespace
- if (this._createNamespace() && this.getArrayMgr("content").perspectiveData.owner == this)
- {
- widget.setAttribute("value", this.getArrayMgr("content").data);
- }
- }
-
- // Children need to be loaded
- widget.loadFromXML(_node);
-
- return widget;
- }
-
- /**
- * The parseXMLAttrs function takes an XML DOM attributes object
- * and adds the given attributes to the _target associative array. This
- * function also parses the legacyOptions.
- *
- * @param _attrsObj is the XML DOM attributes object
- * @param {object} _target is the object to which the attributes should be written.
- * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute
- */
- parseXMLAttrs(_attrsObj, _target, _proto)
- {
- // Check whether the attributes object is really existing, if not abort
- if (typeof _attrsObj == "undefined")
- {
- return;
- }
-
- // Iterate over the given attributes and parse them
- var mgr = this.getArrayMgr("content");
- for (var i = 0; i < _attrsObj.length; i++)
- {
- var attrName = _attrsObj[i].name;
- var attrValue = _attrsObj[i].value;
-
- // Special handling for the legacy options
- if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
- {
- let legacy = _proto.constructor.legacyOptions || [];
- let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
- // Check for modifications on legacy options here. Normal modifications
- // are handled in widget constructor, but it's too late for legacy options then
- if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
- {
- var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
- if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
- }
- // expand legacyOptions with content
- if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
- {
- attrValue = mgr.expandName(attrValue);
- }
-
- // Parse the legacy options (as a string, other types not allowed)
- var splitted = et2_csvSplit(attrValue + "");
-
- for (var j = 0; j < splitted.length && j < legacy.length; j++)
- {
- // Blank = not set, unless there's more legacy options provided after
- if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
-
- // Check to make sure we don't overwrite a current option with a legacy option
- if (typeof _target[legacy[j]] === "undefined")
- {
- attrValue = splitted[j];
-
- /**
- If more legacy options than expected, stuff them all in the last legacy option
- Some legacy options take a comma separated list.
- */
- if (j == legacy.length - 1 && splitted.length > legacy.length)
- {
- attrValue = splitted.slice(j);
- }
-
- var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {};
-
- // If the attribute is marked as boolean, parse the
- // expression as bool expression.
- if (attr.type == "boolean")
- {
- attrValue = mgr.parseBoolExpression(attrValue);
- }
- else if (typeof attrValue != "object")
- {
- attrValue = mgr.expandName(attrValue);
- }
- _target[legacy[j]] = attrValue;
- }
- }
- }
- else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
- {
- // do NOT overwrite already evaluated readonly attribute
- }
- else
- {
- let attrs = et2_attribute_registry[_proto.constructor.name] || {};
- if (mgr != null && typeof attrs[attrName] != "undefined")
- {
- var attr = attrs[attrName];
-
- // If the attribute is marked as boolean, parse the
- // expression as bool expression.
- if (attr.type == "boolean")
- {
- attrValue = mgr.parseBoolExpression(attrValue);
- }
- else
- {
- attrValue = mgr.expandName(attrValue);
- }
- }
-
- // Set the attribute
- _target[attrName] = attrValue;
- }
- }
- }
-
- iterateOver(_callback: Function, _context, _type)
- {
- if (et2_implements_registry[_type] && et2_implements_registry[_type](this))
- {
- _callback.call(_context, this);
- }
- // TODO: children
- }
-
- /**
- * Needed for legacy compatability.
- *
- * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed.
- */
- loadingFinished(promises)
- {
- /**
- * This is needed mostly as a bridge between non-WebComponent widgets and
- * connectedCallback(). It's not really needed if the whole tree is WebComponent.
- * WebComponents can be added as children immediately after creation, and they handle the
- * rest themselves with their normal lifecycle (especially connectedCallback(), which is kind
- * of the equivalent of doLoadingFinished()
- */
- if (this.getParent() instanceof et2_widget)
- {
- this.getParent().getDOMNode(this).append(this);
- }
-
- for (let i = 0; i < this._legacy_children.length; i++)
- {
- let child = this._legacy_children[i];
- let child_node = typeof child.getDOMNode !== "undefined" ? child.getDOMNode(child) : null;
- if (child_node && child_node !== this)
- {
- this.append(child_node);
- }
- child.loadingFinished(promises);
- }
- }
-
- getWidgetById(_id)
- {
- if (this.id == _id)
- {
- return this;
- }
- }
-
- setParent(new_parent: Et2WidgetClass | et2_widget)
- {
- this._parent = new_parent;
-
- if (this.id)
- {
- // Create a namespace for this object
- if (this._createNamespace())
- {
- this.checkCreateNamespace();
- }
- }
-
- }
-
- getParent(): HTMLElement | et2_widget
- {
- let parentNode = this.parentNode;
-
- // If parent is an old et2_widget, use it
- if (this._parent)
- {
- return this._parent;
- }
-
- return parentNode;
- }
-
- addChild(child: et2_widget | Et2WidgetClass)
- {
- if (child instanceof et2_widget)
- {
- child._parent = this;
-
- // During legacy widget creation, the child's DOM node won't be available yet.
- this._legacy_children.push(child);
- let child_node = typeof child.getDOMNode !== "undefined" ? child.getDOMNode(child) : null;
- if (child_node && child_node !== this)
- {
- this.append(child_node);
- }
- }
- else
- {
- this.append(child);
- }
- }
-
- /**
- * Get [legacy] children
- * Use .children to get web component children
- * @returns {et2_widget[]}
- */
- getChildren()
- {
- return this._legacy_children;
- }
-
- getType(): string
- {
- return this.nodeName;
- }
-
- getDOMNode(): HTMLElement
- {
- return this;
- }
-
- /**
- * Sets the array manager for the given part
- *
- * @param {string} _part which array mgr to set
- * @param {object} _mgr
- */
- setArrayMgr(_part: string, _mgr: et2_arrayMgr)
- {
- this._mgrs[_part] = _mgr;
- }
-
- /**
- * Returns the array manager object for the given part
- *
- * @param {string} managed_array_type name of array mgr to return
- */
- getArrayMgr(managed_array_type: string): et2_arrayMgr | null
- {
- if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
- {
- return this._mgrs[managed_array_type];
- }
- else if (this.getParent())
- {
- return this.getParent().getArrayMgr(managed_array_type);
- }
-
- return null;
- }
-
- /**
- * Sets all array manager objects - this function can be used to set the
- * root array managers of the container object.
- *
- * @param {object} _mgrs
- */
- setArrayMgrs(_mgrs)
- {
- this._mgrs = et2_cloneObject(_mgrs);
- }
-
- /**
- * Returns an associative array containing the top-most array managers.
- *
- * @param _mgrs is used internally and should not be supplied.
- */
- getArrayMgrs(_mgrs?: object)
- {
- if (typeof _mgrs == "undefined")
- {
- _mgrs = {};
- }
-
- // Add all managers of this object to the result, if they have not already
- // been set in the result
- for (var key in this._mgrs)
- {
- if (typeof _mgrs[key] == "undefined")
- {
- _mgrs[key] = this._mgrs[key];
- }
- }
-
- // Recursively applies this function to the parent widget
- if (this._parent)
- {
- this._parent.getArrayMgrs(_mgrs);
- }
-
- return _mgrs;
- }
-
- /**
- * Checks whether a namespace exists for this element in the content array.
- * If yes, an own perspective of the content array is created. If not, the
- * parent content manager is used.
- *
- * Constructor attributes are passed in case a child needs to make decisions
- */
- checkCreateNamespace()
- {
- // Get the content manager
- var mgrs = this.getArrayMgrs();
-
- for (var key in mgrs)
- {
- var mgr = mgrs[key];
-
- // Get the original content manager if we have already created a
- // perspective for this node
- if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
- {
- mgr = mgr.parentMgr;
- }
-
- // Check whether the manager has a namespace for the id of this object
- var entry = mgr.getEntry(this.id);
- if (typeof entry === 'object' && entry !== null || this.id)
- {
- // The content manager has an own node for this object, so
- // create an own perspective.
- this._mgrs[key] = mgr.openPerspective(this, this.id);
- }
- else
- {
- // The current content manager does not have an own namespace for
- // this element, so use the content manager of the parent.
- delete (this._mgrs[key]);
- }
- }
- }
-
- /**
- * Returns the instance manager
- *
- * @return {etemplate2}
- */
- getInstanceManager()
- {
- if (this._inst != null)
- {
- return this._inst;
- }
- else if (this.getParent())
- {
- return this.getParent().getInstanceManager();
- }
-
- return null;
- }
-
- /**
- * Returns the path into the data array. By default, array manager takes care of
- * this, but some extensions need to override this
- */
- getPath()
- {
- var path = this.getArrayMgr("content").getPath();
-
- // Prevent namespaced widgets with value from going an extra layer deep
- if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop();
-
- return path;
- }
-
- _createNamespace(): boolean
- {
- return false;
- }
-
- egw(): IegwAppLocal
- {
- if (this.getParent() != null && !(this.getParent() instanceof HTMLElement))
- {
- return (this.getParent()).egw();
- }
-
- // Get the window this object belongs to
- var wnd = null;
- // @ts-ignore Technically this doesn't have implements(), but it's mixed in
- if (this.implements(et2_IDOMNode))
- {
- var node = (this).getDOMNode();
- if (node && node.ownerDocument)
- {
- wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
- }
- }
-
- // If we're the root object, return the phpgwapi API instance
- return egw('phpgwapi', wnd);
- }
- };
-
- function applyMixins(derivedCtor: any, baseCtors: any[])
- {
- baseCtors.forEach(baseCtor =>
- {
- Object.getOwnPropertyNames(baseCtor.prototype).forEach(name =>
- {
- if (name !== 'constructor')
- {
- derivedCtor.prototype[name] = baseCtor.prototype[name];
- }
- });
- });
+ };
}
- // Add some more stuff in
- applyMixins(Et2WidgetClass, [ClassWithInterfaces]);
+ /**
+ * Widget Mixin constructor
+ *
+ * Note the ...args parameter and super() call
+ *
+ * @param args
+ */
+ constructor(...args: any[])
+ {
+ super(...args);
+ }
- return Et2WidgetClass as unknown as Constructor & T;
+ connectedCallback()
+ {
+ super.connectedCallback();
+
+ this.set_label(this.label);
+
+ if (this.statustext)
+ {
+ this.egw().tooltipBind(this, this.statustext);
+ }
+ }
+
+ disconnectedCallback()
+ {
+ this.egw().tooltipUnbind(this);
+ }
+
+ /**
+ * NOT the setter, since we cannot add to the DOM before connectedCallback()
+ *
+ * TODO: This is not best practice. Should just set property, DOM modification should be done in render
+ * https://lit-element.polymer-project.org/guide/templates#design-a-performant-template
+ *
+ * @param value
+ */
+ set_label(value)
+ {
+ let oldValue = this.label;
+
+ // Remove old
+ let oldLabels = this.getElementsByClassName("et2_label");
+ while (oldLabels[0])
+ {
+ this.removeChild(oldLabels[0]);
+ }
+
+ this.label = value;
+ if (value)
+ {
+ let label = document.createElement("span");
+ label.classList.add("et2_label");
+ label.textContent = this.label;
+ // We should have a slot in the template for the label
+ //label.slot="label";
+ this.appendChild(label);
+ this.requestUpdate('label', oldValue);
+ }
+ }
+
+ /**
+ * Event handlers
+ */
+
+ /**
+ * Click handler calling custom handler set via onclick attribute to this.onclick
+ *
+ * @param _ev
+ * @returns
+ */
+ _handleClick(_ev: MouseEvent): boolean
+ {
+ if (typeof this.onclick == 'function')
+ {
+ // Make sure function gets a reference to the widget, splice it in as 2. argument if not
+ var args = Array.prototype.slice.call(arguments);
+ if (args.indexOf(this) == -1) args.splice(1, 0, this);
+
+ return this.onclick.apply(this, args);
+ }
+
+ return true;
+ }
+
+ /** et2_widget compatability **/
+ destroy()
+ {
+ // Not really needed, use the disconnectedCallback() and let the browser handle it
+ }
+
+ isInTree(): boolean
+ {
+ // TODO: Probably should watch the state or something
+ return true;
+ }
+
+ /**
+ * Loads the widget tree from an XML node
+ *
+ * @param _node xml node
+ */
+ loadFromXML(_node)
+ {
+ // Load the child nodes.
+ for (var i = 0; i < _node.childNodes.length; i++)
+ {
+ var node = _node.childNodes[i];
+ var widgetType = node.nodeName.toLowerCase();
+
+ if (widgetType == "#comment")
+ {
+ continue;
+ }
+
+ if (widgetType == "#text")
+ {
+ if (node.data.replace(/^\s+|\s+$/g, ''))
+ {
+ this.innerText = node.data;
+ }
+ continue;
+ }
+
+ // Create the new element
+ this.createElementFromNode(node);
+ }
+ }
+
+ /**
+ * Create a et2_widget from an XML node.
+ *
+ * First the type and attributes are read from the node. Then the readonly & modifications
+ * arrays are checked for changes specific to the loaded data. Then the appropriate
+ * constructor is called. After the constructor returns, the widget has a chance to
+ * further initialize itself from the XML node when the widget's loadFromXML() method
+ * is called with the node.
+ *
+ * @param _node XML node to read
+ * @param _name XML node name
+ *
+ * @return et2_widget
+ */
+ createElementFromNode(_node, _name?)
+ {
+ var attributes = {};
+ debugger;
+ // Parse the "readonly" and "type" flag for this element here, as they
+ // determine which constructor is used
+ var _nodeName = attributes["type"] = _node.getAttribute("type") ?
+ _node.getAttribute("type") : _node.nodeName.toLowerCase();
+ var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
+ (this.getArrayMgr("readonlys")).isReadOnly(
+ _node.getAttribute("id"), _node.getAttribute("readonly"),
+ typeof this.readonly !== "undefined" ? this.readonly : false) : false;
+
+ // Check to see if modifications change type
+ var modifications = this.getArrayMgr("modifications");
+ if (modifications && _node.getAttribute("id"))
+ {
+ let entry: any = modifications.getEntry(_node.getAttribute("id"));
+ if (entry == null)
+ {
+ // Try again, but skip the fancy stuff
+ // TODO: Figure out why the getEntry() call doesn't always work
+ entry = modifications.data[_node.getAttribute("id")];
+ if (entry)
+ {
+ this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry);
+ }
+ else
+ {
+ // Try the root, in case a namespace got missed
+ entry = modifications.getRoot().getEntry(_node.getAttribute("id"));
+ }
+ }
+ if (entry && entry.type && typeof entry.type === 'string')
+ {
+ _nodeName = attributes["type"] = entry.type;
+ }
+ entry = null;
+ }
+
+ // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"),
+ // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs!
+ if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
+ {
+ _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
+ }
+
+ let widget = null;
+ if (undefined == window.customElements.get(_nodeName))
+ {
+ // Get the constructor - if the widget is readonly, use the special "_ro"
+ // constructor if it is available
+ var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
+ if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
+ {
+ constructor = et2_registry[_nodeName + "_ro"];
+ }
+
+ // Parse the attributes from the given XML attributes object
+ this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
+
+ // Do an sanity check for the attributes
+ ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
+
+ // Creates the new widget, passes this widget as an instance and
+ // passes the widgetType. Then it goes on loading the XML for it.
+ widget = new constructor(this, attributes);
+
+ // Load the widget itself from XML
+ widget.loadFromXML(_node);
+ }
+ else
+ {
+ widget = this.loadWebComponent(_nodeName, _node);
+
+ if (this.addChild)
+ {
+ // webcomponent going into old et2_widget
+ this.addChild(widget);
+ }
+ }
+ return widget;
+ }
+
+ /**
+ * Load a Web Component
+ * @param _nodeName
+ * @param _node
+ */
+ loadWebComponent(_nodeName: string, _node): HTMLElement
+ {
+ let widget = document.createElement(_nodeName);
+ widget.textContent = _node.textContent;
+
+ const widget_class = window.customElements.get(_nodeName);
+ if (!widget_class)
+ {
+ throw Error("Unknown or unregistered WebComponent '" + _nodeName + "', could not find class");
+ }
+ widget.setParent(this);
+ var mgr = widget.getArrayMgr("content");
+ debugger;
+ // Apply any set attributes - widget will do its own coercion
+ _node.getAttributeNames().forEach(attribute =>
+ {
+ let attrValue = _node.getAttribute(attribute);
+
+ // If there is not attribute set, ignore it. Widget sets its own default.
+ if (typeof attrValue === "undefined") return;
+
+ // If the attribute is marked as boolean, parse the
+ // expression as bool expression.
+ if (widget_class.getPropertyOptions(attribute).type == "Boolean")
+ {
+ attrValue = mgr.parseBoolExpression(attrValue);
+ }
+ else
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
+ widget.setAttribute(attribute, attrValue);
+ });
+
+ if (widget_class.getPropertyOptions("value") && widget.set_value)
+ {
+ if (mgr != null)
+ {
+ let val = mgr.getEntry(widget.id, false, true);
+ if (val !== null)
+ {
+ widget.set_value(val);
+ }
+ }
+ }
+
+ // Children need to be loaded
+ widget.loadFromXML(_node);
+
+ return widget;
+ }
+
+ /**
+ * The parseXMLAttrs function takes an XML DOM attributes object
+ * and adds the given attributes to the _target associative array. This
+ * function also parses the legacyOptions.
+ *
+ * @param _attrsObj is the XML DOM attributes object
+ * @param {object} _target is the object to which the attributes should be written.
+ * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute
+ */
+ parseXMLAttrs(_attrsObj, _target, _proto)
+ {
+ // Check whether the attributes object is really existing, if not abort
+ if (typeof _attrsObj == "undefined")
+ {
+ return;
+ }
+
+ // Iterate over the given attributes and parse them
+ var mgr = this.getArrayMgr("content");
+ for (var i = 0; i < _attrsObj.length; i++)
+ {
+ var attrName = _attrsObj[i].name;
+ var attrValue = _attrsObj[i].value;
+
+ // Special handling for the legacy options
+ if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
+ {
+ let legacy = _proto.constructor.legacyOptions || [];
+ let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
+ // Check for modifications on legacy options here. Normal modifications
+ // are handled in widget constructor, but it's too late for legacy options then
+ if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
+ {
+ var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
+ if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
+ }
+ // expand legacyOptions with content
+ if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
+
+ // Parse the legacy options (as a string, other types not allowed)
+ var splitted = et2_csvSplit(attrValue + "");
+
+ for (var j = 0; j < splitted.length && j < legacy.length; j++)
+ {
+ // Blank = not set, unless there's more legacy options provided after
+ if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
+
+ // Check to make sure we don't overwrite a current option with a legacy option
+ if (typeof _target[legacy[j]] === "undefined")
+ {
+ attrValue = splitted[j];
+
+ /**
+ If more legacy options than expected, stuff them all in the last legacy option
+ Some legacy options take a comma separated list.
+ */
+ if (j == legacy.length - 1 && splitted.length > legacy.length)
+ {
+ attrValue = splitted.slice(j);
+ }
+
+ var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {};
+
+ // If the attribute is marked as boolean, parse the
+ // expression as bool expression.
+ if (attr.type == "boolean")
+ {
+ attrValue = mgr.parseBoolExpression(attrValue);
+ }
+ else if (typeof attrValue != "object")
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
+ _target[legacy[j]] = attrValue;
+ }
+ }
+ }
+ else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
+ {
+ // do NOT overwrite already evaluated readonly attribute
+ }
+ else
+ {
+ let attrs = et2_attribute_registry[_proto.constructor.name] || {};
+ if (mgr != null && typeof attrs[attrName] != "undefined")
+ {
+ var attr = attrs[attrName];
+
+ // If the attribute is marked as boolean, parse the
+ // expression as bool expression.
+ if (attr.type == "boolean")
+ {
+ attrValue = mgr.parseBoolExpression(attrValue);
+ }
+ else
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
+ }
+
+ // Set the attribute
+ _target[attrName] = attrValue;
+ }
+ }
+ }
+
+ iterateOver(_callback: Function, _context, _type)
+ {
+ if (et2_implements_registry[_type] && et2_implements_registry[_type](this))
+ {
+ _callback.call(_context, this);
+ }
+ // TODO: children
+ }
+
+ /**
+ * Needed for legacy compatability.
+ *
+ * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed.
+ */
+ loadingFinished(promises)
+ {
+ /**
+ * This is needed mostly as a bridge between non-WebComponent widgets and
+ * connectedCallback(). It's not really needed if the whole tree is WebComponent.
+ * WebComponents can be added as children immediately after creation, and they handle the
+ * rest themselves with their normal lifecycle (especially connectedCallback(), which is kind
+ * of the equivalent of doLoadingFinished()
+ */
+ if (this.getParent() instanceof et2_widget)
+ {
+ this.getParent().getDOMNode(this).append(this);
+ }
+
+ for (let i = 0; i < this._legacy_children.length; i++)
+ {
+ let child = this._legacy_children[i];
+ let child_node = typeof child.getDOMNode !== "undefined" ? child.getDOMNode(child) : null;
+ if (child_node && child_node !== this)
+ {
+ this.append(child_node);
+ }
+ child.loadingFinished(promises);
+ }
+ }
+
+ getWidgetById(_id)
+ {
+ if (this.id == _id)
+ {
+ return this;
+ }
+ }
+
+ setParent(new_parent: Et2WidgetClass | et2_widget)
+ {
+ this._parent = new_parent;
+
+ if (this.id)
+ {
+ // Create a namespace for this object
+ if (this._createNamespace())
+ {
+ this.checkCreateNamespace();
+ }
+ }
+
+ }
+
+ getParent(): HTMLElement | et2_widget
+ {
+ let parentNode = this.parentNode;
+
+ // If parent is an old et2_widget, use it
+ if (this._parent)
+ {
+ return this._parent;
+ }
+
+ return parentNode;
+ }
+
+ addChild(child: et2_widget | Et2WidgetClass)
+ {
+ if (child instanceof et2_widget)
+ {
+ child._parent = this;
+
+ // During legacy widget creation, the child's DOM node won't be available yet.
+ this._legacy_children.push(child);
+ let child_node = typeof child.getDOMNode !== "undefined" ? child.getDOMNode(child) : null;
+ if (child_node && child_node !== this)
+ {
+ this.append(child_node);
+ }
+ }
+ else
+ {
+ this.append(child);
+ }
+ }
+
+ /**
+ * Get [legacy] children
+ * Use .children to get web component children
+ * @returns {et2_widget[]}
+ */
+ getChildren()
+ {
+ return this._legacy_children;
+ }
+
+ getType(): string
+ {
+ return this.nodeName;
+ }
+
+ getDOMNode(): HTMLElement
+ {
+ return this;
+ }
+
+ /**
+ * Sets the array manager for the given part
+ *
+ * @param {string} _part which array mgr to set
+ * @param {object} _mgr
+ */
+ setArrayMgr(_part: string, _mgr: et2_arrayMgr)
+ {
+ this._mgrs[_part] = _mgr;
+ }
+
+ /**
+ * Returns the array manager object for the given part
+ *
+ * @param {string} managed_array_type name of array mgr to return
+ */
+ getArrayMgr(managed_array_type: string): et2_arrayMgr | null
+ {
+ if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
+ {
+ return this._mgrs[managed_array_type];
+ }
+ else if (this.getParent())
+ {
+ return this.getParent().getArrayMgr(managed_array_type);
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets all array manager objects - this function can be used to set the
+ * root array managers of the container object.
+ *
+ * @param {object} _mgrs
+ */
+ setArrayMgrs(_mgrs)
+ {
+ this._mgrs = et2_cloneObject(_mgrs);
+ }
+
+ /**
+ * Returns an associative array containing the top-most array managers.
+ *
+ * @param _mgrs is used internally and should not be supplied.
+ */
+ getArrayMgrs(_mgrs?: object)
+ {
+ if (typeof _mgrs == "undefined")
+ {
+ _mgrs = {};
+ }
+
+ // Add all managers of this object to the result, if they have not already
+ // been set in the result
+ for (var key in this._mgrs)
+ {
+ if (typeof _mgrs[key] == "undefined")
+ {
+ _mgrs[key] = this._mgrs[key];
+ }
+ }
+
+ // Recursively applies this function to the parent widget
+ if (this._parent)
+ {
+ this._parent.getArrayMgrs(_mgrs);
+ }
+
+ return _mgrs;
+ }
+
+ /**
+ * Checks whether a namespace exists for this element in the content array.
+ * If yes, an own perspective of the content array is created. If not, the
+ * parent content manager is used.
+ *
+ * Constructor attributes are passed in case a child needs to make decisions
+ */
+ checkCreateNamespace()
+ {
+ // Get the content manager
+ var mgrs = this.getArrayMgrs();
+
+ for (var key in mgrs)
+ {
+ var mgr = mgrs[key];
+
+ // Get the original content manager if we have already created a
+ // perspective for this node
+ if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
+ {
+ mgr = mgr.parentMgr;
+ }
+
+ // Check whether the manager has a namespace for the id of this object
+ var entry = mgr.getEntry(this.id);
+ if (typeof entry === 'object' && entry !== null || this.id)
+ {
+ // The content manager has an own node for this object, so
+ // create an own perspective.
+ this._mgrs[key] = mgr.openPerspective(this, this.id);
+ }
+ else
+ {
+ // The current content manager does not have an own namespace for
+ // this element, so use the content manager of the parent.
+ delete (this._mgrs[key]);
+ }
+ }
+ }
+
+ /**
+ * Returns the instance manager
+ *
+ * @return {etemplate2}
+ */
+ getInstanceManager()
+ {
+ if (this._inst != null)
+ {
+ return this._inst;
+ }
+ else if (this.getParent())
+ {
+ return this.getParent().getInstanceManager();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the path into the data array. By default, array manager takes care of
+ * this, but some extensions need to override this
+ */
+ getPath()
+ {
+ var path = this.getArrayMgr("content").getPath();
+
+ // Prevent namespaced widgets with value from going an extra layer deep
+ if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop();
+
+ return path;
+ }
+
+ _createNamespace(): boolean
+ {
+ return false;
+ }
+
+ egw(): IegwAppLocal
+ {
+ if (this.getParent() != null && !(this.getParent() instanceof HTMLElement))
+ {
+ return (this.getParent()).egw();
+ }
+
+ // Get the window this object belongs to
+ var wnd = null;
+ // @ts-ignore Technically this doesn't have implements(), but it's mixed in
+ if (this.implements(et2_IDOMNode))
+ {
+ var node = (this).getDOMNode();
+ if (node && node.ownerDocument)
+ {
+ wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
+ }
+ }
+
+ // If we're the root object, return the phpgwapi API instance
+ return egw('phpgwapi', wnd);
+ }
+ };
+
+ function applyMixins(derivedCtor: any, baseCtors: any[])
+ {
+ baseCtors.forEach(baseCtor =>
+ {
+ Object.getOwnPropertyNames(baseCtor.prototype).forEach(name =>
+ {
+ if (name !== 'constructor')
+ {
+ derivedCtor.prototype[name] = baseCtor.prototype[name];
+ }
+ });
+ });
+ }
+
+ // Add some more stuff in
+ applyMixins(Et2WidgetClass, [ClassWithInterfaces]);
+
+ return Et2WidgetClass as unknown as Constructor & T;
}
\ No newline at end of file
diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts
index e4f23ca00a..bfb57a449a 100644
--- a/api/js/etemplate/et2_core_inputWidget.ts
+++ b/api/js/etemplate/et2_core_inputWidget.ts
@@ -15,17 +15,19 @@
*/
import {et2_no_init} from "./et2_core_common";
-import { ClassWithAttributes } from "./et2_core_inheritance";
-import { et2_widget, WidgetConfig } from "./et2_core_widget";
-import { et2_valueWidget } from './et2_core_valueWidget'
+import {ClassWithAttributes} from "./et2_core_inheritance";
+import {et2_widget, WidgetConfig} from "./et2_core_widget";
+import {et2_valueWidget} from './et2_core_valueWidget'
import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "./et2_core_interfaces";
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
// fixing circular dependencies by only importing the type (not in compiled .js)
import type {et2_tabbox} from "./et2_widget_tabs";
-export interface et2_input {
- getInputNode() : HTMLInputElement|HTMLElement;
+export interface et2_input
+{
+ getInputNode(): HTMLInputElement | HTMLElement;
}
+
/**
* et2_inputWidget derrives from et2_simpleWidget and implements the IInput
* interface. When derriving from this class, call setDOMNode with an input
@@ -33,9 +35,9 @@ export interface et2_input {
*/
export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ISubmitListener, et2_input
{
- static readonly _attributes : any = {
+ static readonly _attributes: any = {
"needed": {
- "name": "Required",
+ "name": "Required",
"default": false,
"type": "boolean",
"description": "If required, the user must enter a value before the form can be submitted"
@@ -78,7 +80,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
/**
* Constructor
*/
- constructor(_parent, _attrs? : WidgetConfig, _child? : object)
+ constructor(_parent, _attrs?: WidgetConfig, _child?: object)
{
// Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {}));
@@ -105,7 +107,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
/**
* Make sure dirty flag is properly set
*/
- doLoadingFinished() : boolean | JQueryPromise
+ doLoadingFinished(): boolean | JQueryPromise
{
let result = super.doLoadingFinished();
@@ -141,10 +143,12 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
{
jQuery(node)
.off('.et2_inputWidget')
- .bind("change.et2_inputWidget", this, function(e) {
+ .bind("change.et2_inputWidget", this, function (e)
+ {
e.data.change.call(e.data, this);
})
- .bind("focus.et2_inputWidget", this, function(e) {
+ .bind("focus.et2_inputWidget", this, function (e)
+ {
e.data.focus.call(e.data, this);
});
}
@@ -173,14 +177,16 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
if (valid && this.onchange)
{
- if(typeof this.onchange == 'function')
+ if (typeof this.onchange == '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);
+ if (args.indexOf(this) == -1) args.push(this);
return this.onchange.apply(this, args);
- } else {
+ }
+ else
+ {
return (et2_compileLegacyJS(this.options.onchange, this, _node))();
}
}
@@ -189,11 +195,11 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
focus(_node)
{
- if(typeof this.options.onfocus == 'function')
+ if (typeof this.options.onfocus == '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);
+ if (args.indexOf(this) == -1) args.push(this);
return this.options.onfocus.apply(this, args);
}
@@ -206,13 +212,13 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
*
* @param {string} _value value to set
*/
- set_value(_value : any | null)
+ set_value(_value: any | null)
{
var node = this.getInputNode();
if (node)
{
jQuery(node).val(_value);
- if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value)
+ if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value)
{
jQuery(node).change();
}
@@ -223,7 +229,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
set_id(_value)
{
this.id = _value;
- this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value;
+ this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value;
// Set the id of the _input_ node (in contrast to the default
// implementation, which sets the base node)
@@ -249,9 +255,12 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
var node = this.getInputNode();
if (node)
{
- if(_value && !this.options.readonly) {
+ if (_value && !this.options.readonly)
+ {
jQuery(node).attr("required", "required");
- } else {
+ }
+ else
+ {
node.removeAttribute("required");
}
}
@@ -273,8 +282,8 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
jQuery(node).addClass("invalid");
// If on a tab, switch to that tab so user can see it
- let widget : et2_widget = this;
- while(widget.getParent() && widget.getType() != 'tabbox')
+ let widget: et2_widget = this;
+ while (widget.getParent() && widget.getType() != 'tabbox')
{
widget = widget.getParent();
}
@@ -319,26 +328,26 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
isDirty()
{
let value = this.getValue();
- if(typeof value !== typeof this._oldValue)
+ if (typeof value !== typeof this._oldValue)
{
return true;
}
- if(this._oldValue === value)
+ if (this._oldValue === value)
{
return false;
}
- switch(typeof this._oldValue)
+ switch (typeof this._oldValue)
{
case "object":
- if(typeof this._oldValue.length !== "undefined" &&
+ if (typeof this._oldValue.length !== "undefined" &&
this._oldValue.length !== value.length
)
{
return true;
}
- for(let key in this._oldValue)
+ for (let key in this._oldValue)
{
- if(this._oldValue[key] !== value[key]) return true;
+ if (this._oldValue[key] !== value[key]) return true;
}
return false;
default:
@@ -392,58 +401,63 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_
*/
type Constructor = new (...args: any[]) => T;
-export const Et2InputWidget = (superClass: T) => {
- class Et2InputWidgetClass extends superClass implements et2_IInput, et2_IInputNode {
+export const Et2InputWidget = (superClass: T) =>
+{
+ class Et2InputWidgetClass extends superClass implements et2_IInput, et2_IInputNode
+ {
label: string = '';
protected value: string | number | Object;
protected _oldValue: string | number | Object;
/** WebComponent **/
- static get properties() {
+ static get properties()
+ {
return {
...super.properties,
value: {attribute: false}
};
}
- constructor() {
+ constructor()
+ {
super();
}
set_value(new_value)
{
- this.modelValue=new_value;
+ this.value = new_value;
}
+
getValue()
{
- return this._inputNode.value;
+ return this.getInputNode().value;
}
isDirty()
{
let value = this.getValue();
- if(typeof value !== typeof this._oldValue)
+ if (typeof value !== typeof this._oldValue)
{
return true;
}
- if(this._oldValue === value)
+ if (this._oldValue === value)
{
return false;
}
- switch(typeof this._oldValue)
+ switch (typeof this._oldValue)
{
case "object":
- if(typeof this._oldValue.length !== "undefined" &&
+ if (typeof this._oldValue.length !== "undefined" &&
this._oldValue.length !== value.length
)
{
return true;
}
- for(let key in this._oldValue)
+ for (let key in this._oldValue)
{
- if(this._oldValue[key] !== value[key]) return true;
+ if (this._oldValue[key] !== value[key]) return true;
}
return false;
default:
diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts
index 6eec09b139..1a54eafc4f 100644
--- a/api/js/etemplate/et2_core_widget.ts
+++ b/api/js/etemplate/et2_core_widget.ts
@@ -43,24 +43,24 @@ export var et2_attribute_registry = {};
*/
export function et2_register_widget(_constructor, _types)
{
- "use strict";
+ "use strict";
- et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor);
- // Iterate over all given types and register those
- for (var i = 0; i < _types.length; i++)
+ et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor);
+ // Iterate over all given types and register those
+ for (var i = 0; i < _types.length; i++)
+ {
+ var type = _types[i].toLowerCase();
+
+ // Check whether a widget has already been registered for one of the
+ // types.
+ if (et2_registry[type])
{
- var type = _types[i].toLowerCase();
-
- // Check whether a widget has already been registered for one of the
- // types.
- if (et2_registry[type])
- {
- egw.debug("warn", "Widget class registered for " + type +
- " will be overwritten.");
- }
-
- et2_registry[type] = _constructor;
+ egw.debug("warn", "Widget class registered for " + type +
+ " will be overwritten.");
}
+
+ et2_registry[type] = _constructor;
+ }
}
/**
@@ -78,41 +78,41 @@ export function et2_register_widget(_constructor, _types)
*/
export function et2_createWidget(_name: string, _attrs: object, _parent?: any): et2_widget
{
- "use strict";
+ "use strict";
- if (typeof _attrs == "undefined")
- {
- _attrs = {};
- }
+ if (typeof _attrs == "undefined")
+ {
+ _attrs = {};
+ }
- if (typeof _attrs != "object")
- {
- _attrs = {};
- }
+ if (typeof _attrs != "object")
+ {
+ _attrs = {};
+ }
- if (typeof _parent == "undefined")
- {
- _parent = null;
- }
+ if (typeof _parent == "undefined")
+ {
+ _parent = null;
+ }
- // Parse the "readonly" and "type" flag for this element here, as they
- // determine which constructor is used
- var nodeName = _attrs["type"] = _name;
- var readonly = _attrs["readonly"] =
- typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"];
+ // Parse the "readonly" and "type" flag for this element here, as they
+ // determine which constructor is used
+ var nodeName = _attrs["type"] = _name;
+ var readonly = _attrs["readonly"] =
+ typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"];
- // Get the constructor - if the widget is readonly, use the special "_ro"
- // constructor if it is available
- let constructor = et2_registry[typeof et2_registry[nodeName] == "undefined" ? 'placeholder' : nodeName];
- if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined")
- {
- constructor = et2_registry[nodeName + "_ro"];
- }
+ // Get the constructor - if the widget is readonly, use the special "_ro"
+ // constructor if it is available
+ let constructor = et2_registry[typeof et2_registry[nodeName] == "undefined" ? 'placeholder' : nodeName];
+ if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined")
+ {
+ constructor = et2_registry[nodeName + "_ro"];
+ }
- // Do an sanity check for the attributes
- ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], _attrs);
- // Create the new widget and return it
- return new constructor(_parent, _attrs);
+ // Do an sanity check for the attributes
+ ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], _attrs);
+ // Create the new widget and return it
+ return new constructor(_parent, _attrs);
}
// make et2_createWidget publicly available as we need to call it from stylite/js/gantt.js (maybe others)
@@ -120,11 +120,11 @@ if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] =
export interface WidgetConfig
{
- type?: string;
- readonly?: boolean;
- width?: number;
+ type?: string;
+ readonly?: boolean;
+ width?: number;
- [propName: string]: any;
+ [propName: string]: any;
}
/**
@@ -134,1122 +134,1117 @@ export interface WidgetConfig
*/
export class et2_widget extends ClassWithAttributes
{
- static readonly _attributes: any = {
- "id": {
- "name": "ID",
- "type": "string",
- "description": "Unique identifier of the widget"
- },
+ static readonly _attributes: any = {
+ "id": {
+ "name": "ID",
+ "type": "string",
+ "description": "Unique identifier of the widget"
+ },
- "no_lang": {
- "name": "No translation",
- "type": "boolean",
- "default": false,
- "description": "If true, no translations are made for this widget"
- },
-
- /**
- * Ignore the "span" property by default - it is read by the grid and
- * other widgets.
- */
- "span": {
- "ignore": true
- },
-
- /**
- * Ignore the "type" tag - it is read by the "createElementFromNode"
- * function and passed as second parameter of the widget constructor
- */
- "type": {
- "name": "Widget type",
- "type": "string",
- "ignore": true,
- "description": "What kind of widget this is"
- },
-
- /**
- * Ignore the readonly tag by default - its also read by the
- * "createElementFromNode" function.
- */
- "readonly": {
- "ignore": true
- },
-
- /**
- * Widget's attributes
- */
- attributes: {
- "name": "Widget attributes",
- "type": "any",
- "ignore": true,
- "description": "Object of widget attributes"
- }
- };
-
- // Set the legacyOptions array to the names of the properties the "options"
- // attribute defines.
- public static readonly legacyOptions: string[] = [];
-
- private _type: string;
- id: string;
- supportedWidgetClasses: any[];
- options: WidgetConfig;
- readonly: boolean;
+ "no_lang": {
+ "name": "No translation",
+ "type": "boolean",
+ "default": false,
+ "description": "If true, no translations are made for this widget"
+ },
/**
- * Widget constructor
- *
- * To implement the attributes inheritance and overriding each extending class/widget needs to call:
- *
- * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {}));
- *
- * @param _parent is the parent object from the XML tree which contains this
- * object. The default constructor always adds the new instance to the
- * children list of the given parent object. _parent may be NULL.
- * @param _attrs is an associative array of attributes.
- * @param _child attributes object from the child
+ * Ignore the "span" property by default - it is read by the grid and
+ * other widgets.
*/
- constructor(_parent?, _attrs?: WidgetConfig, _child?: object)
- {
- super(); // because we in the top of the widget hierarchy
- this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {});
-
- // Check whether all attributes are available
- if (typeof _parent == "undefined")
- {
- _parent = null;
- }
-
- if (typeof _attrs == "undefined")
- {
- _attrs = {};
- }
-
- if (_attrs.attributes)
- {
- jQuery.extend(_attrs, _attrs.attributes);
- }
- // Initialize all important parameters
- this._mgrs = {};
- this._inst = null;
- this._children = [];
- this._type = _attrs["type"];
- this.id = _attrs["id"];
-
- // Add this widget to the given parent widget
- if (_parent != null)
- {
- _parent.addChild(this);
- }
-
- // The supported widget classes array defines a whitelist for all widget
- // classes or interfaces child widgets have to support.
- this.supportedWidgetClasses = [et2_widget, HTMLElement];
-
- if (_attrs["id"])
- {
- // Create a namespace for this object
- if (this._createNamespace())
- {
- this.checkCreateNamespace(_attrs);
- }
- }
-
- if (this.id)
- {
- //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']');
- }
-
- // Add all attributes hidden in the content arrays to the attributes
- // parameter
- this.transformAttributes(_attrs);
-
- // Create a local copy of the options object
- this.options = et2_cloneObject(_attrs);
- }
+ "span": {
+ "ignore": true
+ },
/**
- * The destroy function destroys all children of the widget, removes itself
- * from the parents children list.
- * In all classes derrived from et2_widget ALWAYS override the destroy
- * function and remove ALL references to other objects. Also remember to
- * unbind ANY event this widget created and to remove all DOM-Nodes it
- * created.
+ * Ignore the "type" tag - it is read by the "createElementFromNode"
+ * function and passed as second parameter of the widget constructor
*/
- destroy()
- {
- // Call the destructor of all children
- for (var i = this._children.length - 1; i >= 0; i--)
- {
- this._children[i].destroy();
- }
-
- // Remove this element from the parent, if it exists
- if (typeof this._parent != "undefined" && this._parent !== null)
- {
- this._parent.removeChild(this);
- }
-
- // Free the array managers if they belong to this widget
- for (var key in this._mgrs)
- {
- if (this._mgrs[key] && this._mgrs[key].owner == this)
- {
- this._mgrs[key].destroy();
- }
- }
- }
-
- getType(): string
- {
- return this._type;
- }
-
- setType(_type: string)
- {
- this._type = _type;
- }
+ "type": {
+ "name": "Widget type",
+ "type": "string",
+ "ignore": true,
+ "description": "What kind of widget this is"
+ },
/**
- * Creates a copy of this widget. The parameters given are passed to the
- * constructor of the copied object. If the parameters are omitted, _parent
- * is defaulted to null
- *
- * @param {et2_widget} _parent parent to set for clone, default null
+ * Ignore the readonly tag by default - its also read by the
+ * "createElementFromNode" function.
*/
- clone(_parent)
- {
- // Default _parent to null
- if (typeof _parent == "undefined")
- {
- _parent = null;
- }
-
- // Create the copy
- var copy = new (this.constructor)(_parent, this.options);
-
- // Assign this element to the copy
- copy.assign(this);
-
- return copy;
- }
-
- assign(_obj)
- {
- if (typeof _obj._children == "undefined")
- {
- this.egw().debug("log", "Foo!");
- }
-
- // Create a clone of all child elements of the given object
- for (var i = 0; i < _obj._children.length; i++)
- {
- _obj._children[i].clone(this);
- }
-
- // Copy a reference to the content array manager
- this.setArrayMgrs(_obj.mgrs);
- }
-
- _parent: et2_widget;
+ "readonly": {
+ "ignore": true
+ },
/**
- * Returns the parent widget of this widget
+ * Widget's attributes
*/
- getParent(): et2_widget | null
+ attributes: {
+ "name": "Widget attributes",
+ "type": "any",
+ "ignore": true,
+ "description": "Object of widget attributes"
+ }
+ };
+
+ // Set the legacyOptions array to the names of the properties the "options"
+ // attribute defines.
+ public static readonly legacyOptions: string[] = [];
+
+ private _type: string;
+ id: string;
+ supportedWidgetClasses: any[];
+ options: WidgetConfig;
+ readonly: boolean;
+
+ /**
+ * Widget constructor
+ *
+ * To implement the attributes inheritance and overriding each extending class/widget needs to call:
+ *
+ * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {}));
+ *
+ * @param _parent is the parent object from the XML tree which contains this
+ * object. The default constructor always adds the new instance to the
+ * children list of the given parent object. _parent may be NULL.
+ * @param _attrs is an associative array of attributes.
+ * @param _child attributes object from the child
+ */
+ constructor(_parent?, _attrs?: WidgetConfig, _child?: object)
+ {
+ super(); // because we in the top of the widget hierarchy
+ this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {});
+
+ // Check whether all attributes are available
+ if (typeof _parent == "undefined")
{
- return this._parent;
+ _parent = null;
}
- protected _children = [];
-
- /**
- * Returns the list of children of this widget.
- */
- getChildren(): et2_widget[]
+ if (typeof _attrs == "undefined")
{
- return this._children;
+ _attrs = {};
}
- /**
- * Returns the base widget
- */
- getRoot(): et2_container
+ if (_attrs.attributes)
{
- if (this._parent != null)
- {
- return this._parent.getRoot();
- }
- else
- {
- return this;
- }
+ jQuery.extend(_attrs, _attrs.attributes);
+ }
+ // Initialize all important parameters
+ this._mgrs = {};
+ this._inst = null;
+ this._children = [];
+ this._type = _attrs["type"];
+ this.id = _attrs["id"];
+
+ // Add this widget to the given parent widget
+ if (_parent != null)
+ {
+ _parent.addChild(this);
}
- /**
- * Inserts an child at the end of the list.
- *
- * @param _node is the node which should be added. It has to be an instance
- * of et2_widget
- */
- addChild(_node: et2_widget)
+ // The supported widget classes array defines a whitelist for all widget
+ // classes or interfaces child widgets have to support.
+ this.supportedWidgetClasses = [et2_widget, HTMLElement];
+
+ if (_attrs["id"])
{
- this.insertChild(_node, this._children.length);
+ // Create a namespace for this object
+ if (this._createNamespace())
+ {
+ this.checkCreateNamespace(_attrs);
+ }
}
- /**
- * Inserts a child at the given index.
- *
- * @param _node is the node which should be added. It has to be an instance
- * of et2_widget
- * @param _idx is the position at which the element should be added.
- */
- insertChild(_node: et2_widget, _idx: number)
+ if (this.id)
{
- // Check whether the node is one of the supported widget classes.
- if (this.isOfSupportedWidgetClass(_node))
- {
- // Remove the node from its original parent
- if (_node._parent)
- {
- _node._parent.removeChild(_node);
- }
+ //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']');
+ }
- _node._parent = this;
- this._children.splice(_idx, 0, _node);
- }
- else
- {
- this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this);
+ // Add all attributes hidden in the content arrays to the attributes
+ // parameter
+ this.transformAttributes(_attrs);
+
+ // Create a local copy of the options object
+ this.options = et2_cloneObject(_attrs);
+ }
+
+ /**
+ * The destroy function destroys all children of the widget, removes itself
+ * from the parents children list.
+ * In all classes derrived from et2_widget ALWAYS override the destroy
+ * function and remove ALL references to other objects. Also remember to
+ * unbind ANY event this widget created and to remove all DOM-Nodes it
+ * created.
+ */
+ destroy()
+ {
+ // Call the destructor of all children
+ for (var i = this._children.length - 1; i >= 0; i--)
+ {
+ this._children[i].destroy();
+ }
+
+ // Remove this element from the parent, if it exists
+ if (typeof this._parent != "undefined" && this._parent !== null)
+ {
+ this._parent.removeChild(this);
+ }
+
+ // Free the array managers if they belong to this widget
+ for (var key in this._mgrs)
+ {
+ if (this._mgrs[key] && this._mgrs[key].owner == this)
+ {
+ this._mgrs[key].destroy();
+ }
+ }
+ }
+
+ getType(): string
+ {
+ return this._type;
+ }
+
+ setType(_type: string)
+ {
+ this._type = _type;
+ }
+
+ /**
+ * Creates a copy of this widget. The parameters given are passed to the
+ * constructor of the copied object. If the parameters are omitted, _parent
+ * is defaulted to null
+ *
+ * @param {et2_widget} _parent parent to set for clone, default null
+ */
+ clone(_parent)
+ {
+ // Default _parent to null
+ if (typeof _parent == "undefined")
+ {
+ _parent = null;
+ }
+
+ // Create the copy
+ var copy = new (this.constructor)(_parent, this.options);
+
+ // Assign this element to the copy
+ copy.assign(this);
+
+ return copy;
+ }
+
+ assign(_obj)
+ {
+ if (typeof _obj._children == "undefined")
+ {
+ this.egw().debug("log", "Foo!");
+ }
+
+ // Create a clone of all child elements of the given object
+ for (var i = 0; i < _obj._children.length; i++)
+ {
+ _obj._children[i].clone(this);
+ }
+
+ // Copy a reference to the content array manager
+ this.setArrayMgrs(_obj.mgrs);
+ }
+
+ _parent: et2_widget;
+
+ /**
+ * Returns the parent widget of this widget
+ */
+ getParent(): et2_widget | null
+ {
+ return this._parent;
+ }
+
+ protected _children = [];
+
+ /**
+ * Returns the list of children of this widget.
+ */
+ getChildren(): et2_widget[]
+ {
+ return this._children;
+ }
+
+ /**
+ * Returns the base widget
+ */
+ getRoot(): et2_container
+ {
+ if (this._parent != null)
+ {
+ return this._parent.getRoot();
+ }
+ else
+ {
+ return this;
+ }
+ }
+
+ /**
+ * Inserts an child at the end of the list.
+ *
+ * @param _node is the node which should be added. It has to be an instance
+ * of et2_widget
+ */
+ addChild(_node: et2_widget)
+ {
+ this.insertChild(_node, this._children.length);
+ }
+
+ /**
+ * Inserts a child at the given index.
+ *
+ * @param _node is the node which should be added. It has to be an instance
+ * of et2_widget
+ * @param _idx is the position at which the element should be added.
+ */
+ insertChild(_node: et2_widget, _idx: number)
+ {
+ // Check whether the node is one of the supported widget classes.
+ if (this.isOfSupportedWidgetClass(_node))
+ {
+ // Remove the node from its original parent
+ if (_node._parent)
+ {
+ _node._parent.removeChild(_node);
+ }
+
+ _node._parent = this;
+ this._children.splice(_idx, 0, _node);
+ }
+ else
+ {
+ this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this);
// throw("Widget is not supported by this widget class!");
- }
+ }
+ }
+
+ /**
+ * Removes the child but does not destroy it.
+ *
+ * @param {et2_widget} _node child to remove
+ */
+ removeChild(_node)
+ {
+ // Retrieve the child from the child list
+ var idx = this._children.indexOf(_node);
+
+ if (idx >= 0)
+ {
+ // This element is no longer parent of the child
+ _node._parent = null;
+
+ this._children.splice(idx, 1);
+ }
+ }
+
+ /**
+ * Searches an element by id in the tree, descending into the child levels.
+ *
+ * @param _id is the id you're searching for
+ */
+ getWidgetById(_id): et2_widget | null
+ {
+ if (this.id == _id)
+ {
+ return this;
+ }
+ if (!this._children) return null;
+
+ for (var i = 0; i < this._children.length; i++)
+ {
+ var elem = this._children[i].getWidgetById(_id);
+
+ if (elem != null)
+ {
+ return elem;
+ }
+ }
+ if (this.id && _id.indexOf('[') > -1 && this._children.length)
+ {
+ var ids = (new et2_arrayMgr()).explodeKey(_id);
+ var widget: et2_widget = this;
+ for (var i = 0; i < ids.length && widget !== null; i++)
+ {
+ widget = widget.getWidgetById(ids[i]);
+ }
+ return widget;
}
- /**
- * Removes the child but does not destroy it.
- *
- * @param {et2_widget} _node child to remove
- */
- removeChild(_node)
+ return null;
+ }
+
+ /**
+ * Function which allows iterating over the complete widget tree.
+ *
+ * @param _callback is the function which should be called for each widget
+ * @param _context is the context in which the function should be executed
+ * @param _type is an optional parameter which specifies a class/interface
+ * the elements have to be instanceOf.
+ */
+ iterateOver(_callback, _context, _type?)
+ {
+ if (typeof _type == "undefined")
{
- // Retrieve the child from the child list
- var idx = this._children.indexOf(_node);
-
- if (idx >= 0)
- {
- // This element is no longer parent of the child
- _node._parent = null;
-
- this._children.splice(idx, 1);
- }
+ _type = et2_widget;
}
- /**
- * Searches an element by id in the tree, descending into the child levels.
- *
- * @param _id is the id you're searching for
- */
- getWidgetById(_id): et2_widget | null
+ if (this.isInTree() && this.instanceOf(_type))
{
- if (this.id == _id)
- {
- return this;
- }
- if (!this._children) return null;
-
- for (var i = 0; i < this._children.length; i++)
- {
- var elem = this._children[i].getWidgetById(_id);
-
- if (elem != null)
- {
- return elem;
- }
- }
- if (this.id && _id.indexOf('[') > -1 && this._children.length)
- {
- var ids = (new et2_arrayMgr()).explodeKey(_id);
- var widget: et2_widget = this;
- for (var i = 0; i < ids.length && widget !== null; i++)
- {
- widget = widget.getWidgetById(ids[i]);
- }
- return widget;
- }
-
- return null;
+ _callback.call(_context, this);
}
- /**
- * Function which allows iterating over the complete widget tree.
- *
- * @param _callback is the function which should be called for each widget
- * @param _context is the context in which the function should be executed
- * @param _type is an optional parameter which specifies a class/interface
- * the elements have to be instanceOf.
- */
- iterateOver(_callback, _context, _type?)
+ for (var i = 0; i < this._children.length; i++)
{
- if (typeof _type == "undefined")
- {
- _type = et2_widget;
- }
+ this._children[i].iterateOver(_callback, _context, _type);
+ }
+ }
- if (this.isInTree() && this.instanceOf(_type))
- {
- _callback.call(_context, this);
- }
-
- for (var i = 0; i < this._children.length; i++)
- {
- this._children[i].iterateOver(_callback, _context, _type);
- }
+ /**
+ * Returns true if the widget currently resides in the visible part of the
+ * widget tree. E.g. Templates which have been cloned are not in the visible
+ * part of the widget tree.
+ *
+ * @param _sender
+ * @param {boolean} _vis can be used by widgets overwriting this function - simply
+ * write
+ * return this._super(inTree);
+ * when calling this function the _vis parameter does not have to be supplied.
+ */
+ isInTree(_sender?, _vis?: boolean)
+ {
+ if (typeof _vis == "undefined")
+ {
+ _vis = true;
}
- /**
- * Returns true if the widget currently resides in the visible part of the
- * widget tree. E.g. Templates which have been cloned are not in the visible
- * part of the widget tree.
- *
- * @param _sender
- * @param {boolean} _vis can be used by widgets overwriting this function - simply
- * write
- * return this._super(inTree);
- * when calling this function the _vis parameter does not have to be supplied.
- */
- isInTree(_sender?, _vis?: boolean)
+ if (this._parent)
{
- if (typeof _vis == "undefined")
- {
- _vis = true;
- }
-
- if (this._parent)
- {
- return _vis && this._parent.isInTree(this);
- }
-
- return _vis;
+ return _vis && this._parent.isInTree(this);
}
- isOfSupportedWidgetClass(_obj)
+ return _vis;
+ }
+
+ isOfSupportedWidgetClass(_obj)
+ {
+ for (var i = 0; i < this.supportedWidgetClasses.length; i++)
{
- for (var i = 0; i < this.supportedWidgetClasses.length; i++)
- {
- if (_obj instanceof this.supportedWidgetClasses[i])
- {
- return true;
- }
- }
- return false;
+ if (_obj instanceof this.supportedWidgetClasses[i])
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The parseXMLAttrs function takes an XML DOM attributes object
+ * and adds the given attributes to the _target associative array. This
+ * function also parses the legacyOptions.
+ *
+ * @param _attrsObj is the XML DOM attributes object
+ * @param {object} _target is the object to which the attributes should be written.
+ * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute
+ */
+ parseXMLAttrs(_attrsObj, _target, _proto)
+ {
+ // Check whether the attributes object is really existing, if not abort
+ if (typeof _attrsObj == "undefined")
+ {
+ return;
}
- /**
- * The parseXMLAttrs function takes an XML DOM attributes object
- * and adds the given attributes to the _target associative array. This
- * function also parses the legacyOptions.
- *
- * @param _attrsObj is the XML DOM attributes object
- * @param {object} _target is the object to which the attributes should be written.
- * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute
- */
- parseXMLAttrs(_attrsObj, _target, _proto)
+ // Iterate over the given attributes and parse them
+ var mgr = this.getArrayMgr("content");
+ for (var i = 0; i < _attrsObj.length; i++)
{
- // Check whether the attributes object is really existing, if not abort
- if (typeof _attrsObj == "undefined")
+ var attrName = _attrsObj[i].name;
+ var attrValue = _attrsObj[i].value;
+
+ // Special handling for the legacy options
+ if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
+ {
+ let legacy = _proto.constructor.legacyOptions || [];
+ let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
+ // Check for modifications on legacy options here. Normal modifications
+ // are handled in widget constructor, but it's too late for legacy options then
+ if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
{
- return;
+ var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
+ if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
+ }
+ // expand legacyOptions with content
+ if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
+ {
+ attrValue = mgr.expandName(attrValue);
}
- // Iterate over the given attributes and parse them
- var mgr = this.getArrayMgr("content");
- for (var i = 0; i < _attrsObj.length; i++)
+ // Parse the legacy options (as a string, other types not allowed)
+ var splitted = et2_csvSplit(attrValue + "");
+
+ for (var j = 0; j < splitted.length && j < legacy.length; j++)
{
- var attrName = _attrsObj[i].name;
- var attrValue = _attrsObj[i].value;
+ // Blank = not set, unless there's more legacy options provided after
+ if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
- // Special handling for the legacy options
- if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
- {
- let legacy = _proto.constructor.legacyOptions || [];
- let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
- // Check for modifications on legacy options here. Normal modifications
- // are handled in widget constructor, but it's too late for legacy options then
- if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
- {
- var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
- if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
- }
- // expand legacyOptions with content
- if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
- {
- attrValue = mgr.expandName(attrValue);
- }
+ // Check to make sure we don't overwrite a current option with a legacy option
+ if (typeof _target[legacy[j]] === "undefined")
+ {
+ attrValue = splitted[j];
- // Parse the legacy options (as a string, other types not allowed)
- var splitted = et2_csvSplit(attrValue + "");
-
- for (var j = 0; j < splitted.length && j < legacy.length; j++)
- {
- // Blank = not set, unless there's more legacy options provided after
- if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
-
- // Check to make sure we don't overwrite a current option with a legacy option
- if (typeof _target[legacy[j]] === "undefined")
- {
- attrValue = splitted[j];
-
- /**
+ /**
If more legacy options than expected, stuff them all in the last legacy option
Some legacy options take a comma separated list.
- */
- if (j == legacy.length - 1 && splitted.length > legacy.length)
- {
- attrValue = splitted.slice(j);
- }
-
- var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {};
-
- // If the attribute is marked as boolean, parse the
- // expression as bool expression.
- if (attr.type == "boolean")
- {
- attrValue = mgr.parseBoolExpression(attrValue);
- }
- else if (typeof attrValue != "object")
- {
- attrValue = mgr.expandName(attrValue);
- }
- _target[legacy[j]] = attrValue;
- }
- }
- }
- else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
+ */
+ if (j == legacy.length - 1 && splitted.length > legacy.length)
{
- // do NOT overwrite already evaluated readonly attribute
- }
- else
- {
- let attrs = et2_attribute_registry[_proto.constructor.name] || {};
- if (mgr != null && typeof attrs[attrName] != "undefined")
- {
- var attr = attrs[attrName];
-
- // If the attribute is marked as boolean, parse the
- // expression as bool expression.
- if (attr.type == "boolean")
- {
- attrValue = mgr.parseBoolExpression(attrValue);
- }
- else
- {
- attrValue = mgr.expandName(attrValue);
- }
- }
-
- // Set the attribute
- _target[attrName] = attrValue;
- }
- }
- }
-
- /**
- * Apply the "modifications" to the element and translate attributes marked
- * with "translate: true"
- *
- * @param {object} _attrs
- */
- transformAttributes(_attrs)
- {
-
- // Apply the content of the modifications array
- if (this.id)
- {
- if (typeof this.id != "string")
- {
- console.log(this.id);
+ attrValue = splitted.slice(j);
}
- if (this.getArrayMgr("modifications"))
- {
- var data = this.getArrayMgr("modifications").getEntry(this.id);
-
- // Check for already inside namespace
- if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this)
- {
- data = this.getArrayMgr("modifications").data;
- }
- if (typeof data === 'object')
- {
- for (var key in data)
- {
- _attrs[key] = data[key];
- }
- }
- }
- }
-
- // Translate the attributes
- for (var key in _attrs)
- {
- if (_attrs[key] && typeof this.attributes[key] != "undefined")
- {
- if (this.attributes[key].translate === true ||
- (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"]))
- {
- let value = _attrs[key];
- // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
- if (value.indexOf('{') !== -1)
- {
- const egw = this.egw();
- _attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1)
- {
- return egw.lang(p1);
- });
- }
- else
- {
- _attrs[key] = this.egw().lang(value);
- }
- }
- }
- }
- }
-
- /**
- * Create a et2_widget from an XML node.
- *
- * First the type and attributes are read from the node. Then the readonly & modifications
- * arrays are checked for changes specific to the loaded data. Then the appropriate
- * constructor is called. After the constructor returns, the widget has a chance to
- * further initialize itself from the XML node when the widget's loadFromXML() method
- * is called with the node.
- *
- * @param _node XML node to read
- * @param _name XML node name
- *
- * @return et2_widget
- */
- createElementFromNode(_node, _name?)
- {
- var attributes = {};
-
- // Parse the "readonly" and "type" flag for this element here, as they
- // determine which constructor is used
- var _nodeName = attributes["type"] = _node.getAttribute("type") ?
- _node.getAttribute("type") : _node.nodeName.toLowerCase();
- var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
- (this.getArrayMgr("readonlys")).isReadOnly(
- _node.getAttribute("id"), _node.getAttribute("readonly"),
- typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false;
-
- // Check to see if modifications change type
- var modifications = this.getArrayMgr("modifications");
- if (modifications && _node.getAttribute("id"))
- {
- var entry: any = modifications.getEntry(_node.getAttribute("id"));
- if (entry == null)
- {
- // Try again, but skip the fancy stuff
- // TODO: Figure out why the getEntry() call doesn't always work
- var entry = modifications.data[_node.getAttribute("id")];
- if (entry)
- {
- this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry);
- }
- else
- {
- // Try the root, in case a namespace got missed
- entry = modifications.getRoot().getEntry(_node.getAttribute("id"));
- }
- }
- if (entry && entry.type && typeof entry.type === 'string')
- {
- _nodeName = attributes["type"] = entry.type;
- }
- entry = null;
- }
-
- // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"),
- // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs!
- if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
- {
- _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
- }
-
- // Get the constructor - if the widget is readonly, use the special "_ro"
- // constructor if it is available
- var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
- if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
- {
- constructor = et2_registry[_nodeName + "_ro"];
- }
-
- if (undefined == window.customElements.get(_nodeName))
- {
- // Parse the attributes from the given XML attributes object
- this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
-
- // Do an sanity check for the attributes
- ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
-
- // Creates the new widget, passes this widget as an instance and
- // passes the widgetType. Then it goes on loading the XML for it.
- var widget = new constructor(this, attributes);
-
- // Load the widget itself from XML
- widget.loadFromXML(_node);
- }
- else
- {
- widget = this.loadWebComponent(_nodeName, _node);
-
- if (this.addChild)
- {
- // webcomponent going into old et2_widget
- this.addChild(widget);
- }
- }
- return widget;
- }
-
- /**
- * Load a Web Component
- * @param _nodeName
- * @param _node
- */
- loadWebComponent(_nodeName: string, _node): HTMLElement
- {
- let widget = document.createElement(_nodeName);
- widget.textContent = _node.textContent;
-
- const widget_class = window.customElements.get(_nodeName);
- if (!widget_class)
- {
- throw Error("Unknown or unregistered WebComponent '" + _nodeName + "', could not find class");
- }
- widget.setParent(this);
- var mgr = widget.getArrayMgr("content");
- debugger;
- // Apply any set attributes - widget will do its own coercion
- _node.getAttributeNames().forEach(attribute =>
- {
- let attrValue = _node.getAttribute(attribute);
-
- // If there is not attribute set, ignore it. Widget sets its own default.
- if (typeof attrValue === "undefined") return;
+ var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {};
// If the attribute is marked as boolean, parse the
// expression as bool expression.
- if (widget_class.getPropertyOptions(attribute).type == "Boolean")
+ if (attr.type == "boolean")
{
- attrValue = mgr.parseBoolExpression(attrValue);
+ attrValue = mgr.parseBoolExpression(attrValue);
}
- else
+ else if (typeof attrValue != "object")
{
- attrValue = mgr.expandName(attrValue);
+ attrValue = mgr.expandName(attrValue);
}
- widget.setAttribute(attribute, attrValue);
- });
-
- if (widget_class.getPropertyOptions("value") && widget.set_value)
+ _target[legacy[j]] = attrValue;
+ }
+ }
+ }
+ else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
+ {
+ // do NOT overwrite already evaluated readonly attribute
+ }
+ else
+ {
+ let attrs = et2_attribute_registry[_proto.constructor.name] || {};
+ if (mgr != null && typeof attrs[attrName] != "undefined")
{
- if (mgr != null)
- {
- let val = mgr.getEntry(widget.id, false, true);
- if (val !== null)
- {
- widget.setAttribute("value", val);
- }
- }
- // Check for already inside namespace
- if (this._createNamespace() && this.getArrayMgr("content").perspectiveData.owner == this)
- {
- widget.setAttribute("value", this.getArrayMgr("content").data);
- }
+ var attr = attrs[attrName];
+
+ // If the attribute is marked as boolean, parse the
+ // expression as bool expression.
+ if (attr.type == "boolean")
+ {
+ attrValue = mgr.parseBoolExpression(attrValue);
+ }
+ else
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
}
- // Children need to be loaded
- widget.loadFromXML(_node);
+ // Set the attribute
+ _target[attrName] = attrValue;
+ }
+ }
+ }
- return widget;
+ /**
+ * Apply the "modifications" to the element and translate attributes marked
+ * with "translate: true"
+ *
+ * @param {object} _attrs
+ */
+ transformAttributes(_attrs)
+ {
+
+ // Apply the content of the modifications array
+ if (this.id)
+ {
+ if (typeof this.id != "string")
+ {
+ console.log(this.id);
+ }
+
+ if (this.getArrayMgr("modifications"))
+ {
+ var data = this.getArrayMgr("modifications").getEntry(this.id);
+
+ // Check for already inside namespace
+ if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this)
+ {
+ data = this.getArrayMgr("modifications").data;
+ }
+ if (typeof data === 'object')
+ {
+ for (var key in data)
+ {
+ _attrs[key] = data[key];
+ }
+ }
+ }
}
- /**
- * Loads the widget tree from an XML node
- *
- * @param _node xml node
- */
- loadFromXML(_node)
+ // Translate the attributes
+ for (var key in _attrs)
{
- // Load the child nodes.
- for (var i = 0; i < _node.childNodes.length; i++)
+ if (_attrs[key] && typeof this.attributes[key] != "undefined")
+ {
+ if (this.attributes[key].translate === true ||
+ (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"]))
{
- var node = _node.childNodes[i];
- var widgetType = node.nodeName.toLowerCase();
-
- if (widgetType == "#comment")
+ let value = _attrs[key];
+ // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
+ if (value.indexOf('{') !== -1)
+ {
+ const egw = this.egw();
+ _attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1)
{
- continue;
- }
-
- if (widgetType == "#text")
- {
- if (node.data.replace(/^\s+|\s+$/g, ''))
- {
- this.loadContent(node.data);
- }
- continue;
- }
-
- // Create the new element
- this.createElementFromNode(node);
+ return egw.lang(p1);
+ });
+ }
+ else
+ {
+ _attrs[key] = this.egw().lang(value);
+ }
}
+ }
}
+ }
- /**
- * Called whenever textNodes are loaded from the XML tree
- *
- * @param _content
- */
- loadContent(_content)
+ /**
+ * Create a et2_widget from an XML node.
+ *
+ * First the type and attributes are read from the node. Then the readonly & modifications
+ * arrays are checked for changes specific to the loaded data. Then the appropriate
+ * constructor is called. After the constructor returns, the widget has a chance to
+ * further initialize itself from the XML node when the widget's loadFromXML() method
+ * is called with the node.
+ *
+ * @param _node XML node to read
+ * @param _name XML node name
+ *
+ * @return et2_widget
+ */
+ createElementFromNode(_node, _name?)
+ {
+ var attributes = {};
+
+ // Parse the "readonly" and "type" flag for this element here, as they
+ // determine which constructor is used
+ var _nodeName = attributes["type"] = _node.getAttribute("type") ?
+ _node.getAttribute("type") : _node.nodeName.toLowerCase();
+ var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
+ (this.getArrayMgr("readonlys")).isReadOnly(
+ _node.getAttribute("id"), _node.getAttribute("readonly"),
+ typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false;
+
+ // Check to see if modifications change type
+ var modifications = this.getArrayMgr("modifications");
+ if (modifications && _node.getAttribute("id"))
{
- }
-
- /**
- * Called when loading the widget (sub-tree) is finished. First when this
- * function is called, the DOM-Tree is created. loadingFinished is
- * recursively called for all child elements. Do not directly override this
- * function but the doLoadingFinished function which is executed before
- * descending deeper into the DOM-Tree.
- *
- * Some widgets (template) do not load immediately because they request
- * additional resources via AJAX. They will return a Deferred Promise object.
- * If you call loadingFinished(promises) after creating such a widget
- * programmatically, you might need to wait for it to fully complete its
- * loading before proceeding. In that case use:
- *
- * var promises = [];
- * widget.loadingFinished(promises);
- * jQuery.when.apply(null, promises).done( doneCallback );
- *
- * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred}
- *
- * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed.
- */
- loadingFinished(promises?)
- {
- // Call all availble setters
- this.initAttributes(this.options);
-
- // Make sure promises is defined to avoid errors.
- // We'll warn (below) if programmer should have passed it.
- if (typeof promises == "undefined")
+ var entry: any = modifications.getEntry(_node.getAttribute("id"));
+ if (entry == null)
+ {
+ // Try again, but skip the fancy stuff
+ // TODO: Figure out why the getEntry() call doesn't always work
+ var entry = modifications.data[_node.getAttribute("id")];
+ if (entry)
{
- promises = [];
- var warn_if_deferred = true;
+ this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry);
}
-
- var loadChildren = function ()
+ else
{
- // Descend recursively into the tree
- for (var i = 0; i < this._children.length; i++)
- {
- try
- {
- this._children[i].loadingFinished(promises);
- }
- catch (e)
- {
- egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack);
- }
- }
- };
-
- var result = this.doLoadingFinished();
- if (typeof result == "boolean" && result)
- {
- // Simple widget finishes nicely
- loadChildren.apply(this, arguments);
+ // Try the root, in case a namespace got missed
+ entry = modifications.getRoot().getEntry(_node.getAttribute("id"));
}
- else if (typeof result == "object" && result.done)
+ }
+ if (entry && entry.type && typeof entry.type === 'string')
+ {
+ _nodeName = attributes["type"] = entry.type;
+ }
+ entry = null;
+ }
+
+ // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"),
+ // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs!
+ if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0)
+ {
+ _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName);
+ }
+
+ // Get the constructor - if the widget is readonly, use the special "_ro"
+ // constructor if it is available
+ var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
+ if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
+ {
+ constructor = et2_registry[_nodeName + "_ro"];
+ }
+
+ if (undefined == window.customElements.get(_nodeName))
+ {
+ // Parse the attributes from the given XML attributes object
+ this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
+
+ // Do an sanity check for the attributes
+ ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
+
+ // Creates the new widget, passes this widget as an instance and
+ // passes the widgetType. Then it goes on loading the XML for it.
+ var widget = new constructor(this, attributes);
+
+ // Load the widget itself from XML
+ widget.loadFromXML(_node);
+ }
+ else
+ {
+ widget = this.loadWebComponent(_nodeName, _node);
+
+ if (this.addChild)
+ {
+ // webcomponent going into old et2_widget
+ this.addChild(widget);
+ }
+ }
+ return widget;
+ }
+
+ /**
+ * Load a Web Component
+ * @param _nodeName
+ * @param _node
+ */
+ loadWebComponent(_nodeName: string, _node): HTMLElement
+ {
+ let widget = document.createElement(_nodeName);
+ widget.textContent = _node.textContent;
+
+ const widget_class = window.customElements.get(_nodeName);
+ if (!widget_class)
+ {
+ throw Error("Unknown or unregistered WebComponent '" + _nodeName + "', could not find class");
+ }
+ widget.setParent(this);
+ var mgr = widget.getArrayMgr("content");
+ debugger;
+ // Apply any set attributes - widget will do its own coercion
+ _node.getAttributeNames().forEach(attribute =>
+ {
+ let attrValue = _node.getAttribute(attribute);
+
+ // If there is not attribute set, ignore it. Widget sets its own default.
+ if (typeof attrValue === "undefined") return;
+
+ // If the attribute is marked as boolean, parse the
+ // expression as bool expression.
+ if (widget_class.getPropertyOptions(attribute).type == "Boolean")
+ {
+ attrValue = mgr.parseBoolExpression(attrValue);
+ }
+ else
+ {
+ attrValue = mgr.expandName(attrValue);
+ }
+ widget.setAttribute(attribute, attrValue);
+ });
+
+ if (widget_class.getPropertyOptions("value") && widget.set_value)
+ {
+ if (mgr != null)
+ {
+ let val = mgr.getEntry(widget.id, false, true);
+ if (val !== null)
{
- // Warn if list was not provided
- if (warn_if_deferred)
- {
- // Might not be a problem, but if you need the widget to be really loaded, it could be
- egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this);
- }
- // Widget is waiting. Add to the list
- promises.push(result);
- // Fihish loading when it's finished
- result.done(jQuery.proxy(loadChildren, this));
+ widget.setAttribute("value", val);
}
+ }
}
- /**
- * The initAttributes function sets the attributes to their default
- * values. The attributes are not overwritten, which means, that the
- * default is only set, if either a setter exists or this[propName] does
- * not exist yet.
- *
- * Overwritten here to compile legacy JS code in attributes of type "js"
- *
- * @param {object} _attrs
- */
- initAttributes(_attrs)
+ // Children need to be loaded
+ widget.loadFromXML(_node);
+
+ return widget;
+ }
+
+ /**
+ * Loads the widget tree from an XML node
+ *
+ * @param _node xml node
+ */
+ loadFromXML(_node)
+ {
+ // Load the child nodes.
+ for (var i = 0; i < _node.childNodes.length; i++)
{
- for (var key in _attrs)
+ var node = _node.childNodes[i];
+ var widgetType = node.nodeName.toLowerCase();
+
+ if (widgetType == "#comment")
+ {
+ continue;
+ }
+
+ if (widgetType == "#text")
+ {
+ if (node.data.replace(/^\s+|\s+$/g, ''))
{
- if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
- {
- var val = _attrs[key];
- // compile string values of attribute type "js" to functions
- if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string')
- {
- val = et2_compileLegacyJS(val, this,
- this.implements(et2_IInputNode) ? (this).getInputNode() :
- (this.implements(et2_IDOMNode) ? (this).getDOMNode() : null));
- }
- this.setAttribute(key, val, false);
- }
+ this.loadContent(node.data);
}
+ continue;
+ }
+
+ // Create the new element
+ this.createElementFromNode(node);
+ }
+ }
+
+ /**
+ * Called whenever textNodes are loaded from the XML tree
+ *
+ * @param _content
+ */
+ loadContent(_content)
+ {
+ }
+
+ /**
+ * Called when loading the widget (sub-tree) is finished. First when this
+ * function is called, the DOM-Tree is created. loadingFinished is
+ * recursively called for all child elements. Do not directly override this
+ * function but the doLoadingFinished function which is executed before
+ * descending deeper into the DOM-Tree.
+ *
+ * Some widgets (template) do not load immediately because they request
+ * additional resources via AJAX. They will return a Deferred Promise object.
+ * If you call loadingFinished(promises) after creating such a widget
+ * programmatically, you might need to wait for it to fully complete its
+ * loading before proceeding. In that case use:
+ *
+ * var promises = [];
+ * widget.loadingFinished(promises);
+ * jQuery.when.apply(null, promises).done( doneCallback );
+ *
+ * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred}
+ *
+ * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed.
+ */
+ loadingFinished(promises?)
+ {
+ // Call all availble setters
+ this.initAttributes(this.options);
+
+ // Make sure promises is defined to avoid errors.
+ // We'll warn (below) if programmer should have passed it.
+ if (typeof promises == "undefined")
+ {
+ promises = [];
+ var warn_if_deferred = true;
}
- /**
- * Does specific post-processing after the widget is loaded. Most widgets should not
- * need to do anything here, it should all be done before.
- *
- * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children,
- * or a Promise if loading is not actually finished (eg. waiting for AJAX)
- *
- * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise}
- */
- doLoadingFinished(): JQueryPromise | boolean
+ var loadChildren = function ()
{
- return true;
- }
-
- private _egw: IegwAppLocal;
-
- /**
- * The egw function returns the instance of the client side api belonging
- * to this widget tree. The api instance can be set in the "container"
- * widget using the setApiInstance function.
- */
- egw(): IegwAppLocal
- {
- // The _egw property is not set
- if (typeof this._egw === 'undefined')
+ // Descend recursively into the tree
+ for (var i = 0; i < this._children.length; i++)
+ {
+ try
{
- if (this._parent != null)
- {
- return this._parent.egw();
- }
-
- // Get the window this object belongs to
- var wnd = null;
- if (this.implements(et2_IDOMNode))
- {
- var node = (this).getDOMNode();
- if (node && node.ownerDocument)
- {
- wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
- }
- }
-
- // If we're the root object, return the phpgwapi API instance
- return egw('phpgwapi', wnd);
+ this._children[i].loadingFinished(promises);
}
-
- return this._egw;
- }
-
- /**
- * Sets the client side api instance. It can be retrieved by the widget tree
- * by using the "egw()" function.
- *
- * @param {IegwAppLocal} _egw egw object to set
- */
- setApiInstance(_egw: IegwAppLocal)
- {
- this._egw = _egw;
- }
-
- protected _mgrs = {};
-
- /**
- * Sets all array manager objects - this function can be used to set the
- * root array managers of the container object.
- *
- * @param {object} _mgrs
- */
- setArrayMgrs(_mgrs)
- {
- this._mgrs = et2_cloneObject(_mgrs);
- }
-
- /**
- * Returns an associative array containing the top-most array managers.
- *
- * @param _mgrs is used internally and should not be supplied.
- */
- getArrayMgrs(_mgrs?: object)
- {
- if (typeof _mgrs == "undefined")
+ catch (e)
{
- _mgrs = {};
+ egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack);
}
+ }
+ };
- // Add all managers of this object to the result, if they have not already
- // been set in the result
- for (var key in this._mgrs)
- {
- if (typeof _mgrs[key] == "undefined")
- {
- _mgrs[key] = this._mgrs[key];
- }
- }
-
- // Recursively applies this function to the parent widget
- if (this._parent)
- {
- this._parent.getArrayMgrs(_mgrs);
- }
-
- return _mgrs;
- }
-
- /**
- * Sets the array manager for the given part
- *
- * @param {string} _part which array mgr to set
- * @param {object} _mgr
- */
- setArrayMgr(_part: string, _mgr: et2_arrayMgr)
+ var result = this.doLoadingFinished();
+ if (typeof result == "boolean" && result)
{
- this._mgrs[_part] = _mgr;
+ // Simple widget finishes nicely
+ loadChildren.apply(this, arguments);
}
-
- /**
- * Returns the array manager object for the given part
- *
- * @param {string} managed_array_type name of array mgr to return
- */
- getArrayMgr(managed_array_type: string): et2_arrayMgr | null
+ else if (typeof result == "object" && result.done)
{
- if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
+ // Warn if list was not provided
+ if (warn_if_deferred)
+ {
+ // Might not be a problem, but if you need the widget to be really loaded, it could be
+ egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this);
+ }
+ // Widget is waiting. Add to the list
+ promises.push(result);
+ // Fihish loading when it's finished
+ result.done(jQuery.proxy(loadChildren, this));
+ }
+ }
+
+ /**
+ * The initAttributes function sets the attributes to their default
+ * values. The attributes are not overwritten, which means, that the
+ * default is only set, if either a setter exists or this[propName] does
+ * not exist yet.
+ *
+ * Overwritten here to compile legacy JS code in attributes of type "js"
+ *
+ * @param {object} _attrs
+ */
+ initAttributes(_attrs)
+ {
+ for (var key in _attrs)
+ {
+ if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined))
+ {
+ var val = _attrs[key];
+ // compile string values of attribute type "js" to functions
+ if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string')
{
- return this._mgrs[managed_array_type];
+ val = et2_compileLegacyJS(val, this,
+ this.implements(et2_IInputNode) ? (this).getInputNode() :
+ (this.implements(et2_IDOMNode) ? (this).getDOMNode() : null));
}
- else if (this._parent)
+ this.setAttribute(key, val, false);
+ }
+ }
+ }
+
+ /**
+ * Does specific post-processing after the widget is loaded. Most widgets should not
+ * need to do anything here, it should all be done before.
+ *
+ * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children,
+ * or a Promise if loading is not actually finished (eg. waiting for AJAX)
+ *
+ * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise}
+ */
+ doLoadingFinished(): JQueryPromise | boolean
+ {
+ return true;
+ }
+
+ private _egw: IegwAppLocal;
+
+ /**
+ * The egw function returns the instance of the client side api belonging
+ * to this widget tree. The api instance can be set in the "container"
+ * widget using the setApiInstance function.
+ */
+ egw(): IegwAppLocal
+ {
+ // The _egw property is not set
+ if (typeof this._egw === 'undefined')
+ {
+ if (this._parent != null)
+ {
+ return this._parent.egw();
+ }
+
+ // Get the window this object belongs to
+ var wnd = null;
+ if (this.implements(et2_IDOMNode))
+ {
+ var node = (this).getDOMNode();
+ if (node && node.ownerDocument)
{
- return this._parent.getArrayMgr(managed_array_type);
+ wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
}
+ }
- return null;
+ // If we're the root object, return the phpgwapi API instance
+ return egw('phpgwapi', wnd);
}
- /**
- * Checks whether a namespace exists for this element in the content array.
- * If yes, an own perspective of the content array is created. If not, the
- * parent content manager is used.
- *
- * Constructor attributes are passed in case a child needs to make decisions
- */
- checkCreateNamespace(_attrs?: any)
+ return this._egw;
+ }
+
+ /**
+ * Sets the client side api instance. It can be retrieved by the widget tree
+ * by using the "egw()" function.
+ *
+ * @param {IegwAppLocal} _egw egw object to set
+ */
+ setApiInstance(_egw: IegwAppLocal)
+ {
+ this._egw = _egw;
+ }
+
+ protected _mgrs = {};
+
+ /**
+ * Sets all array manager objects - this function can be used to set the
+ * root array managers of the container object.
+ *
+ * @param {object} _mgrs
+ */
+ setArrayMgrs(_mgrs)
+ {
+ this._mgrs = et2_cloneObject(_mgrs);
+ }
+
+ /**
+ * Returns an associative array containing the top-most array managers.
+ *
+ * @param _mgrs is used internally and should not be supplied.
+ */
+ getArrayMgrs(_mgrs?: object)
+ {
+ if (typeof _mgrs == "undefined")
{
- // Get the content manager
- var mgrs = this.getArrayMgrs();
-
- for (var key in mgrs)
- {
- var mgr = mgrs[key];
-
- // Get the original content manager if we have already created a
- // perspective for this node
- if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
- {
- mgr = mgr.parentMgr;
- }
-
- // Check whether the manager has a namespace for the id of this object
- var entry = mgr.getEntry(this.id);
- if (typeof entry === 'object' && entry !== null || this.id)
- {
- // The content manager has an own node for this object, so
- // create an own perspective.
- this._mgrs[key] = mgr.openPerspective(this, this.id);
- }
- else
- {
- // The current content manager does not have an own namespace for
- // this element, so use the content manager of the parent.
- delete (this._mgrs[key]);
- }
- }
+ _mgrs = {};
}
- /**
- * Widgets that do support a namespace should override and return true.
- *
- * Since a private attribute doesn't get instanciated properly before it's needed,
- * we use a method so we can get what we need while still in the constructor.
- *
- * @private
- */
- protected _createNamespace(): boolean
+ // Add all managers of this object to the result, if they have not already
+ // been set in the result
+ for (var key in this._mgrs)
{
- return false;
+ if (typeof _mgrs[key] == "undefined")
+ {
+ _mgrs[key] = this._mgrs[key];
+ }
}
- /**
- * This is used and therefore it we can not (yet) make it private
- *
- * @deprecated use this.getInstanceMgr()
- */
- _inst = null;
-
- /**
- * Sets the instance manager object (of type etemplate2, see etemplate2.js)
- *
- * @param {etemplate2} _inst
- */
- setInstanceManager(_inst)
+ // Recursively applies this function to the parent widget
+ if (this._parent)
{
- this._inst = _inst;
+ this._parent.getArrayMgrs(_mgrs);
}
- /**
- * Returns the instance manager
- *
- * @return {etemplate2}
- */
- getInstanceManager()
+ return _mgrs;
+ }
+
+ /**
+ * Sets the array manager for the given part
+ *
+ * @param {string} _part which array mgr to set
+ * @param {object} _mgr
+ */
+ setArrayMgr(_part: string, _mgr: et2_arrayMgr)
+ {
+ this._mgrs[_part] = _mgr;
+ }
+
+ /**
+ * Returns the array manager object for the given part
+ *
+ * @param {string} managed_array_type name of array mgr to return
+ */
+ getArrayMgr(managed_array_type: string): et2_arrayMgr | null
+ {
+ if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
{
- if (this._inst != null)
- {
- return this._inst;
- }
- else if (this._parent)
- {
- return this._parent.getInstanceManager();
- }
-
- return null;
+ return this._mgrs[managed_array_type];
}
-
- /**
- * Returns the path into the data array. By default, array manager takes care of
- * this, but some extensions need to override this
- */
- getPath()
+ else if (this._parent)
{
- var path = this.getArrayMgr("content").getPath();
-
- // Prevent namespaced widgets with value from going an extra layer deep
- if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop();
-
- return path;
+ return this._parent.getArrayMgr(managed_array_type);
}
+
+ return null;
+ }
+
+ /**
+ * Checks whether a namespace exists for this element in the content array.
+ * If yes, an own perspective of the content array is created. If not, the
+ * parent content manager is used.
+ *
+ * Constructor attributes are passed in case a child needs to make decisions
+ */
+ checkCreateNamespace(_attrs?: any)
+ {
+ // Get the content manager
+ var mgrs = this.getArrayMgrs();
+
+ for (var key in mgrs)
+ {
+ var mgr = mgrs[key];
+
+ // Get the original content manager if we have already created a
+ // perspective for this node
+ if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
+ {
+ mgr = mgr.parentMgr;
+ }
+
+ // Check whether the manager has a namespace for the id of this object
+ var entry = mgr.getEntry(this.id);
+ if (typeof entry === 'object' && entry !== null || this.id)
+ {
+ // The content manager has an own node for this object, so
+ // create an own perspective.
+ this._mgrs[key] = mgr.openPerspective(this, this.id);
+ }
+ else
+ {
+ // The current content manager does not have an own namespace for
+ // this element, so use the content manager of the parent.
+ delete (this._mgrs[key]);
+ }
+ }
+ }
+
+ /**
+ * Widgets that do support a namespace should override and return true.
+ *
+ * Since a private attribute doesn't get instanciated properly before it's needed,
+ * we use a method so we can get what we need while still in the constructor.
+ *
+ * @private
+ */
+ protected _createNamespace(): boolean
+ {
+ return false;
+ }
+
+ /**
+ * This is used and therefore it we can not (yet) make it private
+ *
+ * @deprecated use this.getInstanceMgr()
+ */
+ _inst = null;
+
+ /**
+ * Sets the instance manager object (of type etemplate2, see etemplate2.js)
+ *
+ * @param {etemplate2} _inst
+ */
+ setInstanceManager(_inst)
+ {
+ this._inst = _inst;
+ }
+
+ /**
+ * Returns the instance manager
+ *
+ * @return {etemplate2}
+ */
+ getInstanceManager()
+ {
+ if (this._inst != null)
+ {
+ return this._inst;
+ }
+ else if (this._parent)
+ {
+ return this._parent.getInstanceManager();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the path into the data array. By default, array manager takes care of
+ * this, but some extensions need to override this
+ */
+ getPath()
+ {
+ var path = this.getArrayMgr("content").getPath();
+
+ // Prevent namespaced widgets with value from going an extra layer deep
+ if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop();
+
+ return path;
+ }
}
diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts
index 3f6f4ce868..6f75ffb99a 100644
--- a/api/js/etemplate/etemplate2.ts
+++ b/api/js/etemplate/etemplate2.ts
@@ -25,6 +25,7 @@ import '../jsapi/egw_json.js';
import {egwIsMobile} from "../egw_action/egw_action_common.js";
import './Et2Box';
import './Et2Button';
+import './Et2Date';
import './Et2Textbox';
/* Include all widget classes here, we only care about them registering, not importing anything*/
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
@@ -88,1383 +89,1383 @@ import './vfsSelectUI';
*/
export class etemplate2
{
- /**
- * List of loaded templates
- */
- public static templates = {};
- /**
- * List of etemplates by loaded template
- */
- private static _byTemplate = {};
+ /**
+ * List of loaded templates
+ */
+ public static templates = {};
+ /**
+ * List of etemplates by loaded template
+ */
+ private static _byTemplate = {};
- private _etemplate_exec_id: string;
- private readonly menuaction: string;
- name: string;
- private uniqueId: void | string;
- private template_base_url: string;
+ private _etemplate_exec_id: string;
+ private readonly menuaction: string;
+ name: string;
+ private uniqueId: void | string;
+ private template_base_url: string;
- private _widgetContainer: et2_container;
- private _DOMContainer: HTMLElement;
+ private _widgetContainer: et2_container;
+ private _DOMContainer: HTMLElement;
- private resize_timeout: number | boolean;
- private destroy_session: any;
- private close_prompt: any;
- private _skip_close_prompt: boolean;
- private app_obj: EgwApp;
- app: string;
+ private resize_timeout: number | boolean;
+ private destroy_session: any;
+ private close_prompt: any;
+ private _skip_close_prompt: boolean;
+ private app_obj: EgwApp;
+ app: string;
- constructor(_container: HTMLElement, _menuaction?: string, _uniqueId?: string)
+ constructor(_container: HTMLElement, _menuaction?: string, _uniqueId?: string)
+ {
+ if (typeof _menuaction == "undefined")
{
- if (typeof _menuaction == "undefined")
+ _menuaction = "EGroupware\\Api\\Etemplate::ajax_process_content";
+ }
+
+ // Copy the given parameters
+ this._DOMContainer = _container;
+ this.menuaction = _menuaction;
+
+ // Unique ID to prevent DOM collisions across multiple templates
+ this.uniqueId = _uniqueId ? _uniqueId : (_container.getAttribute("id") ? _container.getAttribute("id").replace('.', '-') : '');
+
+ /**
+ * Preset the object variable
+ * @type {et2_container}
+ */
+ this._widgetContainer = null;
+
+
+ // List of templates (XML) that are known, not always used. Indexed by id.
+ // We share list of templates with iframes and popups
+ try
+ {
+ if (opener && opener.etemplate2)
+ {
+ etemplate2.templates = opener.etemplate2.templates;
+ }
+ // @ts-ignore
+ else if (top.etemplate2)
+ {
+ // @ts-ignore
+ etemplate2.templates = top.etemplate2.templates;
+ }
+ }
+ catch (e)
+ {
+ // catch security exception if opener is from a different domain
+ console.log('Security exception accessing etemplate2.prototype of opener or top!');
+ }
+ if (typeof etemplate2.templates == "undefined")
+ {
+ etemplate2.templates = {};
+ }
+ }
+
+ /**
+ * Calls the resize event of all widgets
+ *
+ * @param {jQuery.event} e
+ */
+ public resize(e)
+ {
+ const event = e;
+ const self = this;
+ let excess_height: number | boolean = false;
+
+ // Check if the framework has an specific excess height calculation
+ if (typeof window.framework != 'undefined' && typeof window.framework.get_wExcessHeight != 'undefined')
+ {
+ excess_height = window.framework.get_wExcessHeight(window);
+ }
+
+ //@TODO implement getaccess height for other framework and remove
+ if (typeof event != 'undefined' && event.type == 'resize')
+ {
+ if (this.resize_timeout)
+ {
+ clearTimeout(this.resize_timeout);
+ }
+ this.resize_timeout = setTimeout(function ()
+ {
+ self.resize_timeout = false;
+ if (self._widgetContainer)
{
- _menuaction = "EGroupware\\Api\\Etemplate::ajax_process_content";
+ const appHeader = jQuery('#divAppboxHeader');
+
+ //Calculate the excess height
+ excess_height = egw(window).is_popup() ? jQuery(window).height() - jQuery(self._DOMContainer).height() - appHeader.outerHeight() + 11 : 0;
+ // Recalculate excess height if the appheader is shown
+ if (appHeader.length > 0 && appHeader.is(':visible')) excess_height -= appHeader.outerHeight() - 9;
+
+ // Do not resize if the template height is bigger than screen available height
+ // For templates which have sub templates and they are bigger than screenHeight
+ if (screen.availHeight < jQuery(self._DOMContainer).height()) excess_height = 0;
+
+ // If we're visible, call the "resize" event of all functions which implement the
+ // "IResizeable" interface
+ if (jQuery(self.DOMContainer).is(":visible"))
+ {
+ self._widgetContainer.iterateOver(function (_widget)
+ {
+ _widget.resize(excess_height);
+ }, self, et2_IResizeable);
+ }
+ }
+ }, 100);
+ }
+ // Initial resize needs to be resized immediately (for instance for nextmatch resize)
+ else if (this._widgetContainer)
+ {
+ // Call the "resize" event of all functions which implement the
+ // "IResizeable" interface
+ this._widgetContainer.iterateOver(function (_widget)
+ {
+ _widget.resize(excess_height);
+ }, this, et2_IResizeable);
+ }
+ };
+
+ /**
+ * Clears the current instance.
+ * @param _keep_app_object keep app object
+ * @param _keep_session keep server-side et2 session eg. for vfs-select
+ */
+ public clear(_keep_app_object?: boolean, _keep_session?: boolean)
+ {
+ jQuery(this._DOMContainer).trigger('clear');
+
+ // Remove any handlers on window (resize)
+ if (this.uniqueId)
+ {
+ jQuery(window).off("." + this.uniqueId);
+ }
+
+ // call our destroy_session handler, if it is not already unbind, and unbind it after
+ if (this.destroy_session)
+ {
+ if (!_keep_session) this.destroy_session();
+ this.unbind_unload();
+ }
+ if (this._widgetContainer != null)
+ {
+ // Un-register handler
+ this._widgetContainer.egw().unregisterJSONPlugin(this.handle_assign, this, 'assign');
+
+ this._widgetContainer.destroy();
+ this._widgetContainer = null;
+ }
+ jQuery(this._DOMContainer).empty();
+
+ // Remove self from the index
+ for (const name in etemplate2.templates)
+ {
+ if (typeof etemplate2._byTemplate[name] == "undefined") continue;
+ for (let i = 0; i < etemplate2._byTemplate[name].length; i++)
+ {
+ if (etemplate2._byTemplate[name][i] === this)
+ {
+ etemplate2._byTemplate[name].splice(i, 1);
+ }
+ }
+ }
+
+ // If using a private app object, remove all of them
+ if (!_keep_app_object && this.app_obj !== window.app)
+ {
+ for (const app_name in this.app_obj)
+ {
+ if (this.app_obj[app_name] instanceof EgwApp)
+ {
+ this.app_obj[app_name].destroy();
+ }
+ }
+ }
+ }
+
+ get widgetContainer(): et2_container
+ {
+ return this._widgetContainer;
+ }
+
+ get DOMContainer(): HTMLElement
+ {
+ return this._DOMContainer;
+ }
+
+ get etemplate_exec_id(): string
+ {
+ return this._etemplate_exec_id;
+ }
+
+ /**
+ * Creates an associative array containing the data array managers for each part
+ * of the associative data array. A part is something like "content", "readonlys"
+ * or "sel_options".
+ *
+ * @param {object} _data object with values for attributes content, sel_options, readonlys, modifications
+ */
+ private _createArrayManagers(_data)
+ {
+ if (typeof _data == "undefined")
+ {
+ _data = {};
+ }
+
+ // Create all neccessary _data entries
+ const neededEntries = ["content", "sel_options", "readonlys", "modifications",
+ "validation_errors"];
+ for (let i = 0; i < neededEntries.length; i++)
+ {
+ if (typeof _data[neededEntries[i]] == "undefined" || !_data[neededEntries[i]])
+ {
+ egw.debug("log", "Created not passed entry '" + neededEntries[i] +
+ "' in data array.");
+ _data[neededEntries[i]] = {};
+ }
+ }
+
+ const result = {};
+
+ // Create an array manager object for each part of the _data array.
+ for (const key in _data)
+ {
+ switch (key)
+ {
+ case "etemplate_exec_id": // already processed
+ case "app_header":
+ break;
+ case "readonlys":
+ result[key] = new et2_readonlysArrayMgr(_data[key]);
+ result[key].perspectiveData.owner = this._widgetContainer;
+ break;
+ default:
+ result[key] = new et2_arrayMgr(_data[key]);
+ result[key].perspectiveData.owner = this._widgetContainer;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Bind our unload handler to notify server that eT session/request no longer needed
+ *
+ * We only bind, if we have an etemplate_exec_id: not the case for pure client-side
+ * calls, eg. via et2_dialog.
+ */
+ bind_unload()
+ {
+ // Prompt user to save for dirty popups
+ if (window !== egw_topWindow() && !this.close_prompt)
+ {
+ this.close_prompt = this._close_changed_prompt.bind(this);
+ window.addEventListener("beforeunload", this.close_prompt);
+ }
+ if (this._etemplate_exec_id)
+ {
+ this.destroy_session = jQuery.proxy(function (ev)
+ {
+ // need to use async === "keepalive" to run via beforeunload
+ egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session",
+ [this._etemplate_exec_id], null, null, "keepalive").sendRequest();
+ }, this);
+
+ window.addEventListener("beforeunload", this.destroy_session);
+ }
+ }
+
+ private _close_changed_prompt(e: BeforeUnloadEvent)
+ {
+ if (this._skip_close_prompt || !this.isDirty())
+ {
+ return;
+ }
+
+ // Cancel the event
+ e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
+
+ // Chrome requires returnValue to be set
+ e.returnValue = '';
+ }
+
+ public skip_close_prompt(skip = true)
+ {
+ this._skip_close_prompt = skip;
+ }
+
+ /**
+ * Unbind our unload handler
+ */
+ unbind_unload()
+ {
+ window.removeEventListener("beforeunload", this.destroy_session);
+ window.removeEventListener("beforeunload", this.close_prompt);
+ if (window.onbeforeunload === this.destroy_session)
+ {
+ window.onbeforeunload = null;
+ }
+ else
+ {
+ const onbeforeunload = window.onbeforeunload;
+ window.onbeforeunload = null;
+ // bind unload handler again (can NOT do it direct, as this would be quick enough to be still triggered!)
+ window.setTimeout(function ()
+ {
+ window.onbeforeunload = onbeforeunload;
+ }, 100);
+ }
+ delete this.destroy_session;
+ }
+
+ /**
+ * Download a URL not triggering our unload handler and therefore destroying our et2 request
+ *
+ * We use a new anchor element to avoid not destroying other etemplates as well, which
+ * is what happens if we use window.location
+ *
+ * @param {string} _url
+ */
+ download(_url)
+ {
+ const a = document.createElement('a');
+ a.href = _url;
+ a.download = 'download';
+
+ // Programmatically trigger a click on the anchor element
+ a.click();
+ }
+
+ /**
+ * Loads the template from the given URL and sets the data object
+ *
+ * @param {string} _name name of template
+ * @param {string} _url url to load template
+ * @param {object} _data object with attributes content, langRequire, etemplate_exec_id, ...
+ * @param {function} _callback called after template is loaded
+ * @param {object} _app local app object
+ * @param {boolean} _no_et2_ready true: do not send et2_ready, used by et2_dialog to not overwrite app.js et2 object
+ * @param {string} _open_target flag of string to distinguish between tab target and normal app object
+ * @return Promise
+ */
+ async load(_name, _url, _data, _callback?, _app?, _no_et2_ready?, _open_target?)
+ {
+ let app = _app || window.app;
+ this.name = _name; // store top-level template name to have it available in widgets
+ // store template base url, in case initial template is loaded via webdav, to use that for further loads too
+ // need to split off domain first, as it could contain app-name part of template eg. stylite.report.xet and https://my.stylite.de/egw/...
+ if (_url && _url[0] != '/')
+ {
+ this.template_base_url = _url.match(/https?:\/\/[^/]+/).shift();
+ _url = _url.split(this.template_base_url)[1];
+ }
+ else
+ {
+ this.template_base_url = '';
+ }
+ this.template_base_url += _url.split(_name.split('.').shift())[0];
+
+ egw().debug("info", "Loaded data", _data);
+ const currentapp = this.app = _data.currentapp || egw().app_name();
+ const appname = _name.split('.')[0];
+ // if no app object provided and template app is not currentapp (eg. infolog CRM view)
+ // create private app object / closure with just classes / prototypes
+ if (!_app && appname && appname != currentapp || _open_target)
+ {
+ app = {classes: window.app.classes};
+ }
+ // remember used app object, to eg. use: onchange="widget.getInstanceMgr().app_object[app].callback()"
+ this.app_obj = app;
+
+ // extract $content['msg'] and call egw.message() with it
+ const msg = _data.content.msg;
+ if (typeof msg != 'undefined')
+ {
+ egw(window).message(msg);
+ delete _data.content.msg;
+ }
+
+ // Register a handler for AJAX responses
+ egw(currentapp, window).registerJSONPlugin(this.handle_assign, this, 'assign');
+
+ if (egw.debug_level() >= 3)
+ {
+ if (console.groupCollapsed)
+ {
+ egw.window.console.groupCollapsed("Loading %s into ", _name, '#' + this._DOMContainer.id);
+ }
+ }
+ // Timing & profiling on debug level 'log' (4)
+ if (egw.debug_level() >= 4)
+ {
+ if (console.time)
+ {
+ console.time(_name);
+ }
+ if (console.profile)
+ {
+ console.profile(_name);
+ }
+ var start_time = (new Date).getTime();
+ }
+
+ // require necessary translations from server AND the app.js file, if not already loaded
+ let promisses = [window.egw_ready]; // to wait for legacy-loaded JS
+ if (Array.isArray(_data.langRequire))
+ {
+ promisses.push(egw(currentapp, window).langRequire(window, _data.langRequire));
+ }
+ return Promise.all(promisses).catch((err) =>
+ {
+ console.log("et2.load(): error loading lang-files and app.js: " + err.message);
+ }).then(() =>
+ {
+ this.clear();
+
+ // Initialize application js
+ let app_callback = null;
+ // Only initialize once
+ // new app class with constructor function in app.classes[appname]
+ if (typeof app[appname] !== 'object' && typeof app.classes[appname] == 'function')
+ {
+ app[appname] = new app.classes[appname]();
+ }
+ else if (appname && typeof app[appname] !== "object")
+ {
+ egw.debug("warn", "Did not load '%s' JS object", appname);
+ }
+ // If etemplate current app does not match app owning the template,
+ // initialize the current app too
+ if (typeof app[this.app] !== 'object' && typeof app.classes[this.app] == 'function')
+ {
+ app[this.app] = new app.classes[this.app]();
+ }
+ if (typeof app[appname] == "object")
+ {
+ app_callback = function (_et2, _name)
+ {
+ app[appname].et2_ready(_et2, _name);
+ };
+ }
+
+ // Create the basic widget container and attach it to the DOM
+ this._widgetContainer = new et2_container(null);
+ this._widgetContainer.setApiInstance(egw(currentapp, egw.elemWindow(this._DOMContainer)));
+ this._widgetContainer.setInstanceManager(this);
+ this._widgetContainer.setParentDOMNode(this._DOMContainer);
+
+ // store the id to submit it back to server
+ if (_data)
+ {
+ this._etemplate_exec_id = _data.etemplate_exec_id;
+ // set app_header
+ if (typeof _data.app_header == 'string')
+ {
+ // @ts-ignore
+ window.egw_app_header(_data.app_header);
+ }
+ // bind our unload handler
+ this.bind_unload();
+ }
+
+ const _load = function ()
+ {
+ egw.debug("log", "Loading template...");
+ if (egw.debug_level() >= 4 && console.timeStamp)
+ {
+ console.timeStamp("Begin rendering template");
}
- // Copy the given parameters
- this._DOMContainer = _container;
- this.menuaction = _menuaction;
+ // Add into indexed list - do this before, so anything looking can find it,
+ // even if it's not loaded
+ if (typeof etemplate2._byTemplate[_name] == "undefined")
+ {
+ etemplate2._byTemplate[_name] = [];
+ }
+ etemplate2._byTemplate[_name].push(this);
- // Unique ID to prevent DOM collisions across multiple templates
- this.uniqueId = _uniqueId ? _uniqueId : (_container.getAttribute("id") ? _container.getAttribute("id").replace('.', '-') : '');
+ // Read the XML structure of the requested template
+ this._widgetContainer.loadFromXML(etemplate2.templates[this.name]);
- /**
- * Preset the object variable
- * @type {et2_container}
- */
- this._widgetContainer = null;
+ // List of Promises from widgets that are not quite fully loaded
+ const deferred = [];
+
+ // Inform the widget tree that it has been successfully loaded.
+ this._widgetContainer.loadingFinished(deferred);
+
+ // Connect to the window resize event
+ jQuery(window).on("resize." + this.uniqueId, this, function (e)
+ {
+ e.data.resize(e);
+ });
+
+ if (egw.debug_level() >= 3 && console.groupEnd)
+ {
+ egw.window.console.groupEnd();
+ }
+ if (deferred.length > 0)
+ {
+ let still_deferred = 0;
+ jQuery(deferred).each(function ()
+ {
+ if (this.state() == "pending") still_deferred++;
+ });
+ if (still_deferred > 0)
+ {
+ egw.debug("log", "Template loaded, waiting for %d/%d deferred to finish...", still_deferred, deferred.length);
+ }
+ }
+
+ // Wait for everything to be loaded, then finish it up
+ jQuery.when.apply(jQuery, deferred).done(jQuery.proxy(function ()
+ {
+ egw.debug("log", "Finished loading %s, triggering load event", _name);
+
+ if (typeof window.framework != 'undefined' && typeof window.framework.et2_loadingFinished != 'undefined')
+ {
+ //Call loading finished method of the framework with local window
+ window.framework.et2_loadingFinished(egw(window).window);
+ }
+ // Trigger the "resize" event
+ this.resize();
+
+ // Automatically set focus to first visible input for popups
+ if (this._widgetContainer._egw.is_popup() && jQuery('[autofocus]', this._DOMContainer).focus().length == 0)
+ {
+ const $input = jQuery('input:visible', this._DOMContainer)
+ // Date fields open the calendar popup on focus
+ .not('.et2_date')
+ .filter(function ()
+ {
+ // Skip inputs that are out of tab ordering
+ const $this = jQuery(this);
+ return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0;
+ }).first();
+
+ // mobile device, focus only if the field is empty (usually means new entry)
+ // should focus always for non-mobile one
+ if (egwIsMobile() && $input.val() == "" || !egwIsMobile()) $input.focus();
+ }
+
+ // Tell others about it
+ if (typeof _callback == "function")
+ {
+ _callback.call(window, this, _name);
+ }
+ if (app_callback && _callback != app_callback && !_no_et2_ready)
+ {
+ app_callback.call(window, this, _name);
+ }
+ if (appname && appname != this.app && typeof app[this.app] == "object" && !_no_et2_ready)
+ {
+ // Loaded a template from a different application?
+ // Let the application that loaded it know too
+ app[this.app].et2_ready(this, this.name);
+ }
+
+ jQuery(this._DOMContainer).trigger('load', 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
+ if (egw.debug_level() >= 4)
+ {
+ if (console.timeEnd)
+ {
+ console.timeEnd(_name);
+ }
+ if (console.profileEnd)
+ {
+ console.profileEnd(_name);
+ }
+ const end_time = (new Date).getTime();
+ let gen_time_div = jQuery('#divGenTime_' + appname);
+ if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime');
+ gen_time_div.find('.et2RenderTime').remove();
+ gen_time_div.append('' + egw.lang('eT2 rendering took %1s', '' + ((end_time - start_time) / 1000)) + '');
+ }
+ }, this));
+ };
- // List of templates (XML) that are known, not always used. Indexed by id.
- // We share list of templates with iframes and popups
+ // Load & process
+ try
+ {
+ 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);
+ });
+ }
+
+ /**
+ * Check if template contains any dirty (unsaved) content
+ *
+ * @returns {Boolean}
+ */
+ public isDirty()
+ {
+ let dirty = false;
+ this._widgetContainer.iterateOver(function (_widget)
+ {
+ if (_widget.isDirty && _widget.isDirty())
+ {
+ console.info(_widget.id + " is dirty", _widget);
+ dirty = true;
+ }
+ }, this);
+
+ return dirty;
+ }
+
+ /**
+ * Submit the et2_container form to a blank iframe in order to activate browser autocomplete
+ */
+ autocomplete_fixer()
+ {
+ const self = this;
+ const form = self._DOMContainer;
+
+ // Safari always do the autofill for password field regardless of autocomplete = off
+ // and since there's no other way to switch the autocomplete of, we should switch the
+ // form autocomplete off (e.g. compose dialog, attachment password field)
+ if (navigator.userAgent.match(/safari/i) && !navigator.userAgent.match(/chrome/i)
+ && jQuery('input[type="password"]').length > 0)
+ {
+ return;
+ }
+
+ if (form)
+ {
+ // Stop submit propagation in order to not fire other possible submit events
+ form.onsubmit = function (e)
+ {
+ e.stopPropagation();
+ };
+
+ // Firefox give a security warning when transmitting to "about:blank" from a https site
+ // we work around that by giving existing etemplate/empty.html url
+ // Safari shows same warning, thought Chrome userAgent also includes Safari
+ if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i))
+ {
+ jQuery(form).attr({action: egw.webserverUrl + '/api/templates/default/empty.html', method: 'post'});
+ }
+ // need to trigger submit because submit() would not trigger onsubmit event
+ // since the submit does not get fired directly via user interaction.
+ jQuery(form).trigger('submit');
+ }
+ }
+
+ private _set_button(button, values)
+ {
+ if (typeof button == 'string')
+ {
+ button = this._widgetContainer.getWidgetById(button);
+ }
+ // Button parameter used for submit buttons in datagrid
+ // TODO: This should probably go in nextmatch's getValues(), along with selected rows somehow.
+ // I'm just not sure how.
+ if (button && !values.button)
+ {
+ let i;
+ values.button = {};
+ const path = button.getPath();
+ let target = values;
+ for (i = 0; i < path.length; i++)
+ {
+ if (!values[path[i]]) values[path[i]] = {};
+ target = values[path[i]];
+ }
+ if (target != values || button.id.indexOf('[') != -1 && path.length == 0)
+ {
+ let indexes = button.id.split('[');
+ if (indexes.length > 1)
+ {
+ indexes = [indexes.shift(), indexes.join('[')];
+ indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
+ const children = indexes[1].split('][');
+ if (children.length)
+ {
+ indexes = jQuery.merge([indexes[0]], children);
+ }
+ }
+ let idx = '';
+ for (i = 0; i < indexes.length; i++)
+ {
+ idx = indexes[i];
+ if (!target[idx] || target[idx]['$row_cont']) target[idx] = i < indexes.length - 1 ? {} : true;
+ target = target[idx];
+ }
+ }
+ else if (typeof values.button == 'undefined' || jQuery.isEmptyObject(values.button))
+ {
+ delete values.button;
+ values[button.id] = true;
+ }
+ }
+ }
+
+ /**
+ * Submit form via ajax
+ *
+ * @param {(et2_button|string)} button button widget or string with id
+ * @param {boolean|string} async true: do an asynchronious submit, string: spinner message (please wait...)
+ * default is asynchronoush with message
+ * @param {boolean} no_validation - Do not do individual widget validation, just submit their current values
+ * @param {et2_widget|undefined} _container container to submit, default whole template
+ * @return {boolean} true if submit was send, false if eg. validation stoped submit
+ */
+ submit(button, async, no_validation, _container)
+ {
+ const api = this._widgetContainer.egw();
+
+ if (typeof no_validation == 'undefined')
+ {
+ no_validation = false;
+ }
+ const container = _container || this._widgetContainer;
+
+ // Get the form values
+ const values = this.getValues(container);
+
+ // Trigger the submit event
+ let canSubmit = true;
+ let invalid = null;
+ if (!no_validation)
+ {
+ container.iterateOver(function (_widget)
+ {
+ if (_widget.submit(values) === false)
+ {
+ if (!invalid && !_widget.isValid())
+ {
+ invalid = _widget;
+ }
+ canSubmit = false;
+ }
+ }, this, et2_ISubmitListener);
+ }
+
+ if (canSubmit)
+ {
+ if (typeof async == 'undefined' || typeof async == 'string')
+ {
+ api.loading_prompt('et2_submit_spinner', true, api.lang(typeof async == 'string' ? async : 'Please wait...'));
+ async = true;
+ }
+ if (button) this._set_button(button, values);
+
+ // Create the request object
+ if (this.menuaction)
+ {
+
+ //Autocomplete
+ this.autocomplete_fixer();
+
+ // unbind our session-destroy handler, as we are submitting
+ this.unbind_unload();
+
+
+ const request = api.json(this.menuaction, [this._etemplate_exec_id, values, no_validation], function ()
+ {
+ api.loading_prompt('et2_submit_spinner', false);
+ }, this, async);
+ request.sendRequest();
+ }
+ else
+ {
+ this._widgetContainer.egw().debug("warn", "Missing menuaction for submit. Values: ", values);
+ }
+ }
+ else if (invalid !== null)
+ {
+ // Show the first invalid widget, not the last
+ let messages = [];
+ let valid = invalid.isValid(messages);
+ invalid.set_validation_error(messages);
+ }
+ return canSubmit;
+ }
+
+ /**
+ * Does a full form post submit necessary for downloads
+ *
+ * Only use this one if you need it, use the ajax submit() instead.
+ * It ensures eT2 session continues to exist on server by unbinding unload handler and rebinding it.
+ *
+ * @param {(et2_button|string)} button button widget or string with id
+ */
+ postSubmit(button)
+ {
+ // Get the form values
+ const values = this.getValues(this._widgetContainer);
+
+ // Trigger the submit event
+ let canSubmit = true;
+ this._widgetContainer.iterateOver(function (_widget)
+ {
+ if (_widget.submit(values) === false)
+ {
+ canSubmit = false;
+ }
+ }, this, et2_ISubmitListener);
+
+ if (canSubmit)
+ {
+ if (button) this._set_button(button, values);
+
+ // unbind our session-destroy handler, as we are submitting
+ this.unbind_unload();
+
+ const form = jQuery("