diff --git a/api/js/etemplate/Et2Box.ts b/api/js/etemplate/Et2Box.ts index 3ae8489f8b..1bfefdf630 100644 --- a/api/js/etemplate/Et2Box.ts +++ b/api/js/etemplate/Et2Box.ts @@ -9,18 +9,18 @@ */ -import {css, html, LitElement} from "@lion/core"; +import {css, html, LitElement} from "../../../node_modules/@lion/core/index.js"; import {Et2Widget} from "./Et2Widget"; export class Et2Box extends Et2Widget(LitElement) { - static get styles() - { - return [ - css` + static get styles() + { + return [ + css` :host { - display: block; - width: 100%; + display: block; + width: 100%; } :host > div { display: flex; @@ -31,53 +31,53 @@ export class Et2Box extends Et2Widget(LitElement) ::slotted(*) { /* CSS for child elements */ }`, - ]; - } + ]; + } - render() - { - return html` + render() + { + return html`

Empty box

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