forked from extern/egroupware
f7f4053d1a
This lets us add LitElement's updateComplete Promise into the list of things to wait for, and solves the problem of app init code being run before widgets are complete. https://lit.dev/docs/components/lifecycle/#reactive-update-cycle-completing
1189 lines
31 KiB
TypeScript
1189 lines
31 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2 - JS Widget base class
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link https://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
*/
|
|
|
|
/*egw:uses
|
|
jsapi.egw;
|
|
et2_core_xml;
|
|
et2_core_common;
|
|
et2_core_inheritance;
|
|
et2_core_arrayMgr;
|
|
*/
|
|
|
|
import {ClassWithAttributes} from './et2_core_inheritance';
|
|
import {et2_arrayMgr} from "./et2_core_arrayMgr";
|
|
import {egw, IegwAppLocal} from "../jsapi/egw_global";
|
|
import {et2_cloneObject, et2_csvSplit} from "./et2_core_common";
|
|
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
|
|
import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces";
|
|
import {loadWebComponent} from "./Et2Widget/Et2Widget";
|
|
// fixing circular dependencies by only importing type
|
|
import type {et2_container} from "./et2_core_baseWidget";
|
|
import type {et2_inputWidget} from "./et2_core_inputWidget";
|
|
|
|
/**
|
|
* The registry contains all XML tag names and the corresponding widget
|
|
* constructor.
|
|
*/
|
|
export var et2_registry = {};
|
|
export var et2_attribute_registry = {};
|
|
|
|
|
|
/**
|
|
* Registers the widget class defined by the given constructor, registers all its class attributes, and associates it
|
|
* with the types in the _types array.
|
|
*
|
|
* @param {function} _constructor constructor
|
|
* @param {array} _types widget types _constructor wants to register for
|
|
*/
|
|
export function et2_register_widget(_constructor, _types)
|
|
{
|
|
"use strict";
|
|
|
|
et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor);
|
|
// Iterate over all given types and register those
|
|
for (var i = 0; i < _types.length; i++)
|
|
{
|
|
var type = _types[i].toLowerCase();
|
|
|
|
// Check whether a widget has already been registered for one of the
|
|
// types.
|
|
if (et2_registry[type])
|
|
{
|
|
egw.debug("warn", "Widget class registered for " + type +
|
|
" will be overwritten.");
|
|
}
|
|
|
|
et2_registry[type] = _constructor;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a widget registered for the given tag-name. If "readonly" is listed
|
|
* inside the attributes, et2_createWidget will try to use the "_ro" type of the
|
|
* widget.
|
|
*
|
|
* @param _name is the name of the widget with which it is registered. If the
|
|
* widget is not found, an et2_placeholder will be created.
|
|
* @param _attrs is an associative array with attributes. If not passed, it will
|
|
* default to an empty object.
|
|
* @param _parent is the parent to which the element will be attached. If _parent
|
|
* is not passed, it will default to null. Then you have to attach the element
|
|
* to a parent using the addChild or insertChild method.
|
|
*/
|
|
export function et2_createWidget(_name: string, _attrs: object, _parent?: any): et2_widget
|
|
{
|
|
"use strict";
|
|
|
|
if (typeof _attrs == "undefined")
|
|
{
|
|
_attrs = {};
|
|
}
|
|
|
|
if (typeof _attrs != "object")
|
|
{
|
|
_attrs = {};
|
|
}
|
|
|
|
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"];
|
|
|
|
// 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);
|
|
}
|
|
|
|
// make et2_createWidget publicly available as we need to call it from stylite/js/gantt.js (maybe others)
|
|
if (typeof window.et2_createWidget === 'undefined') window['et2_createWidget'] = et2_createWidget;
|
|
|
|
export interface WidgetConfig
|
|
{
|
|
type?: string;
|
|
readonly?: boolean;
|
|
width?: number;
|
|
|
|
[propName: string]: any;
|
|
}
|
|
|
|
/**
|
|
* The et2 widget base class.
|
|
*
|
|
* @augments ClassWithAttributes
|
|
*/
|
|
export class et2_widget extends ClassWithAttributes
|
|
{
|
|
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;
|
|
|
|
/**
|
|
* Widget constructor
|
|
*
|
|
* To implement the attributes inheritance and overriding each extending class/widget needs to call:
|
|
*
|
|
* super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {}));
|
|
*
|
|
* @param _parent is the parent object from the XML tree which contains this
|
|
* object. The default constructor always adds the new instance to the
|
|
* children list of the given parent object. _parent may be NULL.
|
|
* @param _attrs is an associative array of attributes.
|
|
* @param _child attributes object from the child
|
|
*/
|
|
constructor(_parent?, _attrs?: WidgetConfig, _child?: object)
|
|
{
|
|
super(); // because we in the top of the widget hierarchy
|
|
this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {});
|
|
|
|
// Check whether all attributes are available
|
|
if (typeof _parent == "undefined")
|
|
{
|
|
_parent = null;
|
|
}
|
|
|
|
if (typeof _attrs == "undefined")
|
|
{
|
|
_attrs = {};
|
|
}
|
|
|
|
if (_attrs.attributes)
|
|
{
|
|
jQuery.extend(_attrs, _attrs.attributes);
|
|
}
|
|
// Initialize all important parameters
|
|
this._mgrs = {};
|
|
this._inst = null;
|
|
this._children = [];
|
|
this._type = _attrs["type"];
|
|
this.id = _attrs["id"];
|
|
|
|
// Add this widget to the given parent widget
|
|
if (_parent != null)
|
|
{
|
|
_parent.addChild(this);
|
|
}
|
|
|
|
// The supported widget classes array defines a whitelist for all widget
|
|
// classes or interfaces child widgets have to support.
|
|
this.supportedWidgetClasses = [et2_widget, HTMLElement];
|
|
|
|
if (_attrs["id"])
|
|
{
|
|
// Create a namespace for this object
|
|
if (this._createNamespace())
|
|
{
|
|
this.checkCreateNamespace(_attrs);
|
|
}
|
|
}
|
|
|
|
if (this.id)
|
|
{
|
|
//this.id = this.id.replace(/\[/g,'[').replace(/]/g,']');
|
|
}
|
|
|
|
// Add all attributes hidden in the content arrays to the attributes
|
|
// parameter
|
|
this.transformAttributes(_attrs);
|
|
|
|
// Create a local copy of the options object
|
|
this.options = et2_cloneObject(_attrs);
|
|
}
|
|
|
|
/**
|
|
* The destroy function destroys all children of the widget, removes itself
|
|
* from the parents children list.
|
|
* In all classes derrived from et2_widget ALWAYS override the destroy
|
|
* function and remove ALL references to other objects. Also remember to
|
|
* unbind ANY event this widget created and to remove all DOM-Nodes it
|
|
* created.
|
|
*/
|
|
destroy()
|
|
{
|
|
// Call the destructor of all children
|
|
for (var i = this._children.length - 1; i >= 0; i--)
|
|
{
|
|
this._children[i].destroy();
|
|
}
|
|
|
|
// Remove this element from the parent, if it exists
|
|
if (typeof this._parent != "undefined" && this._parent !== null)
|
|
{
|
|
this._parent.removeChild(this);
|
|
}
|
|
|
|
// Free the array managers if they belong to this widget
|
|
for (var key in this._mgrs)
|
|
{
|
|
if (this._mgrs[key] && this._mgrs[key].owner == this)
|
|
{
|
|
this._mgrs[key].destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
getType(): string
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
setType(_type: string)
|
|
{
|
|
this._type = _type;
|
|
}
|
|
|
|
/**
|
|
* Creates a copy of this widget. The parameters given are passed to the
|
|
* constructor of the copied object. If the parameters are omitted, _parent
|
|
* is defaulted to null
|
|
*
|
|
* @param {et2_widget} _parent parent to set for clone, default null
|
|
*/
|
|
clone(_parent)
|
|
{
|
|
// Default _parent to null
|
|
if (typeof _parent == "undefined")
|
|
{
|
|
_parent = null;
|
|
}
|
|
|
|
// Create the copy
|
|
var copy = new (<typeof et2_widget>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 <et2_container><unknown>this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inserts an child at the end of the list.
|
|
*
|
|
* @param _node is the node which should be added. It has to be an instance
|
|
* of et2_widget
|
|
*/
|
|
addChild(_node: et2_widget)
|
|
{
|
|
this.insertChild(_node, this._children.length);
|
|
}
|
|
|
|
/**
|
|
* Inserts a child at the given index.
|
|
*
|
|
* @param _node is the node which should be added. It has to be an instance
|
|
* of et2_widget
|
|
* @param _idx is the position at which the element should be added.
|
|
*/
|
|
insertChild(_node: et2_widget, _idx: number)
|
|
{
|
|
// Check whether the node is one of the supported widget classes.
|
|
if (this.isOfSupportedWidgetClass(_node))
|
|
{
|
|
// Remove the node from its original parent
|
|
if (_node._parent)
|
|
{
|
|
_node._parent.removeChild(_node);
|
|
}
|
|
|
|
_node._parent = this;
|
|
this._children.splice(_idx, 0, _node);
|
|
}
|
|
else
|
|
{
|
|
this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this);
|
|
// throw("Widget is not supported by this widget class!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the child but does not destroy it.
|
|
*
|
|
* @param {et2_widget} _node child to remove
|
|
*/
|
|
removeChild(_node)
|
|
{
|
|
// Retrieve the child from the child list
|
|
var idx = this._children.indexOf(_node);
|
|
|
|
if (idx >= 0)
|
|
{
|
|
// This element is no longer parent of the child
|
|
_node._parent = null;
|
|
|
|
this._children.splice(idx, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Searches an element by id in the tree, descending into the child levels.
|
|
*
|
|
* @param _id is the id you're searching for
|
|
*/
|
|
getWidgetById(_id): et2_widget | null
|
|
{
|
|
if (this.id == _id)
|
|
{
|
|
return this;
|
|
}
|
|
if (!this._children) return null;
|
|
|
|
for (var i = 0; i < this._children.length; i++)
|
|
{
|
|
var elem = this._children[i].getWidgetById(_id);
|
|
|
|
if (elem != null)
|
|
{
|
|
return elem;
|
|
}
|
|
}
|
|
if (this.id && _id.indexOf('[') > -1 && this._children.length)
|
|
{
|
|
var ids = (new et2_arrayMgr()).explodeKey(_id);
|
|
var widget: et2_widget = this;
|
|
for (var i = 0; i < ids.length && widget !== null; i++)
|
|
{
|
|
widget = widget.getWidgetById(ids[i]);
|
|
}
|
|
return widget;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Function which allows iterating over the complete widget tree.
|
|
*
|
|
* @param _callback is the function which should be called for each widget
|
|
* @param _context is the context in which the function should be executed
|
|
* @param _type is an optional parameter which specifies a class/interface
|
|
* the elements have to be instanceOf.
|
|
*/
|
|
iterateOver(_callback, _context, _type?)
|
|
{
|
|
if (typeof _type == "undefined")
|
|
{
|
|
_type = et2_widget;
|
|
}
|
|
|
|
if (this.isInTree() && this.instanceOf(_type))
|
|
{
|
|
_callback.call(_context, this);
|
|
}
|
|
|
|
for (var i = 0; i < this._children.length; i++)
|
|
{
|
|
this._children[i].iterateOver(_callback, _context, _type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the widget currently resides in the visible part of the
|
|
* widget tree. E.g. Templates which have been cloned are not in the visible
|
|
* part of the widget tree.
|
|
*
|
|
* @param _sender
|
|
* @param {boolean} _vis can be used by widgets overwriting this function - simply
|
|
* write
|
|
* return this._super(inTree);
|
|
* when calling this function the _vis parameter does not have to be supplied.
|
|
*/
|
|
isInTree(_sender?, _vis?: boolean)
|
|
{
|
|
if (typeof _vis == "undefined")
|
|
{
|
|
_vis = true;
|
|
}
|
|
|
|
if (this._parent)
|
|
{
|
|
return _vis && this._parent.isInTree(this);
|
|
}
|
|
|
|
return _vis;
|
|
}
|
|
|
|
isOfSupportedWidgetClass(_obj)
|
|
{
|
|
for (var i = 0; i < this.supportedWidgetClasses.length; i++)
|
|
{
|
|
if (_obj instanceof this.supportedWidgetClasses[i])
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The parseXMLAttrs function takes an XML DOM attributes object
|
|
* and adds the given attributes to the _target associative array. This
|
|
* function also parses the legacyOptions.
|
|
*
|
|
* @param _attrsObj is the XML DOM attributes object
|
|
* @param {object} _target is the object to which the attributes should be written.
|
|
* @param {et2_widget} _proto prototype with attributes and legacyOptions attribute
|
|
*/
|
|
parseXMLAttrs(_attrsObj, _target, _proto)
|
|
{
|
|
// Check whether the attributes object is really existing, if not abort
|
|
if (typeof _attrsObj == "undefined")
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Iterate over the given attributes and parse them
|
|
var mgr = this.getArrayMgr("content");
|
|
for (var i = 0; i < _attrsObj.length; i++)
|
|
{
|
|
var attrName = _attrsObj[i].name;
|
|
var attrValue = _attrsObj[i].value;
|
|
|
|
// Special handling for the legacy options
|
|
if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0)
|
|
{
|
|
let legacy = _proto.constructor.legacyOptions || [];
|
|
let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {};
|
|
// Check for modifications on legacy options here. Normal modifications
|
|
// are handled in widget constructor, but it's too late for legacy options then
|
|
if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id))
|
|
{
|
|
var mod: any = this.getArrayMgr("modifications").getEntry(_target.id);
|
|
if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options;
|
|
}
|
|
// expand legacyOptions with content
|
|
if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1)
|
|
{
|
|
attrValue = mgr.expandName(attrValue);
|
|
}
|
|
|
|
// Parse the legacy options (as a string, other types not allowed)
|
|
var splitted = et2_csvSplit(attrValue + "");
|
|
|
|
for (var j = 0; j < splitted.length && j < legacy.length; j++)
|
|
{
|
|
// Blank = not set, unless there's more legacy options provided after
|
|
if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) continue;
|
|
|
|
// Check to make sure we don't overwrite a current option with a legacy option
|
|
if (typeof _target[legacy[j]] === "undefined")
|
|
{
|
|
attrValue = splitted[j];
|
|
|
|
/**
|
|
If more legacy options than expected, stuff them all in the last legacy option
|
|
Some legacy options take a comma separated list.
|
|
*/
|
|
if (j == legacy.length - 1 && splitted.length > legacy.length)
|
|
{
|
|
attrValue = splitted.slice(j);
|
|
}
|
|
|
|
var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {};
|
|
|
|
// If the attribute is marked as boolean, parse the
|
|
// expression as bool expression.
|
|
if (attr.type == "boolean")
|
|
{
|
|
attrValue = mgr.parseBoolExpression(attrValue);
|
|
}
|
|
else if (typeof attrValue != "object")
|
|
{
|
|
attrValue = mgr.expandName(attrValue);
|
|
}
|
|
_target[legacy[j]] = attrValue;
|
|
}
|
|
}
|
|
}
|
|
else if (attrName == "readonly" && typeof _target[attrName] != "undefined")
|
|
{
|
|
// do NOT overwrite already evaluated readonly attribute
|
|
}
|
|
else
|
|
{
|
|
let attrs = et2_attribute_registry[_proto.constructor.name] || {};
|
|
if (mgr != null && typeof attrs[attrName] != "undefined")
|
|
{
|
|
var attr = attrs[attrName];
|
|
|
|
// If the attribute is marked as boolean, parse the
|
|
// expression as bool expression.
|
|
if (attr.type == "boolean")
|
|
{
|
|
attrValue = mgr.parseBoolExpression(attrValue);
|
|
}
|
|
else
|
|
{
|
|
attrValue = mgr.expandName(attrValue);
|
|
}
|
|
}
|
|
|
|
// Set the attribute
|
|
_target[attrName] = attrValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply the "modifications" to the element and translate attributes marked
|
|
* with "translate: true"
|
|
*
|
|
* @param {object} _attrs
|
|
*/
|
|
transformAttributes(_attrs)
|
|
{
|
|
|
|
// Apply the content of the modifications array
|
|
if (this.id)
|
|
{
|
|
if (typeof this.id != "string")
|
|
{
|
|
console.log(this.id);
|
|
}
|
|
|
|
if (this.getArrayMgr("modifications"))
|
|
{
|
|
var data = this.getArrayMgr("modifications").getEntry(this.id);
|
|
|
|
// Check for already inside namespace
|
|
if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this)
|
|
{
|
|
data = this.getArrayMgr("modifications").data;
|
|
}
|
|
if (typeof data === 'object')
|
|
{
|
|
for (var key in data)
|
|
{
|
|
_attrs[key] = data[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Translate the attributes
|
|
for (var key in _attrs)
|
|
{
|
|
if (_attrs[key] && typeof this.attributes[key] != "undefined")
|
|
{
|
|
if (this.attributes[key].translate === true ||
|
|
(this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"]))
|
|
{
|
|
let value = _attrs[key];
|
|
// allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
|
|
if (value.indexOf('{') !== -1)
|
|
{
|
|
const egw = this.egw();
|
|
_attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1)
|
|
{
|
|
return egw.lang(p1);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
_attrs[key] = this.egw().lang(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a et2_widget from an XML node.
|
|
*
|
|
* First the type and attributes are read from the node. Then the readonly & modifications
|
|
* arrays are checked for changes specific to the loaded data. Then the appropriate
|
|
* constructor is called. After the constructor returns, the widget has a chance to
|
|
* further initialize itself from the XML node when the widget's loadFromXML() method
|
|
* is called with the node.
|
|
*
|
|
* @param _node XML node to read
|
|
* @param _name XML node name
|
|
*
|
|
* @return et2_widget
|
|
*/
|
|
createElementFromNode(_node, _name?)
|
|
{
|
|
var attributes = {};
|
|
|
|
// Parse the "readonly" and "type" flag for this element here, as they
|
|
// determine which constructor is used
|
|
var _nodeName = attributes["type"] = _node.getAttribute("type") ?
|
|
_node.getAttribute("type") : _node.nodeName.toLowerCase();
|
|
var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ?
|
|
(<any>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(typeof window.customElements.get(_node.nodeName.toLowerCase()) !== "undefined")
|
|
{
|
|
widget = loadWebComponent(_node.nodeName.toLowerCase(), _node, this);
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
}
|
|
return widget;
|
|
}
|
|
|
|
/**
|
|
* Loads the widget tree from an XML node
|
|
*
|
|
* @param _node xml node
|
|
*/
|
|
loadFromXML(_node)
|
|
{
|
|
// Load the child nodes.
|
|
for (var i = 0; i < _node.childNodes.length; i++)
|
|
{
|
|
var node = _node.childNodes[i];
|
|
var widgetType = node.nodeName.toLowerCase();
|
|
|
|
if (widgetType == "#comment")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (widgetType == "#text")
|
|
{
|
|
if (node.data.replace(/^\s+|\s+$/g, ''))
|
|
{
|
|
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:
|
|
* <code>
|
|
* var promises = [];
|
|
* widget.loadingFinished(promises);
|
|
* jQuery.when.apply(null, promises).done( doneCallback );
|
|
* </code>
|
|
* @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred}
|
|
*
|
|
* @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed.
|
|
*/
|
|
loadingFinished(promises?)
|
|
{
|
|
// Call all availble setters
|
|
this.initAttributes(this.options);
|
|
|
|
// Make sure promises is defined to avoid errors.
|
|
// We'll warn (below) if programmer should have passed it.
|
|
if (typeof promises == "undefined")
|
|
{
|
|
promises = [];
|
|
var warn_if_deferred = true;
|
|
}
|
|
|
|
var loadChildren = function ()
|
|
{
|
|
// Descend recursively into the tree
|
|
for (var i = 0; i < this._children.length; i++)
|
|
{
|
|
try
|
|
{
|
|
this._children[i].loadingFinished(promises);
|
|
}
|
|
catch (e)
|
|
{
|
|
egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack);
|
|
}
|
|
}
|
|
};
|
|
|
|
var result = this.doLoadingFinished();
|
|
if(typeof result == "boolean" && result)
|
|
{
|
|
// Simple widget finishes nicely
|
|
loadChildren.apply(this, arguments);
|
|
}
|
|
else if(typeof result == "object" && result.then)
|
|
{
|
|
// 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.then(loadChildren.bind(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')
|
|
{
|
|
val = et2_compileLegacyJS(val, this,
|
|
this.implements(et2_IInputNode) ? (<et2_inputWidget><unknown>this).getInputNode() :
|
|
(this.implements(et2_IDOMNode) ? (<et2_IDOMNode><unknown>this).getDOMNode() : null));
|
|
}
|
|
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 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promise}
|
|
*/
|
|
doLoadingFinished() : Promise<any> | 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 = (<et2_IDOMNode><unknown>this).getDOMNode();
|
|
if (node && node.ownerDocument)
|
|
{
|
|
wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView;
|
|
}
|
|
}
|
|
|
|
// If we're the root object, return the phpgwapi API instance
|
|
return egw('phpgwapi', wnd);
|
|
}
|
|
|
|
return this._egw;
|
|
}
|
|
|
|
/**
|
|
* Sets the client side api instance. It can be retrieved by the widget tree
|
|
* by using the "egw()" function.
|
|
*
|
|
* @param {IegwAppLocal} _egw egw object to set
|
|
*/
|
|
setApiInstance(_egw: IegwAppLocal)
|
|
{
|
|
this._egw = _egw;
|
|
}
|
|
|
|
protected _mgrs = {};
|
|
|
|
/**
|
|
* Sets all array manager objects - this function can be used to set the
|
|
* root array managers of the container object.
|
|
*
|
|
* @param {object} _mgrs
|
|
*/
|
|
setArrayMgrs(_mgrs)
|
|
{
|
|
this._mgrs = et2_cloneObject(_mgrs);
|
|
}
|
|
|
|
/**
|
|
* Returns an associative array containing the top-most array managers.
|
|
*
|
|
* @param _mgrs is used internally and should not be supplied.
|
|
*/
|
|
getArrayMgrs(_mgrs?: object)
|
|
{
|
|
if (typeof _mgrs == "undefined")
|
|
{
|
|
_mgrs = {};
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
this._mgrs[_part] = _mgr;
|
|
}
|
|
|
|
/**
|
|
* Returns the array manager object for the given part
|
|
*
|
|
* @param {string} managed_array_type name of array mgr to return
|
|
*/
|
|
getArrayMgr(managed_array_type: string): et2_arrayMgr | null
|
|
{
|
|
if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined")
|
|
{
|
|
return this._mgrs[managed_array_type];
|
|
}
|
|
else if (this._parent)
|
|
{
|
|
return this._parent.getArrayMgr(managed_array_type);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a namespace exists for this element in the content array.
|
|
* If yes, an own perspective of the content array is created. If not, the
|
|
* parent content manager is used.
|
|
*
|
|
* Constructor attributes are passed in case a child needs to make decisions
|
|
*/
|
|
checkCreateNamespace(_attrs?: any)
|
|
{
|
|
// Get the content manager
|
|
var mgrs = this.getArrayMgrs();
|
|
|
|
for (var key in mgrs)
|
|
{
|
|
var mgr = mgrs[key];
|
|
|
|
// Get the original content manager if we have already created a
|
|
// perspective for this node
|
|
if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this)
|
|
{
|
|
mgr = mgr.parentMgr;
|
|
}
|
|
|
|
// Check whether the manager has a namespace for the id of this object
|
|
var entry = mgr.getEntry(this.id);
|
|
if (typeof entry === 'object' && entry !== null || this.id)
|
|
{
|
|
// The content manager has an own node for this object, so
|
|
// create an own perspective.
|
|
this._mgrs[key] = mgr.openPerspective(this, this.id);
|
|
}
|
|
else
|
|
{
|
|
// The current content manager does not have an own namespace for
|
|
// this element, so use the content manager of the parent.
|
|
delete (this._mgrs[key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Widgets that do support a namespace should override and return true.
|
|
*
|
|
* Since a private attribute doesn't get instanciated properly before it's needed,
|
|
* we use a method so we can get what we need while still in the constructor.
|
|
*
|
|
* @private
|
|
*/
|
|
protected _createNamespace(): boolean
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This is used and therefore it we can not (yet) make it private
|
|
*
|
|
* @deprecated use this.getInstanceMgr()
|
|
*/
|
|
_inst = null;
|
|
|
|
/**
|
|
* Sets the instance manager object (of type etemplate2, see etemplate2.js)
|
|
*
|
|
* @param {etemplate2} _inst
|
|
*/
|
|
setInstanceManager(_inst)
|
|
{
|
|
this._inst = _inst;
|
|
}
|
|
|
|
/**
|
|
* Returns the instance manager
|
|
*
|
|
* @return {etemplate2}
|
|
*/
|
|
getInstanceManager()
|
|
{
|
|
if (this._inst != null)
|
|
{
|
|
return this._inst;
|
|
}
|
|
else if (this._parent)
|
|
{
|
|
return this._parent.getInstanceManager();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the path into the data array. By default, array manager takes care of
|
|
* this, but some extensions need to override this
|
|
*/
|
|
getPath()
|
|
{
|
|
var path = this.getArrayMgr("content").getPath();
|
|
|
|
// Prevent namespaced widgets with value from going an extra layer deep
|
|
if (this.id && this._createNamespace() && path[path.length - 1] == this.id) path.pop();
|
|
|
|
return path;
|
|
}
|
|
}
|