diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index c4720bbbd6..41cb7f5fec 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS DOM Widget class * @@ -6,16 +7,31 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_interfaces; - et2_core_widget; - /api/js/egw_action/egw_action.js; + et2_core_interfaces; + et2_core_widget; + /api/js/egw_action/egw_action.js; */ - +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +require("./et2_core_interfaces"); +require("./et2_core_common"); +var et2_core_widget_1 = require("./et2_core_widget"); +require("../egw_action/egw_action.js"); /** * Abstract widget class which can be inserted into the DOM. All widget classes * deriving from this class have to care about implementing the "getDOMNode" @@ -23,820 +39,660 @@ * * @augments et2_widget */ -var et2_DOMWidget = (function(){ "use strict"; return et2_widget.extend(et2_IDOMNode, -{ - attributes: { - "disabled": { - "name": "Disabled", - "type": "boolean", - "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", - "default": false - }, - "width": { - "name": "Width", - "type": "dimension", - "default": et2_no_init, - "description": "Width of the element in pixels, percentage or 'auto'" - }, - "height": { - "name": "Height", - "type": "dimension", - "default": et2_no_init, - "description": "Height of the element in pixels, percentage or 'auto'" - }, - "class": { - "name": "CSS Class", - "type": "string", - "default": et2_no_init, - "description": "CSS Class which is applied to the dom element of this node" - }, - "overflow": { - "name": "Overflow", - "type": "string", - "default": et2_no_init, - "description": "If set, the css-overflow attribute is set to that value" - }, - "parent_node": { - "name": "DOM parent", - "type": "string", - "default": et2_no_init, - "description": "Insert into the target DOM node instead of the normal location" - }, - "actions": { - "name": "Actions list", - "type": "any", - "default": et2_no_init, - "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" - }, - default_execute: { - name: "Default onExecute for actions", - type: "js", - default: et2_no_init, - description: "Set default onExecute javascript method for action not specifying their own" - }, - resize_ratio: { - name: "Resize height of the widget on callback resize", - type:"string", - default: '', - description: "Allow Resize height of the widget based on exess height and given ratio" - }, - data: { - name: "comma-separated name:value pairs set as data attributes on DOM node", - type: "string", - default: '', - description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' - }, - background: { - name: "Add background image", - type: "string", - default:'', - description: "Sets background image, left, right and scale on DOM", - } - }, - - /** - * When the DOMWidget is initialized, it grabs the DOM-Node of the parent - * object (if available) and passes it to its own "createDOMNode" function - * - * @memberOf et2_DOMWidget - */ - init: function() { - // Call the inherited constructor - this._super.apply(this, arguments); - - this.parentNode = null; - - this._attachSet = { - "node": null, - "parent": null - }; - - this.disabled = false; - this._surroundingsMgr = null; - }, - - /** - * Detatches the node from the DOM and clears all references to the parent - * node or the dom node of this widget. - */ - destroy: function() { - - this.detachFromDOM(); - this.parentNode = null; - this._attachSet = {}; - - if(this._actionManager) - { - var app_om = egw_getObjectManager(this.egw().getAppName(), false,1); - if(app_om) - { - var om = app_om.getObjectById(this.id); - if(om) om.remove(); - } - this._actionManager.remove(); - this._actionManager = null; - } - - if (this._surroundingsMgr) - { - this._surroundingsMgr.free(); - this._surroundingsMgr = null; - } - - this._super(); - }, - - /** - * Attaches the container node of this widget to the DOM-Tree - */ - doLoadingFinished: function() { - // Check whether the parent implements the et2_IDOMNode interface. If - // yes, grab the DOM node and create our own. - if (this._parent && this._parent.implements(et2_IDOMNode)) { - if(this.options.parent_node) - { - this.set_parent_node(this.options.parent_node); - } - else - { - this.setParentDOMNode(this._parent.getDOMNode(this)); - } - } - - return true; - }, - - /** - * Detaches the widget from the DOM tree, if it had been attached to the - * DOM-Tree using the attachToDOM method. - */ - detachFromDOM: function() { - - if (this._attachSet.node && this._attachSet.parent) - { - // Remove the current node from the parent node - try { - this._attachSet.parent.removeChild(this._attachSet.node); - } catch (e) { - // Don't throw a DOM error if the node wasn't in the parent - } - - // Reset the "attachSet" - this._attachSet = { - "node": null, - "parent": null - }; - - return true; - } - - return false; - }, - - /** - * Attaches the widget to the DOM tree. Fails if the widget is already - * attached to the tree or no parent node or no node for this widget is - * defined. - */ - attachToDOM: function() { - // Attach the DOM node of this widget (if existing) to the new parent - var node = this.getDOMNode(this); - if (node && this.parentNode && - (node != this._attachSet.node || - this.parentNode != this._attachSet.parent)) - { - // If the surroundings manager exists, surround the DOM-Node of this - // widget with the DOM-Nodes inside the surroundings manager. - if (this._surroundingsMgr) - { - node = this._surroundingsMgr.getDOMNode(node); - } - - // Append this node at its index - var idx = this.getDOMIndex(); - if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) - { - this.parentNode.appendChild(node); - } - else - { - this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); - } - - // Store the currently attached nodes - this._attachSet = { - "node": node, - "parent": this.parentNode - }; - - return true; - } - - return false; - }, - - isAttached: function() { - return this.parentNode != null; - }, - - getSurroundings: function() { - if (!this._surroundingsMgr) - { - this._surroundingsMgr = new et2_surroundingsMgr(this); - } - - return this._surroundingsMgr; - }, - - /** - * Get data for the tab this widget is on. - * - * Will return null if the widget is not on a tab or tab data containing - * - id - * - label - * - widget (top level widget) - * - contentDiv (jQuery object for the div the tab content is in) - * - * @returns {Object|null} Data for tab the widget is on - */ - get_tab_info: function() { - var parent = this; - do { - parent = parent._parent; - } while (parent !== this.getRoot() && parent._type !== 'tabbox'); - - // No tab - if(parent === this.getRoot()) - { - return null; - } - - // Find the tab index - for(var i = 0; i < parent.tabData.length; i++) - { - // Find the tab by DOM heritage - if(parent.tabData[i].contentDiv.has(this.div).length) - { - return parent.tabData[i]; - } - } - // On a tab, but we couldn't find it by DOM nodes Maybe tab template is - // not loaded yet. Try checking IDs. - var template = this; - do { - template = template._parent; - } while (template !== parent && template._type !== 'template'); - for(var i = parent.tabData.length - 1; i >= 0; i--) - { - if(template && template.id && template.id === parent.tabData[i].id) - { - return parent.tabData[i]; - } - } - // Fallback - return this._parent.get_tab_info(); - }, - - /** - * Set the parent DOM node of this element. Takes a wider variety of types - * than setParentDOMNode(), and matches the set_ naming convention. - * - * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. - */ - set_parent_node: function(_node) { - if(typeof _node == "string") - { - var parent = jQuery('#'+_node); - if(parent.length === 0 && window.parent) - { - // Could not find it, try again with wider context - // (in case there's an iframe in admin, for example) - parent = jQuery('#'+_node, window.parent.document); - } - if(parent.length === 0) - { - this.egw().debug('warn','Unable to find DOM parent node with ID "%s" for widget %o.',_node,this); - } - else - { - this.setParentDOMNode(parent.get(0)); - } - } - else - { - this.setParentDOMNode(_node); - } - }, - - /** - * Set the parent DOM node of this element. If another parent node is already - * set, this widget removes itself from the DOM tree - * - * @param _node - */ - setParentDOMNode: function(_node) { - if (_node != this.parentNode) - { - // Detatch this element from the DOM tree - this.detachFromDOM(); - - this.parentNode = _node; - - // And attatch the element to the DOM tree - this.attachToDOM(); - } - }, - - /** - * Returns the parent node. - */ - getParentDOMNode: function() { - return this.parentNode; - }, - - /** - * Returns the index of this element in the DOM tree - */ - getDOMIndex: function() { - if (this._parent) - { - var idx = 0; - var children = this._parent.getChildren(); - - if(children && children.indexOf) return children.indexOf(this); - - egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); - for (var i = 0; i < children.length; i++) - { - if (children[i] == this) - { - return idx; - } - else if (children[i].isInTree()) - { - idx++; - } - } - } - - return -1; - }, - - /** - * Sets the id of the DOM-Node. - * - * DOM id's have dots "." replaced with dashes "-" - * - * @param {string} _value id to set - */ - set_id: function(_value) { - - this.id = _value; - this.dom_id = _value ? this.getInstanceManager().uniqueId+'_'+_value.replace(/\./g, '-') : _value; - - var node = this.getDOMNode(this); - if (node) - { - if (_value != "") - { - node.setAttribute("id", this.dom_id); - } - else - { - node.removeAttribute("id"); - } - } - }, - - set_disabled: function(_value) { - var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); - if (node && this.disabled != _value) - { - this.disabled = _value; - - if (_value) - { - jQuery(node).hide(); - } - else - { - jQuery(node).show(); - } - } - }, - - set_width: function(_value) { - this.width = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("width", _value); - } - }, - - set_height: function(_value) { - this.height = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("height", _value); - } - }, - - set_class: function(_value) { - var node = this.getDOMNode(this); - if (node) - { - if (this["class"]) - { - jQuery(node).removeClass(this["class"]); - } - jQuery(node).addClass(_value); - } - - this["class"] = _value; - }, - - set_overflow: function(_value) { - this.overflow = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("overflow", _value); - } - }, - - set_data: function(_value) - { - var node = this.getDOMNode(this); - if (node && _value) - { - var pairs = _value.split(/,/g); - for(var i=0; i < pairs.length; ++i) - { - var name_value = pairs[i].split(':'); - jQuery(node).attr('data-'+name_value[0], name_value[1]); - } - } - }, - - set_background: function(_value) - { - var node = this.getDOMNode(this); - var values = ''; - if (_value && node) - { - values = _value.split(','); - jQuery(node).css({ - "background-image":'url("'+values[0]+'")', - "background-position-x":values[1], - "background-position-y":values[2], - "background-scale":values[3] - }); - } - }, - - /** - * Set Actions on the widget - * - * Each action is defined as an object: - * - * move: { - * type: "drop", - * acceptedTypes: "mail", - * icon: "move", - * caption: "Move to" - * onExecute: javascript:mail_move" - * } - * - * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, - * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the - * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The - * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler - * can operate in the widget context easily. The location varies depending on your action though. It - * might be action.parent.parent.data.widget - * - * To customise how the actions are handled for a particular widget, override _link_actions(). It handles - * the more widget-specific parts. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method - */ - set_actions: function(actions) - { - if(this.id == "" || typeof this.id == "undefined") - { - this.egw().debug("warn", "Widget should have an ID if you want actions",this); - return; - } - - // Initialize the action manager and add some actions to it - // Only look 1 level deep - var gam = egw_getActionManager(this.egw().appName,true,1); - if(typeof this._actionManager != "object") - { - if(gam.getActionById(this.getInstanceManager().uniqueId,1) !== null) - { - gam = gam.getActionById(this.getInstanceManager().uniqueId,1); - } - if(gam.getActionById(this.id,1) != null) - { - this._actionManager = gam.getActionById(this.id,1); - } - else - { - this._actionManager = gam.addAction("actionManager", this.id); - } - } - this._actionManager.updateActions(actions, this.egw().appName); - if (this.options.default_execute) this._actionManager.setDefaultExecute(this.options.default_execute); - - // Put a reference to the widget into the action stuff, so we can - // easily get back to widget context from the action handler - this._actionManager.data = {widget: this}; - - // Link the actions to the DOM - this._link_actions(actions); - }, - - set_default_execute: function(_default_execute) - { - this.options.default_execute = _default_execute; - - if (this._actionManager) this._actionManager.setDefaultExecute(null, _default_execute); - }, - - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * This can be overwritten to not allow all actions, by not returning them here. - * - * @param actions - * @returns {Array} - */ - _get_action_links: function(actions) - { - var action_links = []; - for(var i in actions) - { - var action = actions[i]; - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - return action_links; - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - // Get the top level element for the tree - var objectManager = egw_getAppObjectManager(true); - var widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = objectManager.insertObject(false, new egwActionObject( - this.id, objectManager, new et2_action_object_impl(this), - this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager - )); - } - else - { - widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); - } - - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - widget_object.updateActionLinks(action_links); - } - -});}).call(this); - +var et2_DOMWidget = /** @class */ (function (_super_1) { + __extends(et2_DOMWidget, _super_1); + /** + * When the DOMWidget is initialized, it grabs the DOM-Node of the parent + * object (if available) and passes it to its own "createDOMNode" function + * + * @memberOf et2_DOMWidget + */ + function et2_DOMWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})) || this; + _this.parentNode = null; + _this._attachSet = { + "node": null, + "parent": null + }; + _this.disabled = false; + _this._surroundingsMgr = null; + return _this; + } + /** + * Detatches the node from the DOM and clears all references to the parent + * node or the dom node of this widget. + */ + et2_DOMWidget.prototype.destroy = function () { + this.detachFromDOM(); + this.parentNode = null; + this._attachSet = {}; + if (this._actionManager) { + var app_om = egw_getObjectManager(this.egw().getAppName(), false, 1); + if (app_om) { + var om = app_om.getObjectById(this.id); + if (om) + om.remove(); + } + this._actionManager.remove(); + this._actionManager = null; + } + if (this._surroundingsMgr) { + this._surroundingsMgr.free(); + this._surroundingsMgr = null; + } + this._super(); + }; + /** + * Attaches the container node of this widget to the DOM-Tree + */ + et2_DOMWidget.prototype.doLoadingFinished = function () { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { + if (this.options.parent_node) { + this.set_parent_node(this.options.parent_node); + } + else { + this.setParentDOMNode(this.getParent().getDOMNode(this)); + } + } + return true; + }; + /** + * Detaches the widget from the DOM tree, if it had been attached to the + * DOM-Tree using the attachToDOM method. + */ + et2_DOMWidget.prototype.detachFromDOM = function () { + if (this._attachSet.node && this._attachSet.parent) { + // Remove the current node from the parent node + try { + this._attachSet.parent.removeChild(this._attachSet.node); + } + catch (e) { + // Don't throw a DOM error if the node wasn't in the parent + } + // Reset the "attachSet" + this._attachSet = { + "node": null, + "parent": null + }; + return true; + } + return false; + }; + /** + * Attaches the widget to the DOM tree. Fails if the widget is already + * attached to the tree or no parent node or no node for this widget is + * defined. + */ + et2_DOMWidget.prototype.attachToDOM = function () { + // Attach the DOM node of this widget (if existing) to the new parent + var node = this.getDOMNode(this); + if (node && this.parentNode && + (node != this._attachSet.node || + this.parentNode != this._attachSet.parent)) { + // If the surroundings manager exists, surround the DOM-Node of this + // widget with the DOM-Nodes inside the surroundings manager. + if (this._surroundingsMgr) { + node = this._surroundingsMgr.getDOMNode(node); + } + // Append this node at its index + var idx = this.getDOMIndex(); + if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) { + this.parentNode.appendChild(node); + } + else { + this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); + } + // Store the currently attached nodes + this._attachSet = { + "node": node, + "parent": this.parentNode + }; + return true; + } + return false; + }; + et2_DOMWidget.prototype.isAttached = function () { + return this.parentNode != null; + }; + et2_DOMWidget.prototype.getSurroundings = function () { + if (!this._surroundingsMgr) { + this._surroundingsMgr = new et2_surroundingsMgr(this); + } + return this._surroundingsMgr; + }; + /** + * Get data for the tab this widget is on. + * + * Will return null if the widget is not on a tab or tab data containing + * - id + * - label + * - widget (top level widget) + * - contentDiv (jQuery object for the div the tab content is in) + * + * @returns {Object|null} Data for tab the widget is on + */ + et2_DOMWidget.prototype.get_tab_info = function () { + var parent = this; + do { + parent = parent._parent; + } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + // No tab + if (parent === this.getRoot()) { + return null; + } + // Find the tab index + for (var i = 0; i < parent.tabData.length; i++) { + // Find the tab by DOM heritage + if (parent.tabData[i].contentDiv.has(this.div).length) { + return parent.tabData[i]; + } + } + // On a tab, but we couldn't find it by DOM nodes Maybe tab template is + // not loaded yet. Try checking IDs. + var template = this; + do { + template = template._parent; + } while (template !== parent && template._type !== 'template'); + for (var i = parent.tabData.length - 1; i >= 0; i--) { + if (template && template.id && template.id === parent.tabData[i].id) { + return parent.tabData[i]; + } + } + // Fallback + return this.getParent().get_tab_info(); + }; + /** + * Set the parent DOM node of this element. Takes a wider variety of types + * than setParentDOMNode(), and matches the set_ naming convention. + * + * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. + */ + et2_DOMWidget.prototype.set_parent_node = function (_node) { + if (typeof _node == "string") { + var parent = jQuery('#' + _node); + if (parent.length === 0 && window.parent) { + // Could not find it, try again with wider context + // (in case there's an iframe in admin, for example) + parent = jQuery('#' + _node, window.parent.document); + } + if (parent.length === 0) { + this.egw().debug('warn', 'Unable to find DOM parent node with ID "%s" for widget %o.', _node, this); + } + else { + this.setParentDOMNode(parent.get(0)); + } + } + else { + this.setParentDOMNode(_node); + } + }; + /** + * Set the parent DOM node of this element. If another parent node is already + * set, this widget removes itself from the DOM tree + * + * @param _node + */ + et2_DOMWidget.prototype.setParentDOMNode = function (_node) { + if (_node != this.parentNode) { + // Detatch this element from the DOM tree + this.detachFromDOM(); + this.parentNode = _node; + // And attatch the element to the DOM tree + this.attachToDOM(); + } + }; + /** + * Returns the parent node. + */ + et2_DOMWidget.prototype.getParentDOMNode = function () { + return this.parentNode; + }; + /** + * Returns the index of this element in the DOM tree + */ + et2_DOMWidget.prototype.getDOMIndex = function () { + if (this.getParent()) { + var idx = 0; + var children = this.getParent().getChildren(); + if (children && children.indexOf) + return children.indexOf(this); + egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); + for (var i = 0; i < children.length; i++) { + if (children[i] == this) { + return idx; + } + else if (children[i].isInTree()) { + idx++; + } + } + } + return -1; + }; + /** + * Sets the id of the DOM-Node. + * + * DOM id's have dots "." replaced with dashes "-" + * + * @param {string} _value id to set + */ + et2_DOMWidget.prototype.set_id = function (_value) { + this.id = _value; + this.dom_id = _value ? this.getInstanceManager().uniqueId + '_' + _value.replace(/\./g, '-') : _value; + var node = this.getDOMNode(this); + if (node) { + if (_value != "") { + node.setAttribute("id", this.dom_id); + } + else { + node.removeAttribute("id"); + } + } + }; + et2_DOMWidget.prototype.set_disabled = function (_value) { + var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); + if (node && this.disabled != _value) { + this.disabled = _value; + if (_value) { + jQuery(node).hide(); + } + else { + jQuery(node).show(); + } + } + }; + et2_DOMWidget.prototype.set_width = function (_value) { + this.width = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("width", _value); + } + }; + et2_DOMWidget.prototype.set_height = function (_value) { + this.height = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("height", _value); + } + }; + et2_DOMWidget.prototype.set_class = function (_value) { + var node = this.getDOMNode(this); + if (node) { + if (this["class"]) { + jQuery(node).removeClass(this["class"]); + } + jQuery(node).addClass(_value); + } + this["class"] = _value; + }; + et2_DOMWidget.prototype.set_overflow = function (_value) { + this.overflow = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("overflow", _value); + } + }; + et2_DOMWidget.prototype.set_data = function (_value) { + var node = this.getDOMNode(this); + if (node && _value) { + var pairs = _value.split(/,/g); + for (var i = 0; i < pairs.length; ++i) { + var name_value = pairs[i].split(':'); + jQuery(node).attr('data-' + name_value[0], name_value[1]); + } + } + }; + et2_DOMWidget.prototype.set_background = function (_value) { + var node = this.getDOMNode(this); + var values = ''; + if (_value && node) { + values = _value.split(','); + jQuery(node).css({ + "background-image": 'url("' + values[0] + '")', + "background-position-x": values[1], + "background-position-y": values[2], + "background-scale": values[3] + }); + } + }; + /** + * Set Actions on the widget + * + * Each action is defined as an object: + * + * move: { + * type: "drop", + * acceptedTypes: "mail", + * icon: "move", + * caption: "Move to" + * onExecute: javascript:mail_move" + * } + * + * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, + * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the + * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The + * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler + * can operate in the widget context easily. The location varies depending on your action though. It + * might be action.parent.parent.data.widget + * + * To customise how the actions are handled for a particular widget, override _link_actions(). It handles + * the more widget-specific parts. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method + */ + et2_DOMWidget.prototype.set_actions = function (actions) { + if (this.id == "" || typeof this.id == "undefined") { + this.egw().debug("warn", "Widget should have an ID if you want actions", this); + return; + } + // Initialize the action manager and add some actions to it + // Only look 1 level deep + var gam = egw_getActionManager(this.egw().appName, true, 1); + if (typeof this._actionManager != "object") { + if (gam.getActionById(this.getInstanceManager().uniqueId, 1) !== null) { + gam = gam.getActionById(this.getInstanceManager().uniqueId, 1); + } + if (gam.getActionById(this.id, 1) != null) { + this._actionManager = gam.getActionById(this.id, 1); + } + else { + this._actionManager = gam.addAction("actionManager", this.id); + } + } + this._actionManager.updateActions(actions, this.egw().appName); + if (this.options.default_execute) + this._actionManager.setDefaultExecute(this.options.default_execute); + // Put a reference to the widget into the action stuff, so we can + // easily get back to widget context from the action handler + this._actionManager.data = { widget: this }; + // Link the actions to the DOM + this._link_actions(actions); + }; + et2_DOMWidget.prototype.set_default_execute = function (_default_execute) { + this.options.default_execute = _default_execute; + if (this._actionManager) + this._actionManager.setDefaultExecute(null, _default_execute); + }; + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * This can be overwritten to not allow all actions, by not returning them here. + * + * @param actions + * @returns {Array} + */ + et2_DOMWidget.prototype._get_action_links = function (actions) { + var action_links = []; + for (var i in actions) { + var action = actions[i]; + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + return action_links; + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_DOMWidget.prototype._link_actions = function (actions) { + // Get the top level element for the tree + var objectManager = egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById(this.id); + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, new et2_action_object_impl(this), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + } + else { + widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + } + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + widget_object.updateActionLinks(action_links); + }; + et2_DOMWidget._attributes = { + "disabled": { + "name": "Disabled", + "type": "boolean", + "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", + "default": false + }, + "width": { + "name": "Width", + "type": "dimension", + "default": et2_no_init, + "description": "Width of the element in pixels, percentage or 'auto'" + }, + "height": { + "name": "Height", + "type": "dimension", + "default": et2_no_init, + "description": "Height of the element in pixels, percentage or 'auto'" + }, + "class": { + "name": "CSS Class", + "type": "string", + "default": et2_no_init, + "description": "CSS Class which is applied to the dom element of this node" + }, + "overflow": { + "name": "Overflow", + "type": "string", + "default": et2_no_init, + "description": "If set, the css-overflow attribute is set to that value" + }, + "parent_node": { + "name": "DOM parent", + "type": "string", + "default": et2_no_init, + "description": "Insert into the target DOM node instead of the normal location" + }, + "actions": { + "name": "Actions list", + "type": "any", + "default": et2_no_init, + "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" + }, + default_execute: { + name: "Default onExecute for actions", + type: "js", + default: et2_no_init, + description: "Set default onExecute javascript method for action not specifying their own" + }, + resize_ratio: { + name: "Resize height of the widget on callback resize", + type: "string", + default: '', + description: "Allow Resize height of the widget based on exess height and given ratio" + }, + data: { + name: "comma-separated name:value pairs set as data attributes on DOM node", + type: "string", + default: '', + description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' + }, + background: { + name: "Add background image", + type: "string", + default: '', + description: "Sets background image, left, right and scale on DOM", + } + }; + return et2_DOMWidget; +}(et2_core_widget_1.et2_widget)); /** * The surroundings manager class allows to append or prepend elements around * an widget node. * * @augments Class */ -var et2_surroundingsMgr = (function(){ "use strict"; return ClassWithAttributes.extend( -{ - /** - * Constructor - * - * @memberOf et2_surroundingsMgr - * @param _widget - */ - init: function(_widget) { - this.widget = _widget; - - this._widgetContainer = null; - this._widgetSurroundings = []; - this._widgetPlaceholder = null; - this._widgetNode = null; - this._ownPlaceholder = true; - }, - - destroy: function() { - this._widgetContainer = null; - this._widgetSurroundings = null; - this._widgetPlaceholder = null; - this._widgetNode = null; - }, - - prependDOMNode: function(_node) { - this._widgetSurroundings.unshift(_node); - this._surroundingsUpdated = true; - }, - - appendDOMNode: function(_node) { - // Append an placeholder first if none is existing yet - if (this._ownPlaceholder && this._widgetPlaceholder == null) - { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - } - - // Append the given node - this._widgetSurroundings.push(_node); - this._surroundingsUpdated = true; - }, - - insertDOMNode: function(_node) { - if (!this._ownPlaceholder || this._widgetPlaceholder == null) - { - this.appendDOMNode(_node); - return; - } - - // Get the index of the widget placeholder and delete it, insert the - // given node instead - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1, _node); - - // Delete the reference to the own placeholder - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - }, - - removeDOMNode: function(_node) { - for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) - { - if (this._widgetSurroundings[i] == _node) - { - this._widgetSurroundings.splice(i, 1); - this._surroundingsUpdated = true; - break; - } - } - }, - - setWidgetPlaceholder: function(_node) { - if (_node != this._widgetPlaceholder) - { - if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) - { - // Delete the current placeholder which was created by the - // widget itself - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1); - - // Delete any reference to the own placeholder and set the - // _ownPlaceholder flag to false - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - } - - this._ownPlaceholder = (_node == null); - this._widgetPlaceholder = _node; - this._surroundingsUpdated = true; - } - }, - - _rebuildContainer: function() { - // Return if there has been no change in the "surroundings-data" - if (!this._surroundingsUpdated) - { - return false; - } - - // Build the widget container - if (this._widgetSurroundings.length > 0) - { - // Check whether the widgetPlaceholder is really inside the DOM-Tree - var hasPlaceholder = et2_hasChild(this._widgetSurroundings, - this._widgetPlaceholder); - - // If not, append another widget placeholder - if (!hasPlaceholder) - { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - - this._ownPlaceholder = true; - } - - // If the surroundings array only contains one element, set this one - // as the widget container - if (this._widgetSurroundings.length == 1) - { - if (this._widgetSurroundings[0] == this._widgetPlaceholder) - { - this._widgetContainer = null; - } - else - { - this._widgetContainer = this._widgetSurroundings[0]; - } - } - else - { - // Create an outer "span" as widgetContainer - this._widgetContainer = document.createElement("span"); - - // Append the children inside the widgetSurroundings array to - // the widget container - for (var i = 0; i < this._widgetSurroundings.length; i++) - { - this._widgetContainer.appendChild(this._widgetSurroundings[i]); - } - } - } - else - { - this._widgetContainer = null; - this._widgetPlaceholder = null; - } - - this._surroundingsUpdated = false; - - return true; - }, - - update: function() { - if (this._surroundingsUpdated) - { - var attached = this.widget ? this.widget.isAttached() : false; - - // Reattach the widget - this will call the "getDOMNode" function - // and trigger the _rebuildContainer function. - if (attached && this.widget) - { - this.widget.detachFromDOM(); - this.widget.attachToDOM(); - } - } - }, - - getDOMNode: function(_widgetNode) { - // Update the whole widgetContainer if this is not the first time this - // function has been called but the widget node has changed. - if (this._widgetNode != null && this._widgetNode != _widgetNode) - { - this._surroundingsUpdated = true; - } - - // Copy a reference to the given node - this._widgetNode = _widgetNode; - - // Build the container if it didn't exist yet. - var updated = this._rebuildContainer(); - - // Return the widget node itself if there are no surroundings arround - // it - if (this._widgetContainer == null) - { - return _widgetNode; - } - - // Replace the widgetPlaceholder with the given widget node if the - // widgetContainer has been updated - if (updated) - { - this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, - this._widgetPlaceholder); - if (!this._ownPlaceholder) - { - this._widgetPlaceholder = _widgetNode; - } - } - - // Return the widget container - return this._widgetContainer; - } - -});}).call(this); - +var et2_surroundingsMgr = /** @class */ (function (_super_1) { + __extends(et2_surroundingsMgr, _super_1); + function et2_surroundingsMgr() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + et2_surroundingsMgr.prototype.init = function (_widget) { + this.widget = _widget; + this._widgetContainer = null; + this._widgetSurroundings = []; + this._widgetPlaceholder = null; + this._widgetNode = null; + this._ownPlaceholder = true; + }; + et2_surroundingsMgr.prototype.destroy = function () { + this._widgetContainer = null; + this._widgetSurroundings = null; + this._widgetPlaceholder = null; + this._widgetNode = null; + }; + et2_surroundingsMgr.prototype.prependDOMNode = function (_node) { + this._widgetSurroundings.unshift(_node); + this._surroundingsUpdated = true; + }; + et2_surroundingsMgr.prototype.appendDOMNode = function (_node) { + // Append an placeholder first if none is existing yet + if (this._ownPlaceholder && this._widgetPlaceholder == null) { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + } + // Append the given node + this._widgetSurroundings.push(_node); + this._surroundingsUpdated = true; + }; + et2_surroundingsMgr.prototype.insertDOMNode = function (_node) { + if (!this._ownPlaceholder || this._widgetPlaceholder == null) { + this.appendDOMNode(_node); + return; + } + // Get the index of the widget placeholder and delete it, insert the + // given node instead + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1, _node); + // Delete the reference to the own placeholder + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + }; + et2_surroundingsMgr.prototype.removeDOMNode = function (_node) { + for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) { + if (this._widgetSurroundings[i] == _node) { + this._widgetSurroundings.splice(i, 1); + this._surroundingsUpdated = true; + break; + } + } + }; + et2_surroundingsMgr.prototype.setWidgetPlaceholder = function (_node) { + if (_node != this._widgetPlaceholder) { + if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) { + // Delete the current placeholder which was created by the + // widget itself + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1); + // Delete any reference to the own placeholder and set the + // _ownPlaceholder flag to false + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + this._ownPlaceholder = (_node == null); + this._widgetPlaceholder = _node; + this._surroundingsUpdated = true; + } + }; + et2_surroundingsMgr.prototype._rebuildContainer = function () { + // Return if there has been no change in the "surroundings-data" + if (!this._surroundingsUpdated) { + return false; + } + // Build the widget container + if (this._widgetSurroundings.length > 0) { + // Check whether the widgetPlaceholder is really inside the DOM-Tree + var hasPlaceholder = et2_hasChild(this._widgetSurroundings, this._widgetPlaceholder); + // If not, append another widget placeholder + if (!hasPlaceholder) { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + this._ownPlaceholder = true; + } + // If the surroundings array only contains one element, set this one + // as the widget container + if (this._widgetSurroundings.length == 1) { + if (this._widgetSurroundings[0] == this._widgetPlaceholder) { + this._widgetContainer = null; + } + else { + this._widgetContainer = this._widgetSurroundings[0]; + } + } + else { + // Create an outer "span" as widgetContainer + this._widgetContainer = document.createElement("span"); + // Append the children inside the widgetSurroundings array to + // the widget container + for (var i = 0; i < this._widgetSurroundings.length; i++) { + this._widgetContainer.appendChild(this._widgetSurroundings[i]); + } + } + } + else { + this._widgetContainer = null; + this._widgetPlaceholder = null; + } + this._surroundingsUpdated = false; + return true; + }; + et2_surroundingsMgr.prototype.update = function () { + if (this._surroundingsUpdated) { + var attached = this.widget ? this.widget.isAttached() : false; + // Reattach the widget - this will call the "getDOMNode" function + // and trigger the _rebuildContainer function. + if (attached && this.widget) { + this.widget.detachFromDOM(); + this.widget.attachToDOM(); + } + } + }; + et2_surroundingsMgr.prototype.getDOMNode = function (_widgetNode) { + // Update the whole widgetContainer if this is not the first time this + // function has been called but the widget node has changed. + if (this._widgetNode != null && this._widgetNode != _widgetNode) { + this._surroundingsUpdated = true; + } + // Copy a reference to the given node + this._widgetNode = _widgetNode; + // Build the container if it didn't exist yet. + var updated = this._rebuildContainer(); + // Return the widget node itself if there are no surroundings arround + // it + if (this._widgetContainer == null) { + return _widgetNode; + } + // Replace the widgetPlaceholder with the given widget node if the + // widgetContainer has been updated + if (updated) { + this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, this._widgetPlaceholder); + if (!this._ownPlaceholder) { + this._widgetPlaceholder = _widgetNode; + } + } + // Return the widget container + return this._widgetContainer; + }; + return et2_surroundingsMgr; +}(et2_core_inheritance_1.ClassWithAttributes)); /** * The egw_action system requires an egwActionObjectInterface Interface implementation * to tie actions to DOM nodes. This one can be used by any widget. @@ -847,40 +703,33 @@ var et2_surroundingsMgr = (function(){ "use strict"; return ClassWithAttributes. * @param {Object} node * */ -function et2_action_object_impl(widget, node) -{ - var aoi = new egwActionObjectInterface(); - var objectNode = node; - - aoi.getWidget = function() { - return widget; - }; - - aoi.doGetDOMNode = function() { - return objectNode?objectNode:widget.getDOMNode(); - }; - -// _outerCall may be used to determine, whether the state change has been -// evoked from the outside and the stateChangeCallback has to be called -// or not. - aoi.doSetState = function(_state, _outerCall) { - }; - -// The doTiggerEvent function may be overritten by the aoi if it wants to -// support certain action implementation specific events like EGW_AI_DRAG_OVER -// or EGW_AI_DRAG_OUT - aoi.doTriggerEvent = function(_event, _data) { - switch(_event) - { - case EGW_AI_DRAG_OVER: - jQuery(this.node).addClass("ui-state-active"); - break; - case EGW_AI_DRAG_OUT: - jQuery(this.node).removeClass("ui-state-active"); - break; - } - }; - - - return aoi; -}; +function et2_action_object_impl(widget, node) { + var aoi = new egwActionObjectInterface(); + var objectNode = node; + aoi.getWidget = function () { + return widget; + }; + aoi.doGetDOMNode = function () { + return objectNode ? objectNode : widget.getDOMNode(); + }; + // _outerCall may be used to determine, whether the state change has been + // evoked from the outside and the stateChangeCallback has to be called + // or not. + aoi.doSetState = function (_state, _outerCall) { + }; + // The doTiggerEvent function may be overritten by the aoi if it wants to + // support certain action implementation specific events like EGW_AI_DRAG_OVER + // or EGW_AI_DRAG_OUT + aoi.doTriggerEvent = function (_event, _data) { + switch (_event) { + case EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + return aoi; +} +; diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts new file mode 100644 index 0000000000..2197a2feff --- /dev/null +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -0,0 +1,899 @@ +/** + * EGroupware eTemplate2 - JS DOM Widget class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + et2_core_interfaces; + et2_core_widget; + /api/js/egw_action/egw_action.js; +*/ + +import { ClassWithAttributes } from './et2_core_inheritance'; +import './et2_core_interfaces'; +import './et2_core_common'; +import {et2_widget, et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import '../egw_action/egw_action.js'; + +/** + * Abstract widget class which can be inserted into the DOM. All widget classes + * deriving from this class have to care about implementing the "getDOMNode" + * function which has to return the DOM-Node. + * + * @augments et2_widget + */ +class et2_DOMWidget extends et2_widget implements et2_IDOMNode +{ + static readonly _attributes : any = { + "disabled": { + "name": "Disabled", + "type": "boolean", + "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", + "default": false + }, + "width": { + "name": "Width", + "type": "dimension", + "default": et2_no_init, + "description": "Width of the element in pixels, percentage or 'auto'" + }, + "height": { + "name": "Height", + "type": "dimension", + "default": et2_no_init, + "description": "Height of the element in pixels, percentage or 'auto'" + }, + "class": { + "name": "CSS Class", + "type": "string", + "default": et2_no_init, + "description": "CSS Class which is applied to the dom element of this node" + }, + "overflow": { + "name": "Overflow", + "type": "string", + "default": et2_no_init, + "description": "If set, the css-overflow attribute is set to that value" + }, + "parent_node": { + "name": "DOM parent", + "type": "string", + "default": et2_no_init, + "description": "Insert into the target DOM node instead of the normal location" + }, + "actions": { + "name": "Actions list", + "type": "any", + "default": et2_no_init, + "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" + }, + default_execute: { + name: "Default onExecute for actions", + type: "js", + default: et2_no_init, + description: "Set default onExecute javascript method for action not specifying their own" + }, + resize_ratio: { + name: "Resize height of the widget on callback resize", + type:"string", + default: '', + description: "Allow Resize height of the widget based on exess height and given ratio" + }, + data: { + name: "comma-separated name:value pairs set as data attributes on DOM node", + type: "string", + default: '', + description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' + }, + background: { + name: "Add background image", + type: "string", + default:'', + description: "Sets background image, left, right and scale on DOM", + } + } + + parentNode : HTMLElement; + disabled : boolean; + private _attachSet: object; + private _actionManager: any; + + /** + * When the DOMWidget is initialized, it grabs the DOM-Node of the parent + * object (if available) and passes it to its own "createDOMNode" function + * + * @memberOf et2_DOMWidget + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + this.parentNode = null; + + this._attachSet = { + "node": null, + "parent": null + }; + + this.disabled = false; + this._surroundingsMgr = null; + } + + /** + * Detatches the node from the DOM and clears all references to the parent + * node or the dom node of this widget. + */ + destroy() + { + this.detachFromDOM(); + this.parentNode = null; + this._attachSet = {}; + + if(this._actionManager) + { + var app_om = egw_getObjectManager(this.egw().getAppName(), false,1); + if(app_om) + { + var om = app_om.getObjectById(this.id); + if(om) om.remove(); + } + this._actionManager.remove(); + this._actionManager = null; + } + + if (this._surroundingsMgr) + { + this._surroundingsMgr.free(); + this._surroundingsMgr = null; + } + + this._super(); + } + + /** + * Attaches the container node of this widget to the DOM-Tree + */ + doLoadingFinished() + { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { + if(this.options.parent_node) + { + this.set_parent_node(this.options.parent_node); + } + else + { + this.setParentDOMNode(this.getParent().getDOMNode(this)); + } + } + + return true; + } + + /** + * Detaches the widget from the DOM tree, if it had been attached to the + * DOM-Tree using the attachToDOM method. + */ + detachFromDOM() { + + if (this._attachSet.node && this._attachSet.parent) + { + // Remove the current node from the parent node + try { + this._attachSet.parent.removeChild(this._attachSet.node); + } catch (e) { + // Don't throw a DOM error if the node wasn't in the parent + } + + // Reset the "attachSet" + this._attachSet = { + "node": null, + "parent": null + }; + + return true; + } + + return false; + } + + /** + * Attaches the widget to the DOM tree. Fails if the widget is already + * attached to the tree or no parent node or no node for this widget is + * defined. + */ + attachToDOM() { + // Attach the DOM node of this widget (if existing) to the new parent + var node = this.getDOMNode(this); + if (node && this.parentNode && + (node != this._attachSet.node || + this.parentNode != this._attachSet.parent)) + { + // If the surroundings manager exists, surround the DOM-Node of this + // widget with the DOM-Nodes inside the surroundings manager. + if (this._surroundingsMgr) + { + node = this._surroundingsMgr.getDOMNode(node); + } + + // Append this node at its index + var idx = this.getDOMIndex(); + if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) + { + this.parentNode.appendChild(node); + } + else + { + this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); + } + + // Store the currently attached nodes + this._attachSet = { + "node": node, + "parent": this.parentNode + }; + + return true; + } + + return false; + } + + isAttached() { + return this.parentNode != null; + } + + private _surroundingsMgr : et2_surroundingsMgr; + + getSurroundings() { + if (!this._surroundingsMgr) + { + this._surroundingsMgr = new et2_surroundingsMgr(this); + } + + return this._surroundingsMgr; + } + + /** + * Get data for the tab this widget is on. + * + * Will return null if the widget is not on a tab or tab data containing + * - id + * - label + * - widget (top level widget) + * - contentDiv (jQuery object for the div the tab content is in) + * + * @returns {Object|null} Data for tab the widget is on + */ + get_tab_info() { + var parent = this; + do { + parent = parent._parent; + } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + + // No tab + if(parent === this.getRoot()) + { + return null; + } + + // Find the tab index + for(var i = 0; i < parent.tabData.length; i++) + { + // Find the tab by DOM heritage + if(parent.tabData[i].contentDiv.has(this.div).length) + { + return parent.tabData[i]; + } + } + // On a tab, but we couldn't find it by DOM nodes Maybe tab template is + // not loaded yet. Try checking IDs. + var template = this; + do { + template = template._parent; + } while (template !== parent && template._type !== 'template'); + for(var i = parent.tabData.length - 1; i >= 0; i--) + { + if(template && template.id && template.id === parent.tabData[i].id) + { + return parent.tabData[i]; + } + } + // Fallback + return this.getParent().get_tab_info(); + } + + /** + * Set the parent DOM node of this element. Takes a wider variety of types + * than setParentDOMNode(), and matches the set_ naming convention. + * + * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. + */ + set_parent_node(_node) { + if(typeof _node == "string") + { + var parent = jQuery('#'+_node); + if(parent.length === 0 && window.parent) + { + // Could not find it, try again with wider context + // (in case there's an iframe in admin, for example) + parent = jQuery('#'+_node, window.parent.document); + } + if(parent.length === 0) + { + this.egw().debug('warn','Unable to find DOM parent node with ID "%s" for widget %o.',_node,this); + } + else + { + this.setParentDOMNode(parent.get(0)); + } + } + else + { + this.setParentDOMNode(_node); + } + } + + /** + * Set the parent DOM node of this element. If another parent node is already + * set, this widget removes itself from the DOM tree + * + * @param _node + */ + setParentDOMNode(_node) { + if (_node != this.parentNode) + { + // Detatch this element from the DOM tree + this.detachFromDOM(); + + this.parentNode = _node; + + // And attatch the element to the DOM tree + this.attachToDOM(); + } + } + + /** + * Returns the parent node. + */ + getParentDOMNode() { + return this.parentNode; + } + + /** + * Returns the index of this element in the DOM tree + */ + getDOMIndex() { + if (this.getParent()) + { + var idx = 0; + var children = this.getParent().getChildren(); + + if(children && children.indexOf) return children.indexOf(this); + + egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); + for (var i = 0; i < children.length; i++) + { + if (children[i] == this) + { + return idx; + } + else if (children[i].isInTree()) + { + idx++; + } + } + } + + return -1; + } + + /** + * Sets the id of the DOM-Node. + * + * DOM id's have dots "." replaced with dashes "-" + * + * @param {string} _value id to set + */ + set_id(_value) { + + this.id = _value; + this.dom_id = _value ? this.getInstanceManager().uniqueId+'_'+_value.replace(/\./g, '-') : _value; + + var node = this.getDOMNode(this); + if (node) + { + if (_value != "") + { + node.setAttribute("id", this.dom_id); + } + else + { + node.removeAttribute("id"); + } + } + } + + set_disabled(_value) { + var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); + if (node && this.disabled != _value) + { + this.disabled = _value; + + if (_value) + { + jQuery(node).hide(); + } + else + { + jQuery(node).show(); + } + } + } + + set_width(_value) { + this.width = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("width", _value); + } + } + + set_height(_value) { + this.height = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("height", _value); + } + } + + set_class(_value) { + var node = this.getDOMNode(this); + if (node) + { + if (this["class"]) + { + jQuery(node).removeClass(this["class"]); + } + jQuery(node).addClass(_value); + } + + this["class"] = _value; + } + + set_overflow(_value) { + this.overflow = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("overflow", _value); + } + } + + set_data(_value) + { + var node = this.getDOMNode(this); + if (node && _value) + { + var pairs = _value.split(/,/g); + for(var i=0; i < pairs.length; ++i) + { + var name_value = pairs[i].split(':'); + jQuery(node).attr('data-'+name_value[0], name_value[1]); + } + } + } + + set_background(_value) + { + var node = this.getDOMNode(this); + var values = ''; + if (_value && node) + { + values = _value.split(','); + jQuery(node).css({ + "background-image":'url("'+values[0]+'")', + "background-position-x":values[1], + "background-position-y":values[2], + "background-scale":values[3] + }); + } + } + + /** + * Set Actions on the widget + * + * Each action is defined as an object: + * + * move: { + * type: "drop", + * acceptedTypes: "mail", + * icon: "move", + * caption: "Move to" + * onExecute: javascript:mail_move" + * } + * + * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, + * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the + * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The + * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler + * can operate in the widget context easily. The location varies depending on your action though. It + * might be action.parent.parent.data.widget + * + * To customise how the actions are handled for a particular widget, override _link_actions(). It handles + * the more widget-specific parts. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method + */ + set_actions(actions) + { + if(this.id == "" || typeof this.id == "undefined") + { + this.egw().debug("warn", "Widget should have an ID if you want actions",this); + return; + } + + // Initialize the action manager and add some actions to it + // Only look 1 level deep + var gam = egw_getActionManager(this.egw().appName,true,1); + if(typeof this._actionManager != "object") + { + if(gam.getActionById(this.getInstanceManager().uniqueId,1) !== null) + { + gam = gam.getActionById(this.getInstanceManager().uniqueId,1); + } + if(gam.getActionById(this.id,1) != null) + { + this._actionManager = gam.getActionById(this.id,1); + } + else + { + this._actionManager = gam.addAction("actionManager", this.id); + } + } + this._actionManager.updateActions(actions, this.egw().appName); + if (this.options.default_execute) this._actionManager.setDefaultExecute(this.options.default_execute); + + // Put a reference to the widget into the action stuff, so we can + // easily get back to widget context from the action handler + this._actionManager.data = {widget: this}; + + // Link the actions to the DOM + this._link_actions(actions); + } + + set_default_execute(_default_execute) + { + this.options.default_execute = _default_execute; + + if (this._actionManager) this._actionManager.setDefaultExecute(null, _default_execute); + } + + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * This can be overwritten to not allow all actions, by not returning them here. + * + * @param actions + * @returns {Array} + */ + _get_action_links(actions) + { + var action_links = []; + for(var i in actions) + { + var action = actions[i]; + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + return action_links; + } + + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + _link_actions(actions) + { + // Get the top level element for the tree + var objectManager = egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById(this.id); + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = objectManager.insertObject(false, new egwActionObject( + this.id, objectManager, new et2_action_object_impl(this), + this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager + )); + } + else + { + widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + } + + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + widget_object.updateActionLinks(action_links); + } + +} + +/** + * The surroundings manager class allows to append or prepend elements around + * an widget node. + * + * @augments Class + */ +class et2_surroundingsMgr extends ClassWithAttributes +{ + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + init(_widget) { + this.widget = _widget; + + this._widgetContainer = null; + this._widgetSurroundings = []; + this._widgetPlaceholder = null; + this._widgetNode = null; + this._ownPlaceholder = true; + } + + destroy() { + this._widgetContainer = null; + this._widgetSurroundings = null; + this._widgetPlaceholder = null; + this._widgetNode = null; + } + + prependDOMNode(_node) { + this._widgetSurroundings.unshift(_node); + this._surroundingsUpdated = true; + } + + appendDOMNode(_node) { + // Append an placeholder first if none is existing yet + if (this._ownPlaceholder && this._widgetPlaceholder == null) + { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + } + + // Append the given node + this._widgetSurroundings.push(_node); + this._surroundingsUpdated = true; + } + + insertDOMNode(_node) { + if (!this._ownPlaceholder || this._widgetPlaceholder == null) + { + this.appendDOMNode(_node); + return; + } + + // Get the index of the widget placeholder and delete it, insert the + // given node instead + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1, _node); + + // Delete the reference to the own placeholder + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + + removeDOMNode(_node) { + for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) + { + if (this._widgetSurroundings[i] == _node) + { + this._widgetSurroundings.splice(i, 1); + this._surroundingsUpdated = true; + break; + } + } + } + + setWidgetPlaceholder(_node) { + if (_node != this._widgetPlaceholder) + { + if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) + { + // Delete the current placeholder which was created by the + // widget itself + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1); + + // Delete any reference to the own placeholder and set the + // _ownPlaceholder flag to false + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + + this._ownPlaceholder = (_node == null); + this._widgetPlaceholder = _node; + this._surroundingsUpdated = true; + } + } + + _rebuildContainer() { + // Return if there has been no change in the "surroundings-data" + if (!this._surroundingsUpdated) + { + return false; + } + + // Build the widget container + if (this._widgetSurroundings.length > 0) + { + // Check whether the widgetPlaceholder is really inside the DOM-Tree + var hasPlaceholder = et2_hasChild(this._widgetSurroundings, + this._widgetPlaceholder); + + // If not, append another widget placeholder + if (!hasPlaceholder) + { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + + this._ownPlaceholder = true; + } + + // If the surroundings array only contains one element, set this one + // as the widget container + if (this._widgetSurroundings.length == 1) + { + if (this._widgetSurroundings[0] == this._widgetPlaceholder) + { + this._widgetContainer = null; + } + else + { + this._widgetContainer = this._widgetSurroundings[0]; + } + } + else + { + // Create an outer "span" as widgetContainer + this._widgetContainer = document.createElement("span"); + + // Append the children inside the widgetSurroundings array to + // the widget container + for (var i = 0; i < this._widgetSurroundings.length; i++) + { + this._widgetContainer.appendChild(this._widgetSurroundings[i]); + } + } + } + else + { + this._widgetContainer = null; + this._widgetPlaceholder = null; + } + + this._surroundingsUpdated = false; + + return true; + } + + update() { + if (this._surroundingsUpdated) + { + var attached = this.widget ? this.widget.isAttached() : false; + + // Reattach the widget - this will call the "getDOMNode" function + // and trigger the _rebuildContainer function. + if (attached && this.widget) + { + this.widget.detachFromDOM(); + this.widget.attachToDOM(); + } + } + } + + getDOMNode(_widgetNode) { + // Update the whole widgetContainer if this is not the first time this + // function has been called but the widget node has changed. + if (this._widgetNode != null && this._widgetNode != _widgetNode) + { + this._surroundingsUpdated = true; + } + + // Copy a reference to the given node + this._widgetNode = _widgetNode; + + // Build the container if it didn't exist yet. + var updated = this._rebuildContainer(); + + // Return the widget node itself if there are no surroundings arround + // it + if (this._widgetContainer == null) + { + return _widgetNode; + } + + // Replace the widgetPlaceholder with the given widget node if the + // widgetContainer has been updated + if (updated) + { + this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, + this._widgetPlaceholder); + if (!this._ownPlaceholder) + { + this._widgetPlaceholder = _widgetNode; + } + } + + // Return the widget container + return this._widgetContainer; + } + +} + +/** + * The egw_action system requires an egwActionObjectInterface Interface implementation + * to tie actions to DOM nodes. This one can be used by any widget. + * + * The class extension is different than the widgets + * + * @param {et2_DOMWidget} widget + * @param {Object} node + * + */ +function et2_action_object_impl(widget, node) +{ + var aoi = new egwActionObjectInterface(); + var objectNode = node; + + aoi.getWidget = function() { + return widget; + }; + + aoi.doGetDOMNode = function() { + return objectNode?objectNode:widget.getDOMNode(); + }; + +// _outerCall may be used to determine, whether the state change has been +// evoked from the outside and the stateChangeCallback has to be called +// or not. + aoi.doSetState = function(_state, _outerCall) { + }; + +// The doTiggerEvent function may be overritten by the aoi if it wants to +// support certain action implementation specific events like EGW_AI_DRAG_OVER +// or EGW_AI_DRAG_OUT + aoi.doTriggerEvent = function(_event, _data) { + switch(_event) + { + case EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + + + return aoi; +}; diff --git a/api/js/etemplate/et2_core_common.js b/api/js/etemplate/et2_core_common.js index 878b913ada..cf751451fb 100644 --- a/api/js/etemplate/et2_core_common.js +++ b/api/js/etemplate/et2_core_common.js @@ -7,79 +7,64 @@ * @link http://www.egroupware.org * @author Andreas Stöckel * @copyright Stylite 2011 - * @version $Id$ */ - /** * IE Fix for array.indexOf */ -if (typeof Array.prototype.indexOf == "undefined") -{ - Array.prototype.indexOf = function(_elem) { - for (var i = 0; i < this.length; i++) - { - if (this[i] === _elem) - return i; - } - return -1; - }; +if (typeof Array.prototype.indexOf == "undefined") { + Array.prototype.indexOf = function (_elem) { + for (var i = 0; i < this.length; i++) { + if (this[i] === _elem) + return i; + } + return -1; + }; } - /** * Array with all types supported by the et2_checkType function. */ var et2_validTypes = ["boolean", "string", "rawstring", "html", "float", "integer", "any", "js", "dimension"]; - /** * Object whith default values for the above types. Do not specify array or * objects inside the et2_typeDefaults object, as this instance will be shared * between all users of it. */ var et2_typeDefaults = { - "boolean": false, - "string": "", - "rawstring": "", // no html-entity decoding - "html": "", - "js": null, - "float": 0.0, - "integer": 0, - "any": null, - "dimension": "auto" + "boolean": false, + "string": "", + "rawstring": "", + "html": "", + "js": null, + "float": 0.0, + "integer": 0, + "any": null, + "dimension": "auto" }; - -function et2_evalBool(_val) -{ - if (typeof _val == "string") - { - if (_val == "false" || _val == "0") - { - return false; - } - } - - return _val ? true : false; +function et2_evalBool(_val) { + if (typeof _val == "string") { + if (_val == "false" || _val == "0") { + return false; + } + } + return _val ? true : false; } - /** * Concat et2 name together, eg. et2_concat("namespace","test[something]") == "namespace[test][something]" * @param variable number of arguments to contact * @returns string */ -function et2_form_name(_cname,_name) -{ - var parts = []; - for(var i=0; i < arguments.length; ++i) - { - var name = arguments[i]; - if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") - { - parts = parts.concat(name.replace(/]/g,'').split('[')); - } - } - var name = parts.shift(); - return parts.length ? name + '['+parts.join('][')+']' : name; +function et2_form_name(_cname, _name) { + var parts = []; + for (var i = 0; i < arguments.length; ++i) { + var name = arguments[i]; + if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") + { + parts = parts.concat(name.replace(/]/g, '').split('[')); + } + } + var name = parts.shift(); + return parts.length ? name + '[' + parts.join('][') + ']' : name; } - /** * Checks whether the given value is of the given type. Strings are converted * into the corresponding type. The (converted) value is returned. All supported @@ -90,192 +75,138 @@ function et2_form_name(_cname,_name) * @param string _attr attribute name * @param object _widget */ -function et2_checkType(_val, _type, _attr, _widget) -{ - if (typeof _attr == "undefined") - { - _attr = null; - } - - function _err() { - var res = et2_typeDefaults[_type]; - - if(typeof _val != "undefined" && _val) - { - egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + - _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + - "and is now '" + res + "'",_widget); - } - return res; - } - - // If the type is "any" simply return the value again - if (_type == "any") - { - return _val; - } - - // we dont check default-value any further, that also fixes type="js" does NOT accept null, - // which happens on expanded values - if (_val === et2_typeDefaults[_type]) - { - return _val; - } - - // If the type is boolean, check whether the given value is exactly true or - // false. Otherwise check whether the value is the string "true" or "false". - if (_type == "boolean") - { - if (_val === true || _val === false) - { - return _val; - } - - if (typeof _val == "string") - { - var lcv = _val.toLowerCase(); - if (lcv === "true" || lcv === "false" || lcv === "") - { - return _val === "true"; - } - if(lcv === "0" || lcv === "1") - { - return _val === "1"; - } - } - else if (typeof _val == "number") - { - return _val != 0; - } - - return _err(); - } - - // Check whether the given value is of the type "string" - if (_type == "string" || _type == "html" || _type == "rawstring") - { - if (typeof _val == "number") // as php is a bit vague here, silently convert to a string - { - return _val.toString(); - } - - if (typeof _val == "string") - { - return _type == "string" ? html_entity_decode(_val) : _val; - } - - // Handle some less common possibilities - // Maybe a split on an empty string - if(typeof _val == "object" && jQuery.isEmptyObject(_val)) return ""; - - return _err(); - } - - // Check whether the value is already a number, otherwise try to convert it - // to one. - if (_type == "float") - { - if (typeof _val == "number") - { - return _val; - } - - if (!isNaN(_val)) - { - return parseFloat(_val); - } - - return _err(); - } - - // Check whether the value is an integer by comparing the result of - // parseInt(_val) to the value itself. - if (_type == "integer") - { - if (parseInt(_val) == _val) - { - return parseInt(_val); - } - - return _err(); - } - - // Parse the given dimension value - if (_type == "dimension") - { - // Case 1: The value is "auto" - if (_val == "auto") - { - return _val; - } - - // Case 2: The value is simply a number, attach "px" - if (!isNaN(_val)) - { - return parseFloat(_val) + "px"; - } - - // Case 3: The value is already a valid css pixel value or a percentage - if (typeof _val == "string" && - ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0])) || - (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0])))) - { - return _val; - } - - return _err(); - } - - // Javascript - if (_type == "js") - { - if (typeof _val == "function" || typeof _val == "undefined") - { - return _val; - } - if (_val) _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); - - // Check to see if it's a string in app.appname.function format, and wrap it in - // a closure to make sure context is preserved - if(typeof _val == "string" && _val.substr(0,4) == "app." && app) - { - var parts = _val.split('.'); - var func = parts.pop(); - var parent = window; - for(var i=0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) - { - parent = parent[parts[i]]; - } - if (typeof parent[func] == 'function') - { - try - { - return jQuery.proxy(parent[func],parent); - } - catch (e) - { - req.egw.debug('error', 'Function', _val); - return _err(); - } - } - } - - if (!_val || typeof _val == "string") - { - return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised - } - } - - // We should never come here - throw("Invalid type identifier '" + _attr + "': '" + _type+"'"); +function et2_checkType(_val, _type, _attr, _widget) { + if (typeof _attr == "undefined") { + _attr = null; + } + function _err() { + var res = et2_typeDefaults[_type]; + if (typeof _val != "undefined" && _val) { + egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + + _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + + "and is now '" + res + "'", _widget); + } + return res; + } + // If the type is "any" simply return the value again + if (_type == "any") { + return _val; + } + // we dont check default-value any further, that also fixes type="js" does NOT accept null, + // which happens on expanded values + if (_val === et2_typeDefaults[_type]) { + return _val; + } + // If the type is boolean, check whether the given value is exactly true or + // false. Otherwise check whether the value is the string "true" or "false". + if (_type == "boolean") { + if (_val === true || _val === false) { + return _val; + } + if (typeof _val == "string") { + var lcv = _val.toLowerCase(); + if (lcv === "true" || lcv === "false" || lcv === "") { + return _val === "true"; + } + if (lcv === "0" || lcv === "1") { + return _val === "1"; + } + } + else if (typeof _val == "number") { + return _val != 0; + } + return _err(); + } + // Check whether the given value is of the type "string" + if (_type == "string" || _type == "html" || _type == "rawstring") { + if (typeof _val == "number") // as php is a bit vague here, silently convert to a string + { + return _val.toString(); + } + if (typeof _val == "string") { + return _type == "string" ? html_entity_decode(_val) : _val; + } + // Handle some less common possibilities + // Maybe a split on an empty string + if (typeof _val == "object" && jQuery.isEmptyObject(_val)) + return ""; + return _err(); + } + // Check whether the value is already a number, otherwise try to convert it + // to one. + if (_type == "float") { + if (typeof _val == "number") { + return _val; + } + if (!isNaN(_val)) { + return parseFloat(_val); + } + return _err(); + } + // Check whether the value is an integer by comparing the result of + // parseInt(_val) to the value itself. + if (_type == "integer") { + if (parseInt(_val) == _val) { + return parseInt(_val); + } + return _err(); + } + // Parse the given dimension value + if (_type == "dimension") { + // Case 1: The value is "auto" + if (_val == "auto") { + return _val; + } + // Case 2: The value is simply a number, attach "px" + if (!isNaN(_val)) { + return parseFloat(_val) + "px"; + } + // Case 3: The value is already a valid css pixel value or a percentage + if (typeof _val == "string" && + ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0])) || + (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0])))) { + return _val; + } + return _err(); + } + // Javascript + if (_type == "js") { + if (typeof _val == "function" || typeof _val == "undefined") { + return _val; + } + if (_val) + _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); + // Check to see if it's a string in app.appname.function format, and wrap it in + // a closure to make sure context is preserved + if (typeof _val == "string" && _val.substr(0, 4) == "app." && app) { + var parts = _val.split('.'); + var func = parts.pop(); + var parent = window; + for (var i = 0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) { + parent = parent[parts[i]]; + } + if (typeof parent[func] == 'function') { + try { + return jQuery.proxy(parent[func], parent); + } + catch (e) { + egw.debug('error', 'Function', _val); + return _err(); + } + } + } + if (!_val || typeof _val == "string") { + return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised + } + } + // We should never come here + throw ("Invalid type identifier '" + _attr + "': '" + _type + "'"); } - /** * If et2_no_init is set as default value, the initAttributes function will not * try to initialize the attribute with the default value. */ var et2_no_init = new Object(); - /** * Validates the given attribute with the given id. The validation checks for * the existance of a human name, a description, a type and a default value. @@ -283,113 +214,81 @@ var et2_no_init = new Object(); * empty string, the type defaults to any and the default to the corresponding * type default. */ -function et2_validateAttrib(_id, _attrib) -{ - // Default ignore to false. - if (typeof _attrib["ignore"] == "undefined") - { - _attrib["ignore"] = false; - } - - // Break if "ignore" is set to true. - if (_attrib.ignore) - { - return; - } - - if (typeof _attrib["name"] == "undefined") - { - _attrib["name"] = _id; - egw.debug("log", "Human name ('name'-Field) for attribute '" + - _id + "' has not been supplied, set to '" + _id + "'"); - } - - if (typeof _attrib["description"] == "undefined") - { - _attrib["description"] = ""; - egw.debug("log", "Description for attribute '" + - _id + "' has not been supplied"); - } - - if (typeof _attrib["type"] == "undefined") - { - _attrib["type"] = "any"; - } - else - { - if (et2_validTypes.indexOf(_attrib["type"]) < 0) - { - egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + - "' supplied. Valid types are ", et2_validTypes); - } - } - - // Set the defaults - if (typeof _attrib["default"] == "undefined") - { - _attrib["default"] = et2_typeDefaults[_attrib["type"]]; - } +function et2_validateAttrib(_id, _attrib) { + // Default ignore to false. + if (typeof _attrib["ignore"] == "undefined") { + _attrib["ignore"] = false; + } + // Break if "ignore" is set to true. + if (_attrib.ignore) { + return; + } + if (typeof _attrib["name"] == "undefined") { + _attrib["name"] = _id; + egw.debug("log", "Human name ('name'-Field) for attribute '" + + _id + "' has not been supplied, set to '" + _id + "'"); + } + if (typeof _attrib["description"] == "undefined") { + _attrib["description"] = ""; + egw.debug("log", "Description for attribute '" + + _id + "' has not been supplied"); + } + if (typeof _attrib["type"] == "undefined") { + _attrib["type"] = "any"; + } + else { + if (et2_validTypes.indexOf(_attrib["type"]) < 0) { + egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + + "' supplied. Valid types are ", et2_validTypes); + } + } + // Set the defaults + if (typeof _attrib["default"] == "undefined") { + _attrib["default"] = et2_typeDefaults[_attrib["type"]]; + } } - /** * Equivalent to the PHP array_values function */ -function et2_arrayValues(_arr) -{ - var result = []; - for (var key in _arr) - { - if (parseInt(key) == key) - { - result.push(_arr[key]); - } - } - - return result; +function et2_arrayValues(_arr) { + var result = []; + for (var key in _arr) { + // @ts-ignore we check key is an integer + if (parseInt(key) == key) { + result.push(_arr[key]); + } + } + return result; } - /** * Equivalent to the PHP array_keys function */ -function et2_arrayKeys(_arr) -{ - var result = []; - for (var key in _arr) - { - result.push(key); - } - - return result; +function et2_arrayKeys(_arr) { + var result = []; + for (var key in _arr) { + result.push(key); + } + return result; } - -function et2_arrayIntKeys(_arr) -{ - var result = []; - for (var key in _arr) - { - result.push(parseInt(key)); - } - - return result; +function et2_arrayIntKeys(_arr) { + var result = []; + for (var key in _arr) { + result.push(parseInt(key)); + } + return result; } - - /** * Equivalent to the PHP substr function, partly take from phpjs, licensed under * the GPL. */ -function et2_substr (str, start, len) { - var end = str.length; - - if (start < 0) - { - start += end; - } - end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); - - return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); +function et2_substr(str, start, len) { + var end = str.length; + if (start < 0) { + start += end; + } + end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); + return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); } - /** * Split a $delimiter-separated options string, which can contain parts with * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php @@ -407,374 +306,276 @@ function et2_substr (str, start, len) { * @param string _enclosure='"' * @return array */ -function et2_csvSplit(_str, _num, _delimiter, _enclosure) -{ - // Default the parameters - if (typeof _str == "undefined" || _str == null) - { - _str = ""; - } - if (typeof _num == "undefined") - { - _num = null; - } - - if (typeof _delimiter == "undefined") - { - _delimiter = ","; - } - - if (typeof _enclosure == "undefined") - { - _enclosure = '"'; - } - - // If the _enclosure string does not occur in the string, simply use the - // split function - if (_str.indexOf(_enclosure) == -1) - { - return _num === null ? _str.split(_delimiter) : - _str.split(_delimiter, _num); - } - - // Split the string at the delimiter and join it again, when a enclosure is - // found at the beginning/end of a part - var parts = _str.split(_delimiter); - for (var n = 0; typeof parts[n] != "undefined"; n++) - { - var part = parts[n]; - - if (part.charAt(0) === _enclosure) - { - var m = n; - while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) - { - parts[n] += _delimiter + parts[++m]; - delete(parts[m]); - } - parts[n] = et2_substr(parts[n].replace( - new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1 , -1); - n = m; - } - } - - // Rebuild the array index - parts = et2_arrayValues(parts); - - // Limit the parts to the given number - if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) - { - parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); - parts = parts.slice(0, _num); - } - - return parts; +function et2_csvSplit(_str, _num, _delimiter, _enclosure) { + // Default the parameters + if (typeof _str == "undefined" || _str == null) { + _str = ""; + } + if (typeof _num == "undefined") { + _num = null; + } + if (typeof _delimiter == "undefined") { + _delimiter = ","; + } + if (typeof _enclosure == "undefined") { + _enclosure = '"'; + } + // If the _enclosure string does not occur in the string, simply use the + // split function + if (_str.indexOf(_enclosure) == -1) { + return _num === null ? _str.split(_delimiter) : + _str.split(_delimiter, _num); + } + // Split the string at the delimiter and join it again, when a enclosure is + // found at the beginning/end of a part + var parts = _str.split(_delimiter); + for (var n = 0; typeof parts[n] != "undefined"; n++) { + var part = parts[n]; + if (part.charAt(0) === _enclosure) { + var m = n; + while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) { + parts[n] += _delimiter + parts[++m]; + delete (parts[m]); + } + parts[n] = et2_substr(parts[n].replace(new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1, -1); + n = m; + } + } + // Rebuild the array index + parts = et2_arrayValues(parts); + // Limit the parts to the given number + if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) { + parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); + parts = parts.slice(0, _num); + } + return parts; } - /** * Parses the given string and returns an array marking parts which are URLs */ -function et2_activateLinks(_content) -{ - var _match = false; - var arr = []; - - function _splitPush(_matches, _proc) - { - if (_matches) - { - // We had a match - _match = true; - - // Replace "undefined" with "" - for (var i = 1; i < _matches.length; i++) - { - if (typeof _matches[i] == "undefined") - { - _matches[i] = ""; - } - } - - // Split the content string at the given position(s) - // but we only handle the first occurence - var splitted = _content.split(_matches[0]); - - // Push the not-matched part - var left = splitted.shift(); - if (left) - { - // activate the links of the left string - arr = arr.concat(et2_activateLinks(left)); - } - - // Call the callback function which converts the matches into an object - // and appends it to the string - _proc(_matches); - - // Set the new working string to the right part - _content = splitted.join(_matches[0]); - } - } - - var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; - - // First match things beginning with http:// (or other protocols) - var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown - var domain = '([\\w-]+\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var http_regExp = new RegExp(protocol + domain + subdir, 'i'); - - // Now match things beginning with www. - var domain = 'www(\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var www_regExp = new RegExp(domain + subdir, 'i'); - - do { - _match = false; - - // Abort if the remaining length of _content is smaller than 20 for - // performance reasons - if (!_content) - { - break; - } - - // No need make emailaddress spam-save, as it gets dynamically created - _splitPush(_content.match(mail_regExp), function(_matches) { - arr.push({ - "href": (_matches[1]?'':'mailto:')+_matches[0], - "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] - }); - }); - - // Create hrefs for links starting with "http://" - _splitPush(_content.match(http_regExp), function(_matches) { - arr.push({ - "href": _matches[0], - "text": _matches[2] + _matches[3] + _matches[4] - }); - }); - - // Create hrefs for links starting with "www." - _splitPush(_content.match(www_regExp), function(_matches) { - arr.push({ - "href": "http://" + _matches[0], - "text": _matches[0] - }); - }); - } while (_match) - - arr.push(_content); - - return arr; +function et2_activateLinks(_content) { + var _match = false; + var arr = []; + function _splitPush(_matches, _proc) { + if (_matches) { + // We had a match + _match = true; + // Replace "undefined" with "" + for (var i = 1; i < _matches.length; i++) { + if (typeof _matches[i] == "undefined") { + _matches[i] = ""; + } + } + // Split the content string at the given position(s) + // but we only handle the first occurence + var splitted = _content.split(_matches[0]); + // Push the not-matched part + var left = splitted.shift(); + if (left) { + // activate the links of the left string + arr = arr.concat(et2_activateLinks(left)); + } + // Call the callback function which converts the matches into an object + // and appends it to the string + _proc(_matches); + // Set the new working string to the right part + _content = splitted.join(_matches[0]); + } + } + var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; + // First match things beginning with http:// (or other protocols) + var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown + var domain = '([\\w-]+\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var http_regExp = new RegExp(protocol + domain + subdir, 'i'); + // Now match things beginning with www. + var domain = 'www(\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var www_regExp = new RegExp(domain + subdir, 'i'); + do { + _match = false; + // Abort if the remaining length of _content is smaller than 20 for + // performance reasons + if (!_content) { + break; + } + // No need make emailaddress spam-save, as it gets dynamically created + _splitPush(_content.match(mail_regExp), function (_matches) { + arr.push({ + "href": (_matches[1] ? '' : 'mailto:') + _matches[0], + "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] + }); + }); + // Create hrefs for links starting with "http://" + _splitPush(_content.match(http_regExp), function (_matches) { + arr.push({ + "href": _matches[0], + "text": _matches[2] + _matches[3] + _matches[4] + }); + }); + // Create hrefs for links starting with "www." + _splitPush(_content.match(www_regExp), function (_matches) { + arr.push({ + "href": "http://" + _matches[0], + "text": _matches[0] + }); + }); + } while (_match); + arr.push(_content); + return arr; } - /** * Inserts the structure generated by et2_activateLinks into the given DOM-Node */ -function et2_insertLinkText(_text, _node, _target) -{ - if(!_node) - { - egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); - return; - } - - // Clear the node - for (var i = _node.childNodes.length - 1; i >= 0; i--) - { - _node.removeChild(_node.childNodes[i]); - } - - for (var i = 0; i < _text.length; i++) - { - var s = _text[i]; - - if (typeof s == "string" || typeof s == "number") - { - // Include line breaks - var lines = s.split ? s.split('\n') : [s]; - - // Insert the lines - for (var j = 0; j < lines.length; j++) - { - _node.appendChild(document.createTextNode(lines[j])); - - if (j < lines.length - 1) - { - _node.appendChild(document.createElement("br")); - } - } - } - else if(s.text) // no need to generate a link, if there is no content in it - { - if(!s.href) - { - egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); - s.href = ""; - } - var a = jQuery(document.createElement("a")) - .attr("href", s.href) - .text(s.text); - - if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") - { - a.attr("target", _target); - } - // open mailto links depending on preferences in mail app - if (s.href.substr(0, 7) == "mailto:" && - (egw.user('apps').mail || egw.user('apps').felamimail) && - egw.preference('force_mailto','addressbook') != '1') - { - a.click(function(event){ - egw.open_link(this.href); - return false; - }); - } - - a.appendTo(_node); - } - } +function et2_insertLinkText(_text, _node, _target) { + if (!_node) { + egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); + return; + } + // Clear the node + for (var i = _node.childNodes.length - 1; i >= 0; i--) { + _node.removeChild(_node.childNodes[i]); + } + for (var i = 0; i < _text.length; i++) { + var s = _text[i]; + if (typeof s == "string" || typeof s == "number") { + // Include line breaks + var lines = typeof s !== "number" && s.split ? s.split('\n') : [s + ""]; + // Insert the lines + for (var j = 0; j < lines.length; j++) { + _node.appendChild(document.createTextNode(lines[j])); + if (j < lines.length - 1) { + _node.appendChild(document.createElement("br")); + } + } + } + else if (s.text) // no need to generate a link, if there is no content in it + { + if (!s.href) { + egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); + s.href = ""; + } + var a = jQuery(document.createElement("a")) + .attr("href", s.href) + .text(s.text); + if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") { + a.attr("target", _target); + } + // open mailto links depending on preferences in mail app + if (s.href.substr(0, 7) == "mailto:" && + (egw.user('apps').mail || egw.user('apps').felamimail) && + egw.preference('force_mailto', 'addressbook') != '1') { + a.click(function (event) { + egw.open_link(this.href); + return false; + }); + } + a.appendTo(_node); + } + } } - /** * Creates a copy of the given object (non recursive) */ -function et2_cloneObject(_obj) -{ - var result = {}; - - for (var key in _obj) - { - result[key] = _obj[key]; - } - - return result; +function et2_cloneObject(_obj) { + var result = {}; + for (var key in _obj) { + result[key] = _obj[key]; + } + return result; } - /** * Returns true if the given array of nodes or their children contains the given * child node. */ -function et2_hasChild(_nodes, _child) -{ - for (var i = 0; i < _nodes.length; i++) - { - if (_nodes[i] == _child) - { - return true; - } - else if (_nodes[i].childNodes) - { - var res = et2_hasChild(_nodes[i].childNodes, _child); - - if (res) - { - return true; - } - } - } - - return false; +function et2_hasChild(_nodes, _child) { + for (var i = 0; i < _nodes.length; i++) { + if (_nodes[i] == _child) { + return true; + } + else if (_nodes[i].childNodes) { + var res = et2_hasChild(_nodes[i].childNodes, _child); + if (res) { + return true; + } + } + } + return false; } - /** * Functions to work with ranges and range intersection (used in the dataview) */ - /** * Common functions used in most view classes */ - /** * Returns an "range" object with the given top position and height */ -function et2_range(_top, _height) -{ - return { - "top": _top, - "bottom": _top + _height - }; +function et2_range(_top, _height) { + return { + "top": _top, + "bottom": _top + _height + }; } - /** * Returns an "area" object with the given top- and bottom position */ -function et2_bounds(_top, _bottom) -{ - return { - "top": _top, - "bottom": _bottom - }; +function et2_bounds(_top, _bottom) { + return { + "top": _top, + "bottom": _bottom + }; } - /** * Returns whether two range objects intersect each other */ -function et2_rangeIntersect(_ar1, _ar2) -{ - return ! (_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); +function et2_rangeIntersect(_ar1, _ar2) { + return !(_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); } - /** * Returns whether two ranges intersect (result = 0) or their relative position * to each other (used to do a binary search inside a list of sorted range objects). */ -function et2_rangeIntersectDir(_ar1, _ar2) -{ - if (_ar1.bottom < _ar2.top) - { - return -1; - } - if (_ar1.top > _ar2.bottom) - { - return 1; - } - return 0; +function et2_rangeIntersectDir(_ar1, _ar2) { + if (_ar1.bottom < _ar2.top) { + return -1; + } + if (_ar1.top > _ar2.bottom) { + return 1; + } + return 0; } - /** * Returns whether two ranges are equal. */ -function et2_rangeEqual(_ar1, _ar2) -{ - return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; +function et2_rangeEqual(_ar1, _ar2) { + return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; } - /** * Substracts _ar2 from _ar1, returns an array of new ranges. */ -function et2_rangeSubstract(_ar1, _ar2) -{ - // Per default return the complete _ar1 range - var res = [_ar1]; - - // Check whether there is an intersection between the given ranges - if (et2_rangeIntersect(_ar1, _ar2)) - { - res = [et2_bounds(_ar1.top, _ar2.top), - et2_bounds(_ar2.bottom, _ar1.bottom)]; - } - - // Remove all zero-length ranges from the result - for (var i = res.length - 1; i >= 0; i--) - { - if (res[i].bottom - res[i].top <= 0) - { - res.splice(i, 1); - } - } - - return res; +function et2_rangeSubstract(_ar1, _ar2) { + // Per default return the complete _ar1 range + var res = [_ar1]; + // Check whether there is an intersection between the given ranges + if (et2_rangeIntersect(_ar1, _ar2)) { + res = [et2_bounds(_ar1.top, _ar2.top), + et2_bounds(_ar2.bottom, _ar1.bottom)]; + } + // Remove all zero-length ranges from the result + for (var i = res.length - 1; i >= 0; i--) { + if (res[i].bottom - res[i].top <= 0) { + res.splice(i, 1); + } + } + return res; } - /** * Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&') === '&' * * @param {string} _str * @returns {string} */ -function html_entity_decode(_str) -{ - return _str && _str.indexOf('&') != -1 ? jQuery(''+_str+'').text() : _str; +function html_entity_decode(_str) { + return _str && _str.indexOf('&') != -1 ? jQuery('' + _str + '').text() : _str; } diff --git a/api/js/etemplate/et2_core_common.ts b/api/js/etemplate/et2_core_common.ts new file mode 100644 index 0000000000..f428e43306 --- /dev/null +++ b/api/js/etemplate/et2_core_common.ts @@ -0,0 +1,780 @@ +/** + * EGroupware eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + */ + +/** + * IE Fix for array.indexOf + */ +if (typeof Array.prototype.indexOf == "undefined") +{ + Array.prototype.indexOf = function(_elem) { + for (var i = 0; i < this.length; i++) + { + if (this[i] === _elem) + return i; + } + return -1; + }; +} + +/** + * Array with all types supported by the et2_checkType function. + */ +var et2_validTypes = ["boolean", "string", "rawstring", "html", "float", "integer", "any", "js", "dimension"]; + +/** + * Object whith default values for the above types. Do not specify array or + * objects inside the et2_typeDefaults object, as this instance will be shared + * between all users of it. + */ +var et2_typeDefaults : object = { + "boolean": false, + "string": "", + "rawstring": "", // no html-entity decoding + "html": "", + "js": null, + "float": 0.0, + "integer": 0, + "any": null, + "dimension": "auto" +}; + +function et2_evalBool(_val) +{ + if (typeof _val == "string") + { + if (_val == "false" || _val == "0") + { + return false; + } + } + + return _val ? true : false; +} + +/** + * Concat et2 name together, eg. et2_concat("namespace","test[something]") == "namespace[test][something]" + * @param variable number of arguments to contact + * @returns string + */ +function et2_form_name(_cname,_name) +{ + var parts = []; + for(var i=0; i < arguments.length; ++i) + { + var name = arguments[i]; + if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") + { + parts = parts.concat(name.replace(/]/g,'').split('[')); + } + } + var name = parts.shift(); + return parts.length ? name + '['+parts.join('][')+']' : name; +} + +/** + * Checks whether the given value is of the given type. Strings are converted + * into the corresponding type. The (converted) value is returned. All supported + * types are listed in the et2_validTypes array. + * + * @param mixed _val value + * @param string _type a valid type eg. "string" or "js" + * @param string _attr attribute name + * @param object _widget + */ +function et2_checkType(_val, _type, _attr, _widget) +{ + if (typeof _attr == "undefined") + { + _attr = null; + } + + function _err() { + var res = et2_typeDefaults[_type]; + + if(typeof _val != "undefined" && _val) + { + egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + + _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + + "and is now '" + res + "'",_widget); + } + return res; + } + + // If the type is "any" simply return the value again + if (_type == "any") + { + return _val; + } + + // we dont check default-value any further, that also fixes type="js" does NOT accept null, + // which happens on expanded values + if (_val === et2_typeDefaults[_type]) + { + return _val; + } + + // If the type is boolean, check whether the given value is exactly true or + // false. Otherwise check whether the value is the string "true" or "false". + if (_type == "boolean") + { + if (_val === true || _val === false) + { + return _val; + } + + if (typeof _val == "string") + { + var lcv = _val.toLowerCase(); + if (lcv === "true" || lcv === "false" || lcv === "") + { + return _val === "true"; + } + if(lcv === "0" || lcv === "1") + { + return _val === "1"; + } + } + else if (typeof _val == "number") + { + return _val != 0; + } + + return _err(); + } + + // Check whether the given value is of the type "string" + if (_type == "string" || _type == "html" || _type == "rawstring") + { + if (typeof _val == "number") // as php is a bit vague here, silently convert to a string + { + return _val.toString(); + } + + if (typeof _val == "string") + { + return _type == "string" ? html_entity_decode(_val) : _val; + } + + // Handle some less common possibilities + // Maybe a split on an empty string + if(typeof _val == "object" && jQuery.isEmptyObject(_val)) return ""; + + return _err(); + } + + // Check whether the value is already a number, otherwise try to convert it + // to one. + if (_type == "float") + { + if (typeof _val == "number") + { + return _val; + } + + if (!isNaN(_val)) + { + return parseFloat(_val); + } + + return _err(); + } + + // Check whether the value is an integer by comparing the result of + // parseInt(_val) to the value itself. + if (_type == "integer") + { + if (parseInt(_val) == _val) + { + return parseInt(_val); + } + + return _err(); + } + + // Parse the given dimension value + if (_type == "dimension") + { + // Case 1: The value is "auto" + if (_val == "auto") + { + return _val; + } + + // Case 2: The value is simply a number, attach "px" + if (!isNaN(_val)) + { + return parseFloat(_val) + "px"; + } + + // Case 3: The value is already a valid css pixel value or a percentage + if (typeof _val == "string" && + ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0] as any)) || + (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0] as any)))) + { + return _val; + } + + return _err(); + } + + // Javascript + if (_type == "js") + { + if (typeof _val == "function" || typeof _val == "undefined") + { + return _val; + } + if (_val) _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); + + // Check to see if it's a string in app.appname.function format, and wrap it in + // a closure to make sure context is preserved + if(typeof _val == "string" && _val.substr(0,4) == "app." && app) + { + var parts = _val.split('.'); + var func = parts.pop(); + var parent = window; + for(var i=0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) + { + parent = parent[parts[i]]; + } + if (typeof parent[func] == 'function') + { + try + { + return jQuery.proxy(parent[func],parent); + } + catch (e) + { + egw.debug('error', 'Function', _val); + return _err(); + } + } + } + + if (!_val || typeof _val == "string") + { + return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised + } + } + + // We should never come here + throw("Invalid type identifier '" + _attr + "': '" + _type+"'"); +} + +/** + * If et2_no_init is set as default value, the initAttributes function will not + * try to initialize the attribute with the default value. + */ +const et2_no_init = new Object(); + +/** + * Validates the given attribute with the given id. The validation checks for + * the existance of a human name, a description, a type and a default value. + * If the human name defaults to the given id, the description defaults to an + * empty string, the type defaults to any and the default to the corresponding + * type default. + */ +function et2_validateAttrib(_id, _attrib) +{ + // Default ignore to false. + if (typeof _attrib["ignore"] == "undefined") + { + _attrib["ignore"] = false; + } + + // Break if "ignore" is set to true. + if (_attrib.ignore) + { + return; + } + + if (typeof _attrib["name"] == "undefined") + { + _attrib["name"] = _id; + egw.debug("log", "Human name ('name'-Field) for attribute '" + + _id + "' has not been supplied, set to '" + _id + "'"); + } + + if (typeof _attrib["description"] == "undefined") + { + _attrib["description"] = ""; + egw.debug("log", "Description for attribute '" + + _id + "' has not been supplied"); + } + + if (typeof _attrib["type"] == "undefined") + { + _attrib["type"] = "any"; + } + else + { + if (et2_validTypes.indexOf(_attrib["type"]) < 0) + { + egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + + "' supplied. Valid types are ", et2_validTypes); + } + } + + // Set the defaults + if (typeof _attrib["default"] == "undefined") + { + _attrib["default"] = et2_typeDefaults[_attrib["type"]]; + } +} + +/** + * Equivalent to the PHP array_values function + */ +function et2_arrayValues(_arr) +{ + var result = []; + for (var key in _arr) + { + // @ts-ignore we check key is an integer + if (parseInt(key) == key) + { + result.push(_arr[key]); + } + } + + return result; +} + +/** + * Equivalent to the PHP array_keys function + */ +function et2_arrayKeys(_arr) +{ + var result = []; + for (var key in _arr) + { + result.push(key); + } + + return result; +} + +function et2_arrayIntKeys(_arr) +{ + var result = []; + for (var key in _arr) + { + result.push(parseInt(key)); + } + + return result; +} + + +/** + * Equivalent to the PHP substr function, partly take from phpjs, licensed under + * the GPL. + */ +function et2_substr (str, start, len) { + var end = str.length; + + if (start < 0) + { + start += end; + } + end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); + + return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); +} + +/** + * Split a $delimiter-separated options string, which can contain parts with + * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php + * + * Examples: + * - et2_csvSplit('"1,2,3",2,3') === array('1,2,3','2','3') + * - et2_csvSplit('1,2,3',2) === array('1','2,3') + * - et2_csvSplit('"1,2,3",2,3',2) === array('1,2,3','2,3') + * - et2_csvSplit('"a""b,c",d') === array('a"b,c','d') // to escape enclosures double them! + * + * @param string _str + * @param int _num=null in how many parts to split maximal, parts over this + * number end up (unseparated) in the last part + * @param string _delimiter=',' + * @param string _enclosure='"' + * @return array + */ +function et2_csvSplit(_str : string, _num? : number, _delimiter? : string, _enclosure? : string) +{ + // Default the parameters + if (typeof _str == "undefined" || _str == null) + { + _str = ""; + } + if (typeof _num == "undefined") + { + _num = null; + } + + if (typeof _delimiter == "undefined") + { + _delimiter = ","; + } + + if (typeof _enclosure == "undefined") + { + _enclosure = '"'; + } + + // If the _enclosure string does not occur in the string, simply use the + // split function + if (_str.indexOf(_enclosure) == -1) + { + return _num === null ? _str.split(_delimiter) : + _str.split(_delimiter, _num); + } + + // Split the string at the delimiter and join it again, when a enclosure is + // found at the beginning/end of a part + var parts = _str.split(_delimiter); + for (var n = 0; typeof parts[n] != "undefined"; n++) + { + var part = parts[n]; + + if (part.charAt(0) === _enclosure) + { + var m = n; + while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) + { + parts[n] += _delimiter + parts[++m]; + delete(parts[m]); + } + parts[n] = et2_substr(parts[n].replace( + new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1 , -1); + n = m; + } + } + + // Rebuild the array index + parts = et2_arrayValues(parts); + + // Limit the parts to the given number + if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) + { + parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); + parts = parts.slice(0, _num); + } + + return parts; +} + +/** + * Parses the given string and returns an array marking parts which are URLs + */ +function et2_activateLinks(_content) +{ + var _match = false; + var arr = []; + + function _splitPush(_matches, _proc) + { + if (_matches) + { + // We had a match + _match = true; + + // Replace "undefined" with "" + for (var i = 1; i < _matches.length; i++) + { + if (typeof _matches[i] == "undefined") + { + _matches[i] = ""; + } + } + + // Split the content string at the given position(s) + // but we only handle the first occurence + var splitted = _content.split(_matches[0]); + + // Push the not-matched part + var left = splitted.shift(); + if (left) + { + // activate the links of the left string + arr = arr.concat(et2_activateLinks(left)); + } + + // Call the callback function which converts the matches into an object + // and appends it to the string + _proc(_matches); + + // Set the new working string to the right part + _content = splitted.join(_matches[0]); + } + } + + var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; + + // First match things beginning with http:// (or other protocols) + var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown + var domain = '([\\w-]+\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var http_regExp = new RegExp(protocol + domain + subdir, 'i'); + + // Now match things beginning with www. + var domain = 'www(\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var www_regExp = new RegExp(domain + subdir, 'i'); + + do { + _match = false; + + // Abort if the remaining length of _content is smaller than 20 for + // performance reasons + if (!_content) + { + break; + } + + // No need make emailaddress spam-save, as it gets dynamically created + _splitPush(_content.match(mail_regExp), function(_matches) { + arr.push({ + "href": (_matches[1]?'':'mailto:')+_matches[0], + "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] + }); + }); + + // Create hrefs for links starting with "http://" + _splitPush(_content.match(http_regExp), function(_matches) { + arr.push({ + "href": _matches[0], + "text": _matches[2] + _matches[3] + _matches[4] + }); + }); + + // Create hrefs for links starting with "www." + _splitPush(_content.match(www_regExp), function(_matches) { + arr.push({ + "href": "http://" + _matches[0], + "text": _matches[0] + }); + }); + } while (_match) + + arr.push(_content); + + return arr; +} + +/** + * Inserts the structure generated by et2_activateLinks into the given DOM-Node + */ +function et2_insertLinkText(_text, _node, _target) +{ + if(!_node) + { + egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); + return; + } + + // Clear the node + for (var i = _node.childNodes.length - 1; i >= 0; i--) + { + _node.removeChild(_node.childNodes[i]); + } + + for (var i = 0; i < _text.length; i++) + { + var s = _text[i]; + + if (typeof s == "string" || typeof s == "number") + { + // Include line breaks + var lines = typeof s !== "number" && s.split ? s.split('\n') : [s+""]; + + // Insert the lines + for (var j = 0; j < lines.length; j++) + { + _node.appendChild(document.createTextNode(lines[j])); + + if (j < lines.length - 1) + { + _node.appendChild(document.createElement("br")); + } + } + } + else if(s.text) // no need to generate a link, if there is no content in it + { + if(!s.href) + { + egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); + s.href = ""; + } + var a = jQuery(document.createElement("a")) + .attr("href", s.href) + .text(s.text); + + if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") + { + a.attr("target", _target); + } + // open mailto links depending on preferences in mail app + if (s.href.substr(0, 7) == "mailto:" && + (egw.user('apps').mail || egw.user('apps').felamimail) && + egw.preference('force_mailto','addressbook') != '1') + { + a.click(function(event){ + egw.open_link(this.href); + return false; + }); + } + + a.appendTo(_node); + } + } +} + +/** + * Creates a copy of the given object (non recursive) + */ +function et2_cloneObject(_obj) +{ + var result = {}; + + for (var key in _obj) + { + result[key] = _obj[key]; + } + + return result; +} + +/** + * Returns true if the given array of nodes or their children contains the given + * child node. + */ +function et2_hasChild(_nodes, _child) +{ + for (var i = 0; i < _nodes.length; i++) + { + if (_nodes[i] == _child) + { + return true; + } + else if (_nodes[i].childNodes) + { + var res = et2_hasChild(_nodes[i].childNodes, _child); + + if (res) + { + return true; + } + } + } + + return false; +} + +/** + * Functions to work with ranges and range intersection (used in the dataview) + */ + +/** + * Common functions used in most view classes + */ + +/** + * Returns an "range" object with the given top position and height + */ +function et2_range(_top, _height) +{ + return { + "top": _top, + "bottom": _top + _height + }; +} + +/** + * Returns an "area" object with the given top- and bottom position + */ +function et2_bounds(_top, _bottom) +{ + return { + "top": _top, + "bottom": _bottom + }; +} + +/** + * Returns whether two range objects intersect each other + */ +function et2_rangeIntersect(_ar1, _ar2) +{ + return ! (_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); +} + +/** + * Returns whether two ranges intersect (result = 0) or their relative position + * to each other (used to do a binary search inside a list of sorted range objects). + */ +function et2_rangeIntersectDir(_ar1, _ar2) +{ + if (_ar1.bottom < _ar2.top) + { + return -1; + } + if (_ar1.top > _ar2.bottom) + { + return 1; + } + return 0; +} + +/** + * Returns whether two ranges are equal. + */ +function et2_rangeEqual(_ar1, _ar2) +{ + return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; +} + +/** + * Substracts _ar2 from _ar1, returns an array of new ranges. + */ +function et2_rangeSubstract(_ar1, _ar2) +{ + // Per default return the complete _ar1 range + var res = [_ar1]; + + // Check whether there is an intersection between the given ranges + if (et2_rangeIntersect(_ar1, _ar2)) + { + res = [et2_bounds(_ar1.top, _ar2.top), + et2_bounds(_ar2.bottom, _ar1.bottom)]; + } + + // Remove all zero-length ranges from the result + for (var i = res.length - 1; i >= 0; i--) + { + if (res[i].bottom - res[i].top <= 0) + { + res.splice(i, 1); + } + } + + return res; +} + +/** + * Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&') === '&' + * + * @param {string} _str + * @returns {string} + */ +function html_entity_decode(_str) +{ + return _str && _str.indexOf('&') != -1 ? jQuery(''+_str+'').text() : _str; +} diff --git a/api/js/etemplate/et2_core_inheritance.js b/api/js/etemplate/et2_core_inheritance.js index 37f00022ae..360bb03454 100644 --- a/api/js/etemplate/et2_core_inheritance.js +++ b/api/js/etemplate/et2_core_inheritance.js @@ -1,158 +1,163 @@ +"use strict"; /** * EGroupware eTemplate2 - JS code for implementing inheritance with attributes * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api - * @link http://www.egroupware.org + * @link: https://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_common; - egw_inheritance; + et2_core_common; */ - -var ClassWithAttributes = (function(){ "use strict"; return Class.extend( -{ - /** - * Returns the value of the given attribute. If the property does not - * exist, an error message is issued. - * - * @param {string} _name - * @return {*} - */ - getAttribute: function(_name) { - if (typeof this.attributes[_name] != "undefined" && - !this.attributes[_name].ignore) - { - if (typeof this["get_" + _name] == "function") - { - return this["get_" + _name](); - } - else - { - return this[_name]; - } - } - else - { - egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); - } - }, - - /** - * The setAttribute function sets the attribute with the given name to - * the given value. _override defines, whether this[_name] will be set, - * if this key already exists. _override defaults to true. A warning - * is issued if the attribute does not exist. - * - * @param {string} _name - * @param {*} _value - * @param {boolean} _override - */ - setAttribute: function(_name, _value, _override) { - if (typeof this.attributes[_name] != "undefined") - { - if (!this.attributes[_name].ignore) - { - if (typeof _override == "undefined") - { - _override = true; - } - - var val = et2_checkType(_value, this.attributes[_name].type, - _name, this); - - if (typeof this["set_" + _name] == "function") - { - this["set_" + _name](val); - } - else if (_override || typeof this[_name] == "undefined") - { - this[_name] = val; - } - } - } - else - { - egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); - } - }, - - /** - * generateAttributeSet sanitizes the given associative array of attributes - * (by passing each entry to "et2_checkType" and checking for existance of - * the attribute) and adds the default values to the associative array. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - generateAttributeSet: function(_attrs) { - - // Sanity check and validation - for (var key in _attrs) - { - if (typeof this.attributes[key] != "undefined") - { - if (!this.attributes[key].ignore) - { - _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, - key, this); - } - } - else - { - // Key does not exist - delete it and issue a warning - delete(_attrs[key]); - egw.debug("warn", this, "Attribute '" + key + - "' does not exist in " + _attrs.type+"!"); - } - } - - // Include default values or already set values for this attribute - for (var key in this.attributes) - { - if (typeof _attrs[key] == "undefined") - { - var _default = this.attributes[key]["default"]; - if (_default == et2_no_init) - { - _default = undefined; - } - - _attrs[key] = _default; - } - } - - return _attrs; - }, - - /** - * The initAttributes function sets the attributes to their default - * values. The attributes are not overwritten, which means, that the - * default is only set, if either a setter exists or this[propName] does - * not exist yet. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - initAttributes: function(_attrs) { - for (var key in _attrs) - { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) - { - this.setAttribute(key, _attrs[key], false); - } - } - }, - - _validate_attributes: function(attributes) - { - // Validate the attributes - for (var key in attributes) - { - et2_validateAttrib(key, attributes[key]); - } - } -});}).call(this); \ No newline at end of file +require("../jsapi/egw_global"); +require("et2_core_common"); +var ClassWithAttributes = /** @class */ (function () { + function ClassWithAttributes() { + } + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + * + * @param {string} _name + * @return {*} + */ + ClassWithAttributes.prototype.getAttribute = function (_name) { + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) { + if (typeof this["get_" + _name] == "function") { + return this["get_" + _name](); + } + else { + return this[_name]; + } + } + else { + egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); + } + }; + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + * + * @param {string} _name + * @param {*} _value + * @param {boolean} _override + */ + ClassWithAttributes.prototype.setAttribute = function (_name, _value, _override) { + if (typeof this.attributes[_name] != "undefined") { + if (!this.attributes[_name].ignore) { + if (typeof _override == "undefined") { + _override = true; + } + var val = et2_checkType(_value, this.attributes[_name].type, _name, this); + if (typeof this["set_" + _name] == "function") { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") { + this[_name] = val; + } + } + } + else { + egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); + } + }; + /** + * generateAttributeSet sanitizes the given associative array of attributes + * (by passing each entry to "et2_checkType" and checking for existance of + * the attribute) and adds the default values to the associative array. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + ClassWithAttributes.prototype.generateAttributeSet = function (_attrs) { + // Sanity check and validation + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined") { + if (!this.attributes[key].ignore) { + _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, key, this); + } + } + else { + // Key does not exist - delete it and issue a warning + delete (_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type + "!"); + } + } + // Include default values or already set values for this attribute + for (var key in this.attributes) { + if (typeof _attrs[key] == "undefined") { + var _default = this.attributes[key]["default"]; + if (_default == et2_no_init) { + _default = undefined; + } + _attrs[key] = _default; + } + } + return _attrs; + }; + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + ClassWithAttributes.prototype.initAttributes = function (_attrs) { + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { + this.setAttribute(key, _attrs[key], false); + } + } + }; + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + ClassWithAttributes.extendAttributes = function (_attributes, _parent) { + function _copyMerge(_new, _old) { + var result = {}; + // Copy the new object + if (typeof _new != "undefined") { + for (var key in _new) { + result[key] = _new[key]; + } + } + // Merge the old object + for (var key in _old) { + if (typeof result[key] == "undefined") { + result[key] = _old[key]; + } + } + return result; + } + var attributes = {}; + // Copy the old attributes + for (var key in _attributes) { + attributes[key] = _copyMerge({}, _attributes[key]); + } + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _parent) { + var _old = _parent[key]; + attributes[key] = _copyMerge(attributes[key], _old); + } + // Validate the attributes + for (var key in attributes) { + et2_validateAttrib(key, attributes[key]); + } + return attributes; + }; + return ClassWithAttributes; +}()); +exports.ClassWithAttributes = ClassWithAttributes; diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts new file mode 100644 index 0000000000..34c891ce30 --- /dev/null +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -0,0 +1,217 @@ +/** + * EGroupware eTemplate2 - JS code for implementing inheritance with attributes + * + * @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 + et2_core_common; +*/ + +import '../jsapi/egw_global'; +import 'et2_core_common'; + +export class ClassWithAttributes +{ + /** + * Object to collect the attributes we operate on + */ + attributes: object; + + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + * + * @param {string} _name + * @return {*} + */ + getAttribute(_name) { + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) + { + if (typeof this["get_" + _name] == "function") + { + return this["get_" + _name](); + } + else + { + return this[_name]; + } + } + else + { + egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); + } + } + + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + * + * @param {string} _name + * @param {*} _value + * @param {boolean} _override + */ + setAttribute(_name, _value, _override) + { + if (typeof this.attributes[_name] != "undefined") + { + if (!this.attributes[_name].ignore) + { + if (typeof _override == "undefined") + { + _override = true; + } + + var val = et2_checkType(_value, this.attributes[_name].type, + _name, this); + + if (typeof this["set_" + _name] == "function") + { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") + { + this[_name] = val; + } + } + } + else + { + egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); + } + } + + /** + * generateAttributeSet sanitizes the given associative array of attributes + * (by passing each entry to "et2_checkType" and checking for existance of + * the attribute) and adds the default values to the associative array. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + generateAttributeSet(_attrs) + { + // Sanity check and validation + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined") + { + if (!this.attributes[key].ignore) + { + _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, + key, this); + } + } + else + { + // Key does not exist - delete it and issue a warning + delete(_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type+"!"); + } + } + + // Include default values or already set values for this attribute + for (var key in this.attributes) + { + if (typeof _attrs[key] == "undefined") + { + var _default = this.attributes[key]["default"]; + if (_default == et2_no_init) + { + _default = undefined; + } + + _attrs[key] = _default; + } + } + + return _attrs; + } + + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + initAttributes(_attrs) + { + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) + { + this.setAttribute(key, _attrs[key], false); + } + } + } + + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + static extendAttributes(_attributes : object, _parent : object) : object + { + function _copyMerge(_new, _old) + { + var result = {}; + + // Copy the new object + if (typeof _new != "undefined") + { + for (var key in _new) + { + result[key] = _new[key]; + } + } + + // Merge the old object + for (var key in _old) + { + if (typeof result[key] == "undefined") + { + result[key] = _old[key]; + } + } + + return result; + } + + var attributes = {}; + + // Copy the old attributes + for (var key in _attributes) + { + attributes[key] = _copyMerge({}, _attributes[key]); + } + + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _parent) + { + var _old = _parent[key]; + + attributes[key] = _copyMerge(attributes[key], _old); + } + + // Validate the attributes + for (var key in attributes) + { + et2_validateAttrib(key, attributes[key]); + } + + return attributes; + } +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_interfaces.js b/api/js/etemplate/et2_core_interfaces.js index dc87241236..86bf331979 100644 --- a/api/js/etemplate/et2_core_interfaces.js +++ b/api/js/etemplate/et2_core_interfaces.js @@ -6,156 +6,39 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - -/*egw:uses - et2_core_inheritance; -*/ - /** - * Interface for all widget classes, which are based on a DOM node. + * Checks if an object / et2_widget implements given methods + * + * @param obj + * @param methods */ -var et2_IDOMNode = new Interface({ - /** - * Returns the DOM-Node of the current widget. The return value has to be - * a plain DOM node. If you want to return an jQuery object as you receive - * it with - * - * obj = jQuery(node); - * - * simply return obj[0]; - * - * @param _sender The _sender parameter defines which widget is asking for - * the DOMNode. Depending on that, the widget may return different nodes. - * This is used in the grid. Normally the _sender parameter can be omitted - * in most implementations of the getDOMNode function. - * However, you should always define the _sender parameter when calling - * getDOMNode! - */ - getDOMNode: function(_sender) {} -}); - -/** - * Interface for all widgets which support returning a value - */ -var et2_IInput = new Interface({ - /** - * getValue has to return the value of the input widget - */ - getValue: function() {}, - - /** - * Is dirty returns true if the value of the widget has changed since it - * was loaded. - */ - isDirty: function() {}, - - /** - * Causes the dirty flag to be reseted. - */ - resetDirty: function() {}, - - /** - * Checks the data to see if it is valid, as far as the client side can tell. - * Return true if it's not possible to tell on the client side, because the server - * will have the chance to validate also. - * - * The messages array is to be populated with everything wrong with the data, - * so don't stop checking after the first problem unless it really makes sense - * to ignore other problems. - * - * @param {String[]} messages List of messages explaining the failure(s). - * messages should be fairly short, and already translated. - * - * @return {boolean} True if the value is valid (enough), false to fail - */ - isValid: function(messages) {} -}); - -/** - * Interface for widgets which should be automatically resized - */ -var et2_IResizeable = new Interface({ - /** - * Called whenever the window is resized - */ - resize: function() {} -}); - -/** - * Interface for widgets which have the align attribute - */ -var et2_IAligned = new Interface({ - get_align: function() {} -}); - -/** - * Interface for widgets which want to e.g. perform clientside validation before - * the form is submitted. - */ -var et2_ISubmitListener = new Interface({ - /** - * Called whenever the template gets submitted. Return false if you want to - * stop submission. - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit: function(_values) {} -}); - -/** - * Interface all widgets must support which can operate on given DOM-Nodes. This - * is used in grid lists, when only a single row gets really stored in the widget - * tree and this instance has to work on multiple copies of the DOM-Tree elements. - */ -var et2_IDetachedDOM = new Interface({ - - /** - * Creates a list of attributes which can be set when working in the - * "detached" mode. The result is stored in the _attrs array which is provided - * by the calling code. - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) {}, - - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes: function() {}, - - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which have to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes: function(_nodes, _values) {} - -}); - -/** - * Interface for widgets that need to do something special before printing - */ -var et2_IPrint = new Interface({ - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint: function() {}, - - /** - * Reset after printing - */ - afterPrint: function() {} -}); \ No newline at end of file +function implements_methods(obj, methods) { + for (var i = 0; i < methods.length; ++i) { + if (typeof obj[methods[i]] !== 'function') { + return false; + } + } + return true; +} +function implements_et2_IDOMNode(obj) { + return implements_methods(obj, ["getDOMNode"]); +} +function implements_et2_IInput(obj) { + return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); +} +function implements_et2_IResizeable(obj) { + return implements_methods(obj, ["resize"]); +} +function implements_et2_IAligned(obj) { + return implements_methods(obj, ["get_align"]); +} +function implements_et2_ISubmitListener(obj) { + return implements_methods(obj, ["submit"]); +} +function implements_et2_IDetachedDOM(obj) { + return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); +} +function implements_et2_IPrint(obj) { + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts new file mode 100644 index 0000000000..ddc1e853bd --- /dev/null +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -0,0 +1,208 @@ +/** + * EGroupware eTemplate2 - File which contains all interfaces + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/** + * Checks if an object / et2_widget implements given methods + * + * @param obj + * @param methods + */ +function implements_methods(obj : et2_widget, methods : string[]) : boolean +{ + for(let i=0; i < methods.length; ++i) + { + if (typeof obj[methods[i]] !== 'function') + { + return false; + } + } + return true; +} + +/** + * Interface for all widget classes, which are based on a DOM node. + */ +interface et2_IDOMNode +{ + /** + * Returns the DOM-Node of the current widget. The return value has to be + * a plain DOM node. If you want to return an jQuery object as you receive + * it with + * + * obj = jQuery(node); + * + * simply return obj[0]; + * + * @param _sender The _sender parameter defines which widget is asking for + * the DOMNode. Depending on that, the widget may return different nodes. + * This is used in the grid. Normally the _sender parameter can be omitted + * in most implementations of the getDOMNode function. + * However, you should always define the _sender parameter when calling + * getDOMNode! + */ + getDOMNode(_sender? : et2_widget) : HTMLElement +} +function implements_et2_IDOMNode(obj : et2_widget) +{ + return implements_methods(obj, ["getDOMNode"]); +} + +/** + * Interface for all widgets which support returning a value + */ +interface et2_IInput +{ + /** + * getValue has to return the value of the input widget + */ + getValue() : any + + /** + * Is dirty returns true if the value of the widget has changed since it + * was loaded. + */ + isDirty() : boolean + + /** + * Causes the dirty flag to be reseted. + */ + resetDirty() : void + + /** + * Checks the data to see if it is valid, as far as the client side can tell. + * Return true if it's not possible to tell on the client side, because the server + * will have the chance to validate also. + * + * The messages array is to be populated with everything wrong with the data, + * so don't stop checking after the first problem unless it really makes sense + * to ignore other problems. + * + * @param {String[]} messages List of messages explaining the failure(s). + * messages should be fairly short, and already translated. + * + * @return {boolean} True if the value is valid (enough), false to fail + */ + isValid(messages) : boolean +} +function implements_et2_IInput(obj : et2_widget) +{ + return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); +} + +/** + * Interface for widgets which should be automatically resized + */ +interface et2_IResizeable +{ + /** + * Called whenever the window is resized + */ + resize() : void +} +function implements_et2_IResizeable(obj : et2_widget) +{ + return implements_methods(obj, ["resize"]); +} + +/** + * Interface for widgets which have the align attribute + */ +interface et2_IAligned +{ + get_align() : string +} +function implements_et2_IAligned(obj : et2_widget) +{ + return implements_methods(obj, ["get_align"]); +} + +/** + * Interface for widgets which want to e.g. perform clientside validation before + * the form is submitted. + */ +interface et2_ISubmitListener +{ + /** + * Called whenever the template gets submitted. Return false if you want to + * stop submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + submit(_values) : void +} +function implements_et2_ISubmitListener(obj : et2_widget) +{ + return implements_methods(obj, ["submit"]); +} + +/** + * Interface all widgets must support which can operate on given DOM-Nodes. This + * is used in grid lists, when only a single row gets really stored in the widget + * tree and this instance has to work on multiple copies of the DOM-Tree elements. + */ +interface et2_IDetachedDOM +{ + + /** + * Creates a list of attributes which can be set when working in the + * "detached" mode. The result is stored in the _attrs array which is provided + * by the calling code. + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs : string[]) : void + + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + getDetachedNodes() : HTMLElement[] + + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which have to be in the same order as + * the nodes returned by "getDetachedNodes" + * @param _values is an associative array which contains a subset of attributes + * returned by the "getDetachedAttributes" function and sets them to the + * given values. + */ + setDetachedAttributes(_nodes : HTMLElement[], _values : object) : void + +} +function implements_et2_IDetachedDOM(obj : et2_widget) +{ + return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); +} + +/** + * Interface for widgets that need to do something special before printing + */ +interface et2_IPrint +{ + /** + * Set up for printing + * + * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up + * (waiting for data) + */ + beforePrint() : JQueryPromise | void + + /** + * Reset after printing + */ + afterPrint() : void +} +function implements_et2_IPrint(obj : et2_widget) +{ + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index fc3c0623ff..b07590740b 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -1,29 +1,40 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api - * @link http://www.egroupware.org + * @link https://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - jsapi.egw; - et2_core_xml; - et2_core_common; - et2_core_inheritance; - et2_core_arrayMgr; + jsapi.egw; + et2_core_xml; + et2_core_common; + et2_core_inheritance; + et2_core_arrayMgr; */ - +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * The registry contains all XML tag names and the corresponding widget * constructor. */ var et2_registry = {}; - /** * Registers the widget class defined by the given constructor and associates it * with the types in the _types array. @@ -31,27 +42,21 @@ var et2_registry = {}; * @param {function} _constructor constructor * @param {array} _types widget types _constructor wants to register for */ -function et2_register_widget(_constructor, _types) -{ - "use strict"; - - // 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; - } +function et2_register_widget(_constructor, _types) { + "use strict"; + // 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; + } } - +exports.et2_register_widget = et2_register_widget; /** * 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 @@ -65,1015 +70,832 @@ function et2_register_widget(_constructor, _types) * is not passed, it will default to null. Then you have to attach the element * to a parent using the addChild or insertChild method. */ -function et2_createWidget(_name, _attrs, _parent) -{ - "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 - var constructor = typeof et2_registry[nodeName] == "undefined" ? - et2_placeholder : et2_registry[nodeName]; - if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") - { - constructor = et2_registry[nodeName + "_ro"]; - } - - // Do an sanity check for the attributes - constructor.prototype.generateAttributeSet(_attrs); - - // Create the new widget and return it - return new constructor(_parent, _attrs); +function et2_createWidget(_name, _attrs, _parent) { + "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 + var constructor = typeof et2_registry[nodeName] == "undefined" ? + et2_placeholder : et2_registry[nodeName]; + if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") { + constructor = et2_registry[nodeName + "_ro"]; + } + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(_attrs); + // Create the new widget and return it + return new constructor(_parent, _attrs); } - +exports.et2_createWidget = et2_createWidget; /** * The et2 widget base class. * * @augments ClassWithAttributes */ -var et2_widget = (function(){ "use strict"; return ClassWithAttributes.extend( -{ - attributes: { - "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. - legacyOptions: [], - - /** - * Set this variable to true if this widget can have namespaces - */ - createNamespace: false, - - /** - * The init function is the constructor of the widget. When deriving new - * classes from the widget base class, always call this constructor unless - * you know what you're doing. - * - * @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. - * @memberOf et2_widget - */ - init: function(_parent, _attrs) { - - // 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]; - - if (_attrs["id"]) - { - // Create a namespace for this object - if (this.createNamespace) - { - this.checkCreateNamespace(); - } - } - - 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: function() { - - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - // 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].free(); - } - } - }, - - /** - * 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: function(_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: function(_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); - }, - - /** - * Returns the parent widget of this widget - */ - getParent: function() { - return this._parent; - }, - - /** - * Returns the list of children of this widget. - */ - getChildren: function() { - return this._children; - }, - - /** - * Returns the base widget - */ - getRoot: function() { - 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: function(_node) { - 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: function(_node, _idx) { - // 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); - - if(_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) - { - _node.detachFromDOM(); - _node.parentNode = this.getDOMNode(_node); - _node.attachToDOM(); - } - - } - 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: function(_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: function(_id) { - 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 = 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: function(_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: function(_sender, _vis) { - if (typeof _vis == "undefined") - { - _vis = true; - } - - if (this._parent) - { - return _vis && this._parent.isInTree(this); - } - - return _vis; - }, - - isOfSupportedWidgetClass: function(_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: function(_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.legacyOptions.length > 0) - { - // 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 = 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 < _proto.legacyOptions.length; j++) - { - // Blank = not set, unless there's more legacy options provided after - if(splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) continue; - - // Check to make sure we don't overwrite a current option with a legacy option - if(typeof _target[_proto.legacyOptions[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 == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) - { - attrValue = splitted.slice(j); - } - - var attr = _proto.attributes[_proto.legacyOptions[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[_proto.legacyOptions[j]] = attrValue; - } - } - } - else if (attrName == "readonly" && typeof _target[attrName] != "undefined") - { - // do NOT overwrite already evaluated readonly attribute - } - else - { - if (mgr != null && typeof _proto.attributes[attrName] != "undefined") - { - var attr = _proto.attributes[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: function(_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"])) - { - _attrs[key] = this.egw().lang(_attrs[key]); - } - } - } - }, - - /** - * 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 - * - * @return et2_widget - */ - createElementFromNode: function(_node) { - 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 = 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 - var 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 = typeof et2_registry[_nodeName] == "undefined" ? - et2_placeholder : et2_registry[_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 - constructor.prototype.generateAttributeSet(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: function(_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: function(_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: function(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.done) - { - // Warn if list was not provided - if(warn_if_deferred) - { - // Might not be a problem, but if you need the widget to be really loaded, it could be - egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); - } - // Widget is waiting. Add to the list - promises.push(result); - // Fihish loading when it's finished - result.done(jQuery.proxy(loadChildren, this)); - } - }, - - /** - * 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: function(_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.instanceOf(et2_inputWidget) ? this.getInputNode() : - (this.implements(et2_IDOMNode) ? 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 http://api.jquery.com/deferred.promise/|jQuery Promise} - */ - doLoadingFinished: function() { - return true; - }, - - /** - * 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: function() { - // The _egw property is not set - if (typeof this._egw === 'undefined') - { - if (this._parent != null) - { - return this._parent.egw(); - } - - // Get the window this object belongs to - var wnd = null; - if (this.implements(et2_IDOMNode)) - { - var node = this.getDOMNode(); - if(node && node.ownerDocument) - { - wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; - } - } - - // If we're the root object, return the phpgwapi API instance - return egw('phpgwapi', wnd); - } - - return this._egw; - }, - - /** - * Sets the client side api instance. It can be retrieved by the widget tree - * by using the "egw()" function. - * - * @param {egw} _egw egw object to set - */ - setApiInstance: function(_egw) { - this._egw = _egw; - }, - - /** - * Sets all array manager objects - this function can be used to set the - * root array managers of the container object. - * - * @param {object} _mgrs - */ - setArrayMgrs: function(_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: function(_mgrs) { - 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: function(_part, _mgr) { - this._mgrs[_part] = _mgr; - }, - - /** - * Returns the array manager object for the given part - * - * @param {string} _part name of array mgr to return - */ - getArrayMgr: function(_part) { - if (this._mgrs && typeof this._mgrs[_part] != "undefined") - { - return this._mgrs[_part]; - } - else if (this._parent) - { - return this._parent.getArrayMgr(_part); - } - - 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. - */ - checkCreateNamespace: function() { - // 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]); - } - } - }, - - /** - * Sets the instance manager object (of type etemplate2, see etemplate2.js) - * - * @param {etemplate2} _inst - */ - setInstanceManager: function(_inst) { - this._inst = _inst; - }, - - /** - * Returns the instance manager - * - * @return {etemplate2} - */ - getInstanceManager: function() { - 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: function() { - 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; - } -});}).call(this); - +var et2_widget = /** @class */ (function (_super) { + __extends(et2_widget, _super); + /** + * 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 + */ + function et2_widget(_parent, _attrs, _child) { + var _this = _super.call(this) || this; + _this._children = []; + _this._mgrs = {}; + /** + * This is used and therefore it we can not (yet) make it private + * + * @deprecated use this.getInstanceMgr() + */ + _this._inst = null; + _this.attributes = et2_core_inheritance_1.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]; + if (_attrs["id"]) { + // Create a namespace for this object + if (_this.createNamespace) { + _this.checkCreateNamespace(); + } + } + 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); + return _this; + } + /** + * 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. + */ + et2_widget.prototype.destroy = function () { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].free(); + } + // 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].free(); + } + } + }; + /** + * 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 + */ + et2_widget.prototype.clone = function (_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; + }; + et2_widget.prototype.assign = function (_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); + }; + /** + * Returns the parent widget of this widget + */ + et2_widget.prototype.getParent = function () { + return this._parent; + }; + /** + * Returns the list of children of this widget. + */ + et2_widget.prototype.getChildren = function () { + return this._children; + }; + /** + * Returns the base widget + */ + et2_widget.prototype.getRoot = function () { + 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 + */ + et2_widget.prototype.addChild = function (_node) { + 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. + */ + et2_widget.prototype.insertChild = function (_node, _idx) { + // 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); + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) { + _node.detachFromDOM(); + _node.parentNode = this.getDOMNode(_node); + _node.attachToDOM(); + } + } + 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 + */ + et2_widget.prototype.removeChild = function (_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 + */ + et2_widget.prototype.getWidgetById = function (_id) { + 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 = 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. + */ + et2_widget.prototype.iterateOver = function (_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. + */ + et2_widget.prototype.isInTree = function (_sender, _vis) { + if (typeof _vis == "undefined") { + _vis = true; + } + if (this._parent) { + return _vis && this._parent.isInTree(this); + } + return _vis; + }; + et2_widget.prototype.isOfSupportedWidgetClass = function (_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 + */ + et2_widget.prototype.parseXMLAttrs = function (_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.legacyOptions.length > 0) { + // 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 = 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 < _proto.legacyOptions.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) + continue; + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[_proto.legacyOptions[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 == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) { + attrValue = splitted.slice(j); + } + var attr = _proto.attributes[_proto.legacyOptions[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[_proto.legacyOptions[j]] = attrValue; + } + } + } + else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } + else { + if (mgr != null && typeof _proto.attributes[attrName] != "undefined") { + var attr = _proto.attributes[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 + */ + et2_widget.prototype.transformAttributes = function (_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"])) { + _attrs[key] = this.egw().lang(_attrs[key]); + } + } + } + }; + /** + * 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 + * + * @return et2_widget + */ + et2_widget.prototype.createElementFromNode = function (_node) { + 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 = 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 + var 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 = typeof et2_registry[_nodeName] == "undefined" ? + et2_placeholder : et2_registry[_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 + constructor.prototype.generateAttributeSet(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 + */ + et2_widget.prototype.loadFromXML = function (_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 + */ + et2_widget.prototype.loadContent = function (_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. + */ + et2_widget.prototype.loadingFinished = function (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.done) { + // Warn if list was not provided + if (warn_if_deferred) { + // Might not be a problem, but if you need the widget to be really loaded, it could be + egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); + } + // Widget is waiting. Add to the list + promises.push(result); + // Fihish loading when it's finished + result.done(jQuery.proxy(loadChildren, this)); + } + }; + /** + * 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 + */ + et2_widget.prototype.initAttributes = function (_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.instanceOf(et2_inputWidget) ? this.getInputNode() : + (this.implements(et2_IDOMNode) ? 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 http://api.jquery.com/deferred.promise/|jQuery Promise} + */ + et2_widget.prototype.doLoadingFinished = function () { + return true; + }; + /** + * 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. + */ + et2_widget.prototype.egw = function () { + // The _egw property is not set + if (typeof this._egw === 'undefined') { + if (this._parent != null) { + return this._parent.egw(); + } + // Get the window this object belongs to + var wnd = null; + if (this.implements(et2_IDOMNode)) { + var node = this.getDOMNode(); + if (node && node.ownerDocument) { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + return this._egw; + }; + /** + * Sets the client side api instance. It can be retrieved by the widget tree + * by using the "egw()" function. + * + * @param {egw} _egw egw object to set + */ + et2_widget.prototype.setApiInstance = function (_egw) { + this._egw = _egw; + }; + /** + * Sets all array manager objects - this function can be used to set the + * root array managers of the container object. + * + * @param {object} _mgrs + */ + et2_widget.prototype.setArrayMgrs = function (_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. + */ + et2_widget.prototype.getArrayMgrs = function (_mgrs) { + 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 + */ + et2_widget.prototype.setArrayMgr = function (_part, _mgr) { + this._mgrs[_part] = _mgr; + }; + /** + * Returns the array manager object for the given part + * + * @param {string} _part name of array mgr to return + */ + et2_widget.prototype.getArrayMgr = function (_part) { + if (this._mgrs && typeof this._mgrs[_part] != "undefined") { + return this._mgrs[_part]; + } + else if (this._parent) { + return this._parent.getArrayMgr(_part); + } + 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. + */ + et2_widget.prototype.checkCreateNamespace = function () { + // 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]); + } + } + }; + /** + * Sets the instance manager object (of type etemplate2, see etemplate2.js) + * + * @param {etemplate2} _inst + */ + et2_widget.prototype.setInstanceManager = function (_inst) { + this._inst = _inst; + }; + /** + * Returns the instance manager + * + * @return {etemplate2} + */ + et2_widget.prototype.getInstanceManager = function () { + 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 + */ + et2_widget.prototype.getPath = function () { + 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; + }; + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + et2_widget.prototype.implements = function (_iface_name) { + if (typeof window['implements_' + _iface_name] === 'function' && + window['implements_' + _iface_name](this)) { + return true; + } + return false; + }; + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + et2_widget.prototype.instanceOf = function (_class_or_interfacename) { + if (typeof _class_or_interfacename === 'string') { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + }; + et2_widget._attributes = { + "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" + } + }; + return et2_widget; +}(et2_core_inheritance_1.ClassWithAttributes)); +exports.et2_widget = et2_widget; diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts new file mode 100644 index 0000000000..bfbf62716f --- /dev/null +++ b/api/js/etemplate/et2_core_widget.ts @@ -0,0 +1,1066 @@ +/** + * 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'; + +/** + * The registry contains all XML tag names and the corresponding widget + * constructor. + */ +var et2_registry = {}; + +/** + * Registers the widget class defined by the given constructor 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"; + + // 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 + var constructor = typeof et2_registry[nodeName] == "undefined" ? + et2_placeholder : et2_registry[nodeName]; + if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") + { + constructor = et2_registry[nodeName + "_ro"]; + } + + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(_attrs); + + // Create the new widget and return it + return new constructor(_parent, _attrs); +} + +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. + legacyOptions: []; + + private _type: string; + id: string; + supportedWidgetClasses : any[]; + options: WidgetConfig; + readonly: boolean; + + /** + * Set this variable to true if this widget can have namespaces + */ + createNamespace: false; + + /** + * 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]; + + if (_attrs["id"]) { + // Create a namespace for this object + if (this.createNamespace) { + this.checkCreateNamespace(); + } + } + + 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].free(); + } + + // 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].free(); + } + } + } + + /** + * 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); + } + + private _parent: et2_widget; + + /** + * Returns the parent widget of this widget + */ + getParent() + { + return this._parent; + } + + private _children = []; + + /** + * Returns the list of children of this widget. + */ + getChildren() + { + return this._children; + } + + /** + * Returns the base widget + */ + getRoot() + { + 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) + { + 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, _idx) + { + // 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); + + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) { + _node.detachFromDOM(); + _node.parentNode = (this).getDOMNode(_node); + _node.attachToDOM(); + } + + } 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) + { + 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 = 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.legacyOptions.length > 0) { + // 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 = 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 < _proto.legacyOptions.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) continue; + + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[_proto.legacyOptions[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 == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) { + attrValue = splitted.slice(j); + } + + var attr = _proto.attributes[_proto.legacyOptions[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[_proto.legacyOptions[j]] = attrValue; + } + } + } else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } else { + if (mgr != null && typeof _proto.attributes[attrName] != "undefined") { + var attr = _proto.attributes[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"])) { + _attrs[key] = this.egw().lang(_attrs[key]); + } + } + } + } + + /** + * 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 + * + * @return et2_widget + */ + createElementFromNode(_node) + { + 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 = 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 + var 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 = typeof et2_registry[_nodeName] == "undefined" ? + et2_placeholder : et2_registry[_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 + constructor.prototype.generateAttributeSet(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: + * + * var promises = []; + * widget.loadingFinished(promises); + * jQuery.when.apply(null, promises).done( doneCallback ); + * + * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} + * + * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. + */ + loadingFinished(promises) + { + // Call all availble setters + this.initAttributes(this.options); + + // Make sure promises is defined to avoid errors. + // We'll warn (below) if programmer should have passed it. + if (typeof promises == "undefined") { + promises = []; + var warn_if_deferred = true; + } + + var loadChildren = function () { + // Descend recursively into the tree + for (var i = 0; i < this._children.length; i++) { + try { + this._children[i].loadingFinished(promises); + } catch (e) { + egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack); + } + } + }; + + var result = this.doLoadingFinished(); + if (typeof result == "boolean" && result) { + // Simple widget finishes nicely + loadChildren.apply(this, arguments); + } else if (typeof result == "object" && result.done) { + // Warn if list was not provided + if (warn_if_deferred) { + // Might not be a problem, but if you need the widget to be really loaded, it could be + egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); + } + // Widget is waiting. Add to the list + promises.push(result); + // Fihish loading when it's finished + result.done(jQuery.proxy(loadChildren, this)); + } + } + + /** + * 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.instanceOf(et2_inputWidget) ? (this).getInputNode() : + (this.implements(et2_IDOMNode) ? (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 http://api.jquery.com/deferred.promise/|jQuery Promise} + */ + doLoadingFinished() : JQueryPromise | boolean + { + return true; + } + + private _egw: object; + + /** + * 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() + { + // The _egw property is not set + if (typeof this._egw === 'undefined') { + if (this._parent != null) { + return this._parent.egw(); + } + + // Get the window this object belongs to + var wnd = null; + if (this.implements(et2_IDOMNode)) { + var node = (this).getDOMNode(); + if (node && node.ownerDocument) { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + + return this._egw; + } + + /** + * Sets the client side api instance. It can be retrieved by the widget tree + * by using the "egw()" function. + * + * @param {egw} _egw egw object to set + */ + setApiInstance(_egw) + { + this._egw = _egw; + } + + private _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) + { + this._mgrs[_part] = _mgr; + } + + /** + * Returns the array manager object for the given part + * + * @param {string} _part name of array mgr to return + */ + getArrayMgr(_part : string) + { + if (this._mgrs && typeof this._mgrs[_part] != "undefined") { + return this._mgrs[_part]; + } else if (this._parent) { + return this._parent.getArrayMgr(_part); + } + + 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. + */ + 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]); + } + } + } + + /** + * 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; + } + + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + implements (_iface_name : string) + { + if (typeof window['implements_'+_iface_name] === 'function' && + window['implements_'+_iface_name](this)) + { + return true + } + return false; + } + + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + instanceOf(_class_or_interfacename: any) : boolean + { + if (typeof _class_or_interfacename === 'string') + { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + } +} diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index c6616da7c3..f94c8e206a 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -3,18 +3,21 @@ declare module eT2 } declare var etemplate2 : any; -declare var et2_DOMWidget : any; +declare class et2_widget{} +declare class et2_DOMWidget{} +declare class et2_inputWidget{ + getInputNode() : HTMLElement +} declare var et2_surroundingsMgr : any; declare var et2_arrayMgr : any; declare var et2_readonlysArrayMgr : any; declare var et2_baseWidget : any; declare var et2_container : any; declare var et2_placeholder : any; -declare var et2_validTypes : any; -declare var et2_typeDefaults : any; -declare var et2_no_init : any; +declare var et2_validTypes : string[]; +declare var et2_typeDefaults : object; +//declare const et2_no_init : object; declare var et2_editableWidget : any; -declare var et2_inputWidget : any; declare var et2_IDOMNode : any; declare var et2_IInput : any; declare var et2_IResizeable : any; @@ -23,8 +26,7 @@ declare var et2_ISubmitListener : any; declare var et2_IDetachedDOM : any; declare var et2_IPrint : any; declare var et2_valueWidget : any; -declare var et2_registry : any; -declare var et2_widget : any; +declare var et2_registry : {}; declare var et2_dataview : any; declare var et2_dataview_controller : any; declare var et2_dataview_selectionManager : any; @@ -145,4 +147,5 @@ declare var et2_vfsSelect : any; declare var et2_video : any; declare var et2_IExposable : any; declare function et2_createWidget(type : string, params : {}, parent? : any) : any; -declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; \ No newline at end of file +declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; +declare function et2_compileLegacyJS(_code : string, _widget : et2_widget, _context? : HTMLElement) : Function; \ No newline at end of file diff --git a/api/js/jsapi/app_base.js b/api/js/jsapi/app_base.js index 1e9085c17b..488bfcbaa2 100644 --- a/api/js/jsapi/app_base.js +++ b/api/js/jsapi/app_base.js @@ -201,6 +201,28 @@ var AppJS = (function(){ "use strict"; return Class.extend( }, + /** + * Push method receives push notification about updates to entries from the application + * + * It can use the extra _data parameter to determine if the client has read access to + * the entry - if an update of the list is necessary. + * + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {mixed} _data eg. owner or responsible to decide if update is necessary + * @returns {undefined} + */ + push: function(_type, _app, _id, _data) + { + + }, + /** * Open an entry. * diff --git a/api/js/jsapi/egw_global.d.ts b/api/js/jsapi/egw_global.d.ts index 3e26f06d7f..0e8acbf0fd 100644 --- a/api/js/jsapi/egw_global.d.ts +++ b/api/js/jsapi/egw_global.d.ts @@ -10,6 +10,7 @@ declare var egw : any; declare var app : {classes: any}; declare var egw_globalObjectManager : any; declare var framework : any; +declare var egw_LAB : any; declare var mailvelope : any;