From db7828ad1d6086b452a11915c726462096eac87e Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 10 Aug 2021 15:02:52 -0600 Subject: [PATCH] Add et2-box (& et2-hbox & et2-vbox) WebComponent Box can have legacy & webcomponent children. Used here in infolog edit created & modified row --- api/js/etemplate/et2-box.ts | 83 + api/js/etemplate/et2-button.ts | 194 +- api/js/etemplate/et2-textbox.ts | 55 +- api/js/etemplate/et2_core_inheritance.ts | 777 +++----- api/js/etemplate/et2_core_webComponent.ts | 778 ++++++++ api/js/etemplate/et2_core_widget.ts | 2133 +++++++++++---------- api/js/etemplate/etemplate2.ts | 1 + infolog/templates/default/edit.xet | 16 +- 8 files changed, 2354 insertions(+), 1683 deletions(-) create mode 100644 api/js/etemplate/et2-box.ts create mode 100644 api/js/etemplate/et2_core_webComponent.ts diff --git a/api/js/etemplate/et2-box.ts b/api/js/etemplate/et2-box.ts new file mode 100644 index 0000000000..36e80274a5 --- /dev/null +++ b/api/js/etemplate/et2-box.ts @@ -0,0 +1,83 @@ +/** + * EGroupware eTemplate2 - Box widget + * + * @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, LitElement} from "../../../node_modules/@lion/core/index.js"; +import {Et2Widget} from "./et2_core_webComponent"; + +export class Et2Box extends Et2Widget(LitElement) +{ + static get styles() + { + return [ + css` + :host { + display: block; + width: 100%; + } + :host > div { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: stretch; + } + ::slotted(*) { + /* CSS for child elements */ + }`, + ]; + } + + render() + { + return html` +
+

Empty box

+
`; + } + + _createNamespace(): boolean + { + return true; + } +} + +customElements.define("et2-box", Et2Box); + +export class Et2HBox extends Et2Box +{ + 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` + :host > div { + flex-direction: column; + }` + ]; + } +} + +customElements.define("et2-vbox", Et2VBox); \ No newline at end of file diff --git a/api/js/etemplate/et2-button.ts b/api/js/etemplate/et2-button.ts index 8732deef3e..8bba7b4a19 100644 --- a/api/js/etemplate/et2-button.ts +++ b/api/js/etemplate/et2-button.ts @@ -9,21 +9,22 @@ */ -import {css,html} from "../../../node_modules/@lion/core/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 "./et2_core_inheritance"; +import {Et2Widget} from "./et2_core_webComponent"; -export class Et2Button extends Et2InputWidget(Et2Widget(LionButton)) +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 */ @@ -35,107 +36,114 @@ export class Et2Button extends Et2InputWidget(Et2Widget(LionButton)) width: 20px; padding-right: 3px; }`, - ]; - } + ]; + } - static get properties() { - return { - image: {type: String}, - onclick: {type: Function} - } - } + static get properties() + { + return { + image: {type: String}, + onclick: {type: Function} + } + } - constructor() - { - super(); + constructor() + { + super(); - // Property default values - this.image = ''; + // 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 + // 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(); - }; - } + // 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(); + connectedCallback() + { + super.connectedCallback(); - //this.classList.add("et2_button") + //this.classList.add("et2_button") - if(this.image) - { - this._created_icon_node.src = egw.image(this.image); - this.appendChild(this._created_icon_node); - } + 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)); - } + this.addEventListener("click", this._handleClick.bind(this)); + } - _handleClick(event: MouseEvent) : boolean - { - debugger; - // ignore click on readonly button - if (this.disabled) return false; + _handleClick(event: MouseEvent): boolean + { + debugger; + // ignore click on readonly button + if (this.disabled) return false; - this.clicked = true; + this.clicked = true; - // Cancel buttons don't trigger the close confirmation prompt - if(this.classList.contains("et2_button_cancel")) - { - this.getInstanceManager()?.skip_close_prompt(); - } + // 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; - } + if (!super._handleClick(event)) + { + this.clicked = false; + return false; + } - this.clicked = false; - this.getInstanceManager()?.skip_close_prompt(false); - return true; - } + this.clicked = false; + this.getInstanceManager()?.skip_close_prompt(false); + return true; + } - render() { - return html`
- - -
`; - } - /** - * Implementation of the et2_IInput interface - */ + render() + { + return html` +
+ + +
`; + } - /** - * Always return false as a button is never dirty - */ - isDirty() - { - return false; - } + /** + * Implementation of the et2_IInput interface + */ - resetDirty() - { - } + /** + * Always return false as a button is never dirty + */ + isDirty() + { + return false; + } - getValue() - { - if (this.clicked) - { - return true; - } + resetDirty() + { + } - // If "null" is returned, the result is not added to the submitted - // array. - return null; - } + getValue() + { + if (this.clicked) + { + return true; + } + + // If "null" is returned, the result is not added to the submitted + // array. + return null; + } } -customElements.define("et2-button",Et2Button); + +customElements.define("et2-button", Et2Button); diff --git a/api/js/etemplate/et2-textbox.ts b/api/js/etemplate/et2-textbox.ts index 6317f9dbb9..afc9fea530 100644 --- a/api/js/etemplate/et2-textbox.ts +++ b/api/js/etemplate/et2-textbox.ts @@ -9,42 +9,45 @@ */ -import {css,html} from "../../../node_modules/@lion/core/index.js"; +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 "./et2_core_inheritance"; +import {Et2Widget} from "./et2_core_webComponent"; -export class Et2Textbox extends Et2InputWidget(Et2Widget(LionInput)) +export class Et2Textbox extends Et2InputWidget(Et2Widget(LionInput)) { - static get styles() { - return [ - ...super.styles, - 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); + +customElements.define("et2-textbox", Et2Textbox); diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts index 55f9306fc7..de28bf316d 100644 --- a/api/js/etemplate/et2_core_inheritance.ts +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -13,576 +13,265 @@ */ import {egw, IegwAppLocal} from "../jsapi/egw_global"; -import {et2_checkType, et2_no_init, et2_validateAttrib} from "./et2_core_common"; +import {et2_checkType, et2_cloneObject, et2_no_init, et2_validateAttrib} from "./et2_core_common"; import {et2_IDOMNode, et2_IInput, et2_IInputNode, et2_implements_registry} from "./et2_core_interfaces"; -import {LitElement} from "../../../node_modules/lit-element/lit-element.js"; -import {et2_arrayMgr} from "./et2_core_arrayMgr"; -import {et2_widget} from "./et2_core_widget"; -import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; -import {etemplate2} from "./etemplate2"; export class ClassWithInterfaces { - /** - * The implements function can be used to check whether the object - * implements the given interface. - * - * As TypeScript can not (yet) check if an objects implements an interface on runtime, - * we currently implements with each interface a function called 'implements_'+interfacename - * to be able to check here. - * - * @param _iface name of interface to check - */ - implements (_iface_name : string) - { - if (typeof et2_implements_registry[_iface_name] === 'function' && - et2_implements_registry[_iface_name](this)) + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + implements(_iface_name: string) { - return true + if (typeof et2_implements_registry[_iface_name] === 'function' && + et2_implements_registry[_iface_name](this)) + { + return true + } + return false; } - return false; - } - /** - * Check if object is an instance of a class or implements an interface (specified by the interfaces name) - * - * @param _class_or_interfacename class(-name) or string with name of interface - */ - instanceOf(_class_or_interfacename: any) : boolean - { - if (typeof _class_or_interfacename === 'string') + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + instanceOf(_class_or_interfacename: any): boolean { - return this.implements(_class_or_interfacename); + if (typeof _class_or_interfacename === 'string') + { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; } - return this instanceof _class_or_interfacename; - } } export class ClassWithAttributes extends ClassWithInterfaces { - /** - * Object to collect the attributes we operate on - */ - attributes: object; + /** + * Object to collect the attributes we operate on + */ + attributes: object; - /** - * Returns the value of the given attribute. If the property does not - * exist, an error message is issued. - * - * @param {string} _name - * @return {*} - */ - getAttribute(_name) - { - if (typeof this.attributes[_name] != "undefined" && - !this.attributes[_name].ignore) { - if (typeof this["get_" + _name] == "function") { - return this["get_" + _name](); - } else { - return this[_name]; - } - } else { - egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); - } - } - - /** - * The setAttribute function sets the attribute with the given name to - * the given value. _override defines, whether this[_name] will be set, - * if this key already exists. _override defaults to true. A warning - * is issued if the attribute does not exist. - * - * @param {string} _name - * @param {*} _value - * @param {boolean} _override - */ - setAttribute(_name, _value, _override) - { - if (typeof this.attributes[_name] != "undefined") { - if (!this.attributes[_name].ignore) { - if (typeof _override == "undefined") { - _override = true; - } - - var val = et2_checkType(_value, this.attributes[_name].type, - _name, this); - - if (typeof this["set_" + _name] == "function") { - this["set_" + _name](val); - } else if (_override || typeof this[_name] == "undefined") { - this[_name] = val; - } - } - } else { - egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); - } - } - - /** - * generateAttributeSet sanitizes the given associative array of attributes - * (by passing each entry to "et2_checkType" and checking for existance of - * the attribute) and adds the default values to the associative array. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - static generateAttributeSet(widget, _attrs) - { - // Sanity check and validation - for (var key in _attrs) { - if (typeof widget[key] != "undefined") { - if (!widget[key].ignore) { - _attrs[key] = et2_checkType(_attrs[key], widget[key].type, - key, this); - } - } else { - // Key does not exist - delete it and issue a warning - delete (_attrs[key]); - egw.debug("warn", this, "Attribute '" + key + - "' does not exist in " + _attrs.type + "!"); - } - } - - // Include default values or already set values for this attribute - for (var key in widget) { - if (typeof _attrs[key] == "undefined") { - var _default = widget[key]["default"]; - if (_default == et2_no_init) { - _default = undefined; - } - - _attrs[key] = _default; - } - } - - return _attrs; - } - - /** - * 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. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - initAttributes(_attrs) - { - for (var key in _attrs) { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { - this.setAttribute(key, _attrs[key], false); - } - } - } - - static buildAttributes(class_prototype: object) - { - let class_tree = []; - let attributes = {}; - let n = 0; - do { - n++; - class_tree.push(class_prototype); - class_prototype = Object.getPrototypeOf(class_prototype); - } while (class_prototype !== ClassWithAttributes && n < 50); - - for (let i = class_tree.length - 1; i >= 0; i--) { - attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes); - } - return attributes; - } - - /** - * Extend current _attributes with the one from the parent class - * - * This gives inheritance from the parent plus the ability to override in the current class. - * - * @param _attributes - * @param _parent - */ - static extendAttributes(_parent: object, _attributes: object): object - { - function _copyMerge(_new, _old) + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + * + * @param {string} _name + * @return {*} + */ + getAttribute(_name) { - var result = {}; - - // Copy the new object - if (typeof _new != "undefined") { - for (var key in _new) { - result[key] = _new[key]; + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) + { + if (typeof this["get_" + _name] == "function") + { + return this["get_" + _name](); + } + else + { + return this[_name]; + } } - } - - // Merge the old object - for (var key in _old) { - if (typeof result[key] == "undefined") { - result[key] = _old[key]; + else + { + egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); } - } - - return result; } - var attributes = {}; + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + * + * @param {string} _name + * @param {*} _value + * @param {boolean} _override + */ + setAttribute(_name, _value, _override) + { + if (typeof this.attributes[_name] != "undefined") + { + if (!this.attributes[_name].ignore) + { + if (typeof _override == "undefined") + { + _override = true; + } - // Copy the old attributes - for (var key in _attributes) { - attributes[key] = _copyMerge({}, _attributes[key]); + var val = et2_checkType(_value, this.attributes[_name].type, + _name, this); + + if (typeof this["set_" + _name] == "function") + { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") + { + this[_name] = val; + } + } + } + else + { + egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); + } } - // Add the old attributes to the new ones. If the attributes already - // exist, they are merged. - for (var key in _parent) { - var _old = _parent[key]; + /** + * generateAttributeSet sanitizes the given associative array of attributes + * (by passing each entry to "et2_checkType" and checking for existance of + * the attribute) and adds the default values to the associative array. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + static generateAttributeSet(widget, _attrs) + { + // Sanity check and validation + for (var key in _attrs) + { + if (typeof widget[key] != "undefined") + { + if (!widget[key].ignore) + { + _attrs[key] = et2_checkType(_attrs[key], widget[key].type, + key, this); + } + } + else + { + // Key does not exist - delete it and issue a warning + delete (_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type + "!"); + } + } - attributes[key] = _copyMerge(attributes[key], _old); + // Include default values or already set values for this attribute + for (var key in widget) + { + if (typeof _attrs[key] == "undefined") + { + var _default = widget[key]["default"]; + if (_default == et2_no_init) + { + _default = undefined; + } + + _attrs[key] = _default; + } + } + + return _attrs; } - // Validate the attributes - for (var key in attributes) { - et2_validateAttrib(key, attributes[key]); + /** + * 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. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + initAttributes(_attrs) + { + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) + { + this.setAttribute(key, _attrs[key], false); + } + } } - return attributes; - } + static buildAttributes(class_prototype: object) + { + let class_tree = []; + let attributes = {}; + let n = 0; + do + { + n++; + class_tree.push(class_prototype); + class_prototype = Object.getPrototypeOf(class_prototype); + } + while (class_prototype !== ClassWithAttributes && n < 50); + + for (let i = class_tree.length - 1; i >= 0; i--) + { + attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes); + } + return attributes; + } + + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + static extendAttributes(_parent: object, _attributes: object): object + { + function _copyMerge(_new, _old) + { + var result = {}; + + // Copy the new object + if (typeof _new != "undefined") + { + for (var key in _new) + { + result[key] = _new[key]; + } + } + + // Merge the old object + for (var key in _old) + { + if (typeof result[key] == "undefined") + { + result[key] = _old[key]; + } + } + + return result; + } + + var attributes = {}; + + // Copy the old attributes + for (var key in _attributes) + { + attributes[key] = _copyMerge({}, _attributes[key]); + } + + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _parent) + { + var _old = _parent[key]; + + attributes[key] = _copyMerge(attributes[key], _old); + } + + // Validate the attributes + for (var key in attributes) + { + et2_validateAttrib(key, attributes[key]); + } + + return attributes; + } } - -/** - * This mixin will allow any LitElement to become an Et2Widget - * - * Usage: - * @example - * export class Et2Loading extends Et2Widget(BXLoading) { ... } - * @example - * export class Et2Button extends Et2InputWidget(Et2Widget(BXButton)) { ... } - * - * @see Mixin explanation https://lit.dev/docs/composition/mixins/ - */ - -type Constructor = new (...args: any[]) => T; -export const Et2Widget = >(superClass: T) => { - 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; - - /** 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); - - // Provide *default* property values in constructor - this.label = ""; - this.statustext = ""; - } - - 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() - * - * @param value - */ - set_label(value) - { - let oldValue = this.label; - - // Remove old - let oldLabels = this.getElementsByClassName("et2_label"); - while(oldLabels[0]) - { - this.removeChild(oldLabels[0]); - } - - 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; - } - iterateOver(_callback: Function, _context, _type) - { - if(et2_implements_registry[_type](this)) - { - _callback.call(_context, this); - } - // TODO: children - } - loadingFinished() - { - /** - * 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 createion, and they handle the - * rest themselves with their normal lifecycle (especially connectedCallback(), which is kind - * of the equivalent of doLoadingFinished() - */ - this.getParent().getDOMNode(this).append(this); - } - getWidgetById(_id) - { - if (this.id == _id) { - return this; - } - } - - setParent(new_parent: Et2WidgetClass | et2_widget) - { - this._parent = new_parent; - } - 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; - } - 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; - } - - /** - * 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(_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]); - } - } - } - - /** - * 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() - { - 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_webComponent.ts b/api/js/etemplate/et2_core_webComponent.ts new file mode 100644 index 0000000000..fbb4a267d4 --- /dev/null +++ b/api/js/etemplate/et2_core_webComponent.ts @@ -0,0 +1,778 @@ +import {et2_IDOMNode, et2_implements_registry} from "./et2_core_interfaces"; +import {et2_arrayMgr} from "./et2_core_arrayMgr"; +import {et2_attribute_registry, et2_registry, et2_widget} from "./et2_core_widget"; +import {etemplate2} from "./etemplate2"; +import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; +import {et2_cloneObject, et2_csvSplit} from "./et2_core_common"; +// @ts-ignore +import {IegwAppLocal} from "../jsapi/egw_global"; +import {ClassWithAttributes, ClassWithInterfaces} from "./et2_core_inheritance"; +import {LitElement} from "../../../node_modules/lit-element/lit-element.js"; +import {forEach} from "carbon-web-components/es/globals/internal/collection-helpers"; + +/** + * This mixin will allow any LitElement to become an Et2Widget + * + * Usage: + * @example + * export class Et2Loading extends Et2Widget(BXLoading) { ... } + * @example + * export class Et2Button extends Et2InputWidget(Et2Widget(BXButton)) { ... } + * + * @see Mixin explanation https://lit.dev/docs/composition/mixins/ + */ + +type Constructor = new (...args: any[]) => T; +export const Et2Widget = >(superClass: T) => +{ + 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() + { + 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 = {}; + 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.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]); + + return Et2WidgetClass as unknown as Constructor & T; +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index 4a11e1f8ec..6eec09b139 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -16,7 +16,7 @@ et2_core_arrayMgr; */ -import {ClassWithAttributes, Et2Widget} from './et2_core_inheritance'; +import {ClassWithAttributes} from './et2_core_inheritance'; import {et2_arrayMgr} from "./et2_core_arrayMgr"; import {egw, IegwAppLocal} from "../jsapi/egw_global"; import {et2_cloneObject, et2_csvSplit} from "./et2_core_common"; @@ -24,8 +24,7 @@ import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions"; import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces"; // fixing circular dependencies by only importing type import type {et2_container} from "./et2_core_baseWidget"; -import type {et2_inputWidget, et2_input} from "./et2_core_inputWidget"; -import {Et2InputWidget} from "./et2_core_inputWidget"; +import type {et2_inputWidget} from "./et2_core_inputWidget"; /** * The registry contains all XML tag names and the corresponding widget @@ -44,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++) - { - var type = _types[i].toLowerCase(); - - // Check whether a widget has already been registered for one of the - // types. - if (et2_registry[type]) + et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor); + // Iterate over all given types and register those + for (var i = 0; i < _types.length; i++) { - egw.debug("warn", "Widget class registered for " + type + - " will be overwritten."); - } + var type = _types[i].toLowerCase(); - et2_registry[type] = _constructor; - } + // 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; + } } /** @@ -77,53 +76,55 @@ export function et2_register_widget(_constructor, _types) * is not passed, it will default to null. Then you have to attach the element * to a parent using the addChild or insertChild method. */ -export function et2_createWidget(_name : string, _attrs : object, _parent? : any) : et2_widget +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) if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] = et2_createWidget; -export interface WidgetConfig { - type?: string; - readonly?: boolean; - width?: number; - [propName: string]: any; +export interface WidgetConfig +{ + type?: string; + readonly?: boolean; + width?: number; + + [propName: string]: any; } /** @@ -133,1010 +134,1122 @@ 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" - }, + "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 "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 "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 - }, + /** + * 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; - - /** - * 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") { - _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); - } - - /** - * 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; - } - - 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") { - _type = et2_widget; - } - - 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; - } - - if (this._parent) { - return _vis && this._parent.isInTree(this); - } - - return _vis; - } - - isOfSupportedWidgetClass(_obj) - { - for (var i = 0; i < this.supportedWidgetClasses.length; i++) { - 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; - } - - // 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; + /** + * Widget's attributes + */ + attributes: { + "name": "Widget attributes", + "type": "any", + "ignore": true, + "description": "Object of widget attributes" } - // expand legacyOptions with content - if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { - attrValue = mgr.expandName(attrValue); + }; + + // 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") + { + _parent = null; } - // Parse the legacy options (as a string, other types not allowed) - var splitted = et2_csvSplit(attrValue + ""); + if (typeof _attrs == "undefined") + { + _attrs = {}; + } - 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; + 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"]; - // Check to make sure we don't overwrite a current option with a legacy option - if (typeof _target[legacy[j]] === "undefined") { - attrValue = splitted[j]; + // Add this widget to the given parent widget + if (_parent != null) + { + _parent.addChild(this); + } - /** - 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); + // 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); + } + + /** + * 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); } - var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {}; + _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; + } + + 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") + { + _type = et2_widget; + } + + 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; + } + + if (this._parent) + { + return _vis && this._parent.isInTree(this); + } + + return _vis; + } + + isOfSupportedWidgetClass(_obj) + { + for (var i = 0; i < this.supportedWidgetClasses.length; i++) + { + 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; + } + + // 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; + } + } + } + + /** + * 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]; + } + } + } + } + + // 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; // 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; - } - } - } - - /** - * 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]; - } - } - } - } - - // 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) + if (widget_class.getPropertyOptions(attribute).type == "Boolean") { - return egw.lang(p1); - }); - } - else - { - _attrs[key] = this.egw().lang(value); - } - } - } - } - } + attrValue = mgr.parseBoolExpression(attrValue); + } + else + { + attrValue = mgr.expandName(attrValue); + } + widget.setAttribute(attribute, attrValue); + }); - /** - * 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; - - // 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) + if (widget_class.getPropertyOptions("value") && widget.set_value) { - widget.setAttribute("value", val); + 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); + } } - } - // 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; } - // Children need to be loaded - //this.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++) + { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); - /** - * 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 == "#comment") { - continue; - } + if (widgetType == "#text") + { + if (node.data.replace(/^\s+|\s+$/g, '')) + { + this.loadContent(node.data); + } + continue; + } - if (widgetType == "#text") { - if (node.data.replace(/^\s+|\s+$/g, '')) { - this.loadContent(node.data); + // Create the new element + this.createElementFromNode(node); } - 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; } - var loadChildren = function () { - // 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); + /** + * 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; } - } - }; - var result = this.doLoadingFinished(); - if (typeof result == "boolean" && result) { - // Simple widget finishes nicely - loadChildren.apply(this, arguments); - } else if (typeof result == "object" && result.done) { - // 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)); - } - } + var loadChildren = function () + { + // 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); + } + } + }; - /** - * 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') { - val = et2_compileLegacyJS(val, this, - this.implements(et2_IInputNode) ? (this).getInputNode() : - (this.implements(et2_IDOMNode) ? (this).getDOMNode() : null)); + var result = this.doLoadingFinished(); + if (typeof result == "boolean" && result) + { + // Simple widget finishes nicely + loadChildren.apply(this, arguments); } - 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) { - wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + else if (typeof result == "object" && result.done) + { + // 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)); } - } - - // If we're the root object, return the phpgwapi API instance - return egw('phpgwapi', wnd); } - 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") { - _mgrs = {}; + /** + * 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') + { + val = et2_compileLegacyJS(val, this, + this.implements(et2_IInputNode) ? (this).getInputNode() : + (this.implements(et2_IDOMNode) ? (this).getDOMNode() : null)); + } + this.setAttribute(key, val, false); + } + } } - // 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]; - } + /** + * 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; } - // Recursively applies this function to the parent widget - if (this._parent) { - this._parent.getArrayMgrs(_mgrs); + 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) + { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + + return this._egw; } - 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") { - return this._mgrs[managed_array_type]; - } else if (this._parent) { - return this._parent.getArrayMgr(managed_array_type); + /** + * 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; } - return null; - } + protected _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(_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(); + /** + * 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); } - return null; - } + /** + * 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 = {}; + } - /** - * 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(); + // 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]; + } + } - // Prevent namespaced widgets with value from going an extra layer deep - if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop(); + // Recursively applies this function to the parent widget + if (this._parent) + { + this._parent.getArrayMgrs(_mgrs); + } - return path; - } + 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") + { + return this._mgrs[managed_array_type]; + } + else if (this._parent) + { + 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 5ce297ab61..cf030a3397 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -23,6 +23,7 @@ import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch import {et2_tabbox} from "./et2_widget_tabs"; import '../jsapi/egw_json.js'; import {egwIsMobile} from "../egw_action/egw_action_common.js"; +import './et2-box'; import './et2-button'; import './et2-textbox'; /* Include all widget classes here, we only care about them registering, not importing anything*/ diff --git a/infolog/templates/default/edit.xet b/infolog/templates/default/edit.xet index 44e86bad75..373139b0b3 100644 --- a/infolog/templates/default/edit.xet +++ b/infolog/templates/default/edit.xet @@ -217,20 +217,16 @@ - - - - + + - + - - - - + + - +